Public: anyone can read this; the crew can change it.

/migrate and a migration's job page speak the home page's language #9827

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9814-migrate-and-a-migration-s-job-page-speak → main 2 commits fork

  • Merged. scout/9814-migrate-and-a-migration-s-job-page-speak was merged into main as 06cba9a.

Review

Sign in to review this change.
2 files changed +117 −22
src/web/pages/migrate.ts +14 −22
@@ -13,7 +13,7 @@
1313 * commits and selects the one the app most likely lives on — the person's
1414 * newest (`origins/work-branch.ts`); the Migrate button carries the choice.
1515 *
16 * An account whose token sees public repositories only says so on its card
16 * An account whose token sees public repositories only says so under its account
1717 * (`origins/token-reach.ts`), with GitHub's new-token page one click away
1818 * and a field that replaces the token in place.
1919 *
@@ -55,7 +55,6 @@ import { flashTo, page, redirect } from "../layout.ts";
5555 import {
5656 badge,
5757 btn,
58 card,
5958 icon,
6059 row,
6160 selectField,
@@ -295,8 +294,9 @@ function accounts(ctx: Ctx, origins: readonly OriginChoices[]): Html {
295294 <a href="/settings/origins">Settings → Origins</a>; a token pasted
296295 there works until then.
297296 </p>`;
298 return card(
299 html`${origins.length === 0
297 return html`<section class="mb" id="migrate-accounts-section">
298 <h2 class="mb">Your accounts</h2>
299 ${origins.length === 0
300300 ? null
301301 : html`<ul class="list" id="migrate-accounts">
302302 ${origins.map((origin) =>
@@ -315,12 +315,8 @@ function accounts(ctx: Ctx, origins: readonly OriginChoices[]): Html {
315315 )}
316316 </ul>`}
317317 ${origins.filter((o) => o.reach === "public").map(publicOnly)}
318 ${connect}`,
319 {
320 head: html`${icon("globe")}
321 <h2>Your accounts</h2>`,
322 },
323 );
318 ${connect}
319 </section>`;
324320 }
325321
326322 /**
@@ -457,9 +453,8 @@ function migrateForm(
457453 </p>`}
458454 ${accounts(ctx, origins)}
459455 <form method="get" action="/migrate" id="migrate-search"></form>
460 ${card(
461 html`<form method="post" action="/migrate">
462 <h3 class="mb">Source</h3>
456 <form method="post" action="/migrate">
457 <h2 class="mb">Source</h2>
463458 ${picker(origins, values, crew.length > 0, branches)}
464459 ${formField({
465460 label: origins.every((o) => o.repos.length === 0)
@@ -473,7 +468,7 @@ function migrateForm(
473468 autofocus: origins.every((o) => o.repos.length === 0),
474469 hint: "Any public repository over https, http or git. It is cloned anonymously.",
475470 })}
476 <h3 class="mb mt">Destination</h3>
471 <h2 class="mb mt">Destination</h2>
477472 ${selectField({
478473 label: "Owner",
479474 name: "owner",
@@ -507,7 +502,7 @@ function migrateForm(
507502 },
508503 ],
509504 })}
510 <h3 class="mb mt">Put a crew on it</h3>
505 <h2 class="mb mt">Put a crew on it</h2>
511506 ${crew.length === 0
512507 ? html`<p class="muted small mb">
513508 ${icon("bot")} No agents on this forge yet, so there is no crew to
@@ -550,7 +545,7 @@ function migrateForm(
550545 >
551546 </label>
552547 </div>`}
553 <h3 class="mb mt">What happens to the GitHub repository</h3>
548 <h2 class="mb mt">What happens to the GitHub repository</h2>
554549 ${selectField({
555550 label: "Afterwards",
556551 name: "after",
@@ -577,8 +572,7 @@ function migrateForm(
577572 ${btn("Migrate", { variant: "primary", icon: "download" })}
578573 ${btn("Cancel", { href: "/", variant: "ghost" })}
579574 </div>
580 </form>`,
581 )}
575 </form>
582576 </div>`;
583577 }
584578
@@ -851,10 +845,8 @@ export function migrationJobPage(ctx: Ctx): Response {
851845 ...(failed ? {} : { scriptJs: migrateJobJs }),
852846 body: html`${pageHead(`Migrating ${job.fullName}`)}
853847 <div class="narrow-wide" id="migrate-job" data-job="${job.id}">
854 ${card(stageRows(job), {
855 head: html`${icon("download")}
856 <h2>${job.fullName}</h2>`,
857 })}
848 <h2 class="mb">${job.fullName}</h2>
849 ${stageRows(job)}
858850 ${failed
859851 ? html`<div class="field" data-invalid>
860852 <span class="error" role="alert" id="migrate-job-err"
test/migrate-language.test.ts added +103
@@ -0,0 +1,103 @@
1 /** /migrate and a migration's job page speak the home page's language (#9814): headings and rows, no card. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp } from "../src/app.ts";
7 import { connectOrigin } from "../src/actions/origins.ts";
8 import { createSession } from "../src/core/sessions.ts";
9 import { createUser, type User } from "../src/core/users.ts";
10 import { serve } from "../src/http/server.ts";
11 import { advance, createJob } from "../src/origins/jobs.ts";
12 import { forgetProviders } from "../src/origins/providers/index.ts";
13 import { newFakeState, serveFakeForge } from "./fake-origin.ts";
14 import { promote } from "./helpers/members.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 let dir = "";
20 let app: ReturnType<typeof createApp>;
21 let servers: Awaited<ReturnType<typeof serve>>;
22 let cookie = "";
23 let other = "";
24 let alice: User;
25 const person = async (username: string): Promise<User> => {
26 const made = await createUser(app.db, {
27 origin: "seed",
28 trusted: true,
29 username,
30 email: `${username}@forge.test`,
31 adminIfFirst: false,
32 });
33 if (!made.ok) throw new Error(String(made.error));
34 promote(app.db, made.value);
35 return made.value;
36 };
37
38 beforeAll(async () => {
39 dir = await mkdtemp(join(tmpdir(), "forge-migrate-lang-"));
40 app = createApp({ dataDir: dir, port: 0 });
41 servers = await serve(app);
42 alice = await person("alice");
43 cookie = `forge_session=${createSession(app.db, alice.id).token}`;
44 const bob = await person("bob");
45 other = `forge_session=${createSession(app.db, bob.id).token}`;
46 });
47 afterAll(async () => {
48 servers.stop();
49 app.close();
50 await rm(dir, { recursive: true, force: true });
51 });
52
53 const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>"));
54 const get = async (path: string, who = cookie): Promise<string> => {
55 const res = await fetch(servers.url + path, { headers: { cookie: who }, redirect: "manual" });
56 expect(res.status).toBe(200);
57 return main(await res.text());
58 };
59 const noCard = (page: string): void => {
60 expect(page).not.toContain('class="card');
61 expect(page).not.toContain("card-body");
62 expect(page).toContain("<h2");
63 };
64
65 test("/migrate with no account: headings, the URL field and Migrate, no card", async () => {
66 const page = await get("/migrate", other);
67 noCard(page);
68 expect(page).toContain("Your accounts");
69 expect(page).toContain("Migrate");
70 expect(page).toContain('name="url"');
71 });
72
73 test("/migrate with an account: one row for it under a heading, no card", async () => {
74 const github = serveFakeForge(newFakeState("github", "octo", "t0ken-secret"));
75 try {
76 forgetProviders(dir);
77 const connected = await connectOrigin(app, alice, { host: github.url, kind: "github", token: "t0ken-secret" });
78 if (!connected.ok) throw new Error(JSON.stringify(connected.error));
79 const page = await get("/migrate");
80 noCard(page);
81 expect(page).toContain('id="migrate-accounts"');
82 expect(page).toContain("Disconnect");
83 expect(page).not.toContain("t0ken-secret");
84 } finally {
85 github.stop();
86 }
87 });
88
89 test("a job's page: stages are rows under one heading; running streams, failed shows its tone", async () => {
90 const running = createJob("", "octo/<b>x", { kind: "migrate", actorId: alice.id });
91 advance(running.id, { stage: "fetching", detail: "receiving 61%" });
92 const live = await get(`/migrate/jobs/${running.id}`);
93 noCard(live);
94 expect(live).toContain("octo/&lt;b&gt;x");
95 expect(live).toContain("data-stage-name");
96
97 const failed = createJob("", "octo/broken", { kind: "migrate", actorId: alice.id });
98 advance(failed.id, { stage: "failed", error: "boom" });
99 const dead = await get(`/migrate/jobs/${failed.id}`);
100 noCard(dead);
101 expect(dead).toContain("boom");
102 expect(dead).toContain("Try again");
103 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs