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

A migration's progress stages draw §5's row and badge, on the server and in the live update #4637

Merged opened this

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

Merged

scout/forge:scout/4595-a-migration-s-progress-stages-draw-5-s-r → main 11 commits fork 7 threads · 7 unresolved

  • Merged. scout/4595-a-migration-s-progress-stages-draw-5-s-r was merged into main as fb443df.

Review

Sign in to review this change.
10 files changed +148 −66
docs/DESIGN.md +2 −2
@@ -182,7 +182,7 @@ specification.
182182 | **where strip** `whereStrip()` (`scope.ts`) | `.where > .where-place` | `data-state=reading\|present\|absent` — accent dot, success dot, faint dot — the dot is the place's `aria-hidden` glyph span, its aside `.muted` | three places, always in this order and these words; a place is a link where one exists, and the third is the pin toggle | `● codeberg.org · ● workbench · ◆ this machine` |
183183 | **live-browse banner** `liveBanner()` | `.live-note` | — | only on a live browse, above the content, one sentence | one accent line under the tab row |
184184 | **where-it-lives block** | `.where-block` | — | origin, last fetch, Sync now and where pushes land, as one block in the About rail | the same four lines under a rule |
185 | **mirror stages** `stageList()` | `.stages > li` | `data-state=done\|now\|failed\|todo` | the stage order is the markup order; a script updates it in place and goes to the mirror on done; without JavaScript the same markup renders and a `<noscript>` Reload link re-reads it. Nothing here refreshes itself: no `<meta refresh>`, no timer | a checklist with `●`/`◐`/`○` |
185 | **mirror stages** `stageList()` | `ol.list.stages[aria-live=polite] > li > .row` | `data-state=done\|now\|todo\|failed` (failed: the stage the job stopped on, a danger "Failed" badge) | the stage order is the markup order; a script updates it in place and goes to the mirror on done; without JavaScript the same markup renders and a `<noscript>` Reload link re-reads it. Nothing here refreshes itself: no `<meta refresh>`, no timer | a checklist with `●`/`◐`/`○` |
186186 | **origin chip** (`chrome.ts`) | `.repo-origin > .origin-chip` | the forge's mark: `github` / `forgejo` / `gitea` | the older one-line form, still used off the repo header | one `mirrored from owner/name @ host` fact line |
187187 | **origin state badge** `badge()` | `.badge` | `success` healthy · `danger` needs re-auth · `attn` error | the error message sits under the row, never inside the badge | the same three words beside the row |
188188 | **imported author** `originTag()` | `.byline.origin-by` | the origin's mark instead of an avatar | never a link to `/name` — that would be a different person | `▲ login` |
@@ -395,7 +395,7 @@ gone, so this list only shrinks.
395395 A `.field` directly inside a `.bar` drops its bottom margin and the bar aligns to the end, so a labelled input and its button share a baseline
396396 - **`style/components.ts`** — parts of the §5 components that the table does not spell out — the palette's list, the menu's items, a dialog's head and foot, comments, the theme picker — and a few small helpers beside them: `.cat` `.clone` `.clone-text` `.comment` `.comment-body` `.comment-form` `.dash` `.dialog-body` `.dialog-foot` `.dialog-head` `.empty-title` `.error-note` `.field-row` `.form-actions` `.hex` `.ico` `.ico-loader` `.list` `.menu-item` `.menu-panel` `.menu-sep` `.menu-title` `.palette-empty` `.palette-foot` `.palette-group` `.palette-heading` `.palette-input` `.palette-item` `.palette-list` `.passkey` `.passkey-off` `.passkey-on` `.preview` `.sep` `.split` `.sprite` `.swatch` `.table-scroll` `.tell-here` `.theme-apply` `.theme-option` `.tick` `.timeline` `.tl-dot` `.toast-body` `.topbar-search`
397397 - **`style/components.ts`, settings rows** — a list whose rows are settings (limits, capacity): the `<ul class="list setting-list">` lets title and meta wrap instead of ellipsizing, and a row's controls take their own line under 640px. `.row-fields` is the form in that row's trail (input, Save, Default): always visible, never hover-gated, wrapping under the text before it would clip. Use both for any page whose rows show a name, a current value, and a control to change it; use `.row-actions` only for secondary actions (delete, wake, pin, revoke). Flags, telemetry, cloud, people, crew roles and subscription headroom/budget use `.row-fields`. Classes: `.row-fields` `.setting-list`
398 - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: `.bform` `.bgrid` `.bmanage` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.commit-body` `.commit-head` `.domain-records` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.frozen` `.frozen-form` `.frozen-sentence` `.headroom` `.issue-head` `.issue-meta` `.mirror-form` `.number` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.stage-detail` `.stage-dot` `.steps` `.sub` `.sys-pair` `.table-wrap` `.tok` `.where-sep`
398 - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: `.bform` `.bgrid` `.bmanage` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.commit-body` `.commit-head` `.domain-records` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.frozen` `.frozen-form` `.frozen-sentence` `.headroom` `.issue-head` `.issue-meta` `.mirror-form` `.number` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.steps` `.sub` `.sys-pair` `.table-wrap` `.tok` `.where-sep`
399399 - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: `.blob-view` `.ln` `.tok-attribute` `.tok-comment` `.tok-constant` `.tok-decorator` `.tok-deleted` `.tok-emphasis` `.tok-function` `.tok-heading` `.tok-inserted` `.tok-keyword` `.tok-link` `.tok-meta` `.tok-number` `.tok-operator` `.tok-plain` `.tok-property` `.tok-punctuation` `.tok-regex` `.tok-string` `.tok-tag` `.tok-type` `.tok-variable`
400400 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
401401 - **`style/content.ts`, diff viewer** — the diff table: its gutters, line kinds, file head and inline review comments: `.add` `.del` `.dh` `.diff` `.diff-body` `.diff-head` `.diff-note` `.diff-path` `.diff-stat` `.diff-stub` `.diff-summary` `.diff-table` `.dl` `.dl-review` `.dn` `.dn-comment` `.nonl` `.rc-hidden` `.rc-resolve` `.rc-resolved` `.wd`
src/origins/jobs.ts +3
@@ -69,6 +69,8 @@ export interface MirrorJob {
6969 readonly repo?: Repo;
7070 /** Set on `failed`: a scrubbed reason, safe to show and to log. */
7171 readonly error?: string;
72 /** Set on `failed`: the stage the job was on when it stopped. */
73 readonly failedAt?: MirrorStage;
7274 }
7375
7476 export type JobListener = (job: MirrorJob) => void;
@@ -158,6 +160,7 @@ export function advance(
158160 ...current,
159161 ...patch,
160162 stage,
163 ...(stage === "failed" ? { failedAt: current.stage } : {}),
161164 ...(terminal(stage) ? { finishedAt: new Date().toISOString() } : {}),
162165 };
163166 jobs.set(jobId, next);
src/web/client.ts +8 −4
@@ -10,6 +10,7 @@
1010 * behaviour that a page depends on to be usable.
1111 */
1212
13 import { STAGE_BADGE_JS } from "./stage-row.ts";
1314 import { paletteHelpersJs } from "./palette.ts";
1415
1516 /**
@@ -105,7 +106,7 @@ export const mirrorJs = /* js */ `
105106
106107 var rows = Array.prototype.slice.call(list.querySelectorAll('li[data-stage-name]'));
107108 var order = rows.map(function (li) { return li.getAttribute('data-stage-name'); });
108 var glyph = { done: '\\u25CF', now: '\\u25D0', failed: '\\u2715', todo: '\\u25CB' };
109 var badges = ${STAGE_BADGE_JS};
109110
110111 function paint(stage, detail) {
111112 list.setAttribute('data-stage', stage);
@@ -116,9 +117,12 @@ export const mirrorJs = /* js */ `
116117 : i === there ? (stage === 'done' ? 'done' : 'now')
117118 : 'todo';
118119 li.setAttribute('data-state', state);
119 var dot = li.querySelector('.stage-dot');
120 if (dot) dot.textContent = glyph[state];
121 var note = li.querySelector('.stage-detail');
120 var badge = li.querySelector('.badge');
121 if (badge) {
122 badge.textContent = badges[state][0];
123 badge.setAttribute('data-variant', badges[state][1]);
124 }
125 var note = li.querySelector('.row-meta');

The if (note) guard silently skips the detail for rows with no .row-meta, so a stage that becomes running after load never shows its detail. Fixing the markup makes this work; add a test.

122126 if (note) note.textContent = state === 'now' ? (detail || '') : '';
123127 });
124128 }
src/web/pages/browse.ts +10 −25
@@ -86,6 +86,7 @@ import {
8686 } from "./explorer.ts";
8787 import { accountOnly, hostLabel, originIcon } from "./origins.ts";
8888 import { refSetFrom } from "./refs.ts";
89 import { stageRow, stageStateFor } from "../stage-row.ts";
8990 import {
9091 blobView,
9192 codePage,
@@ -1006,28 +1007,11 @@ const STAGES: { name: MirrorStage; label: string }[] = [
10061007
10071008 const ORDER = STAGES.map((s) => s.name);
10081009
1009 /** Where one row stands relative to where the job has got to. */
1010 function stageState(
1011 stage: MirrorStage,
1012 at: MirrorStage,
1013 ): "done" | "now" | "todo" {
1014 // A failed job no longer remembers which stage it was on — `advance`
1015 // overwrites it — so nothing is claimed as done. The scrubbed error under
1016 // the list is the honest account, and a checklist that guessed would not be.
1017 if (at === "failed") return "todo";
1018 const here = ORDER.indexOf(stage);
1019 const there = ORDER.indexOf(at);
1020 if (here < there) return "done";
1021 if (here === there) return at === "done" ? "done" : "now";
1022 return "todo";
1023 }
1024
1025 const GLYPH = { done: "●", now: "◐", todo: "○" } as const;
1026
10271010 /** The checklist. Its shape is what `mirrorJs` updates in place. */
10281011 export function stageList(job: MirrorJob, streamUrl: string): Html {
10291012 return html`<ol
1030 class="stages"
1013 class="list stages"
1014 aria-live="polite"
10311015 id="stages"
10321016 data-stream="${streamUrl}"
10331017 data-stage="${job.stage}"
@@ -1036,12 +1020,13 @@ export function stageList(job: MirrorJob, streamUrl: string): Html {
10361020 : `/${job.repo.ownerName}/${job.repo.name}`}"
10371021 >
10381022 ${STAGES.map((stage) => {
1039 const state = stageState(stage.name, job.stage);
1040 return html`<li data-stage-name="${stage.name}" data-state="${state}">
1041 <span class="stage-dot" aria-hidden="true">${GLYPH[state]}</span>
1042 <span class="stage-label">${stage.label}</span>
1043 <span class="stage-detail">${state === "now" ? job.detail : ""}</span>
1044 </li>`;
1023 const state = stageStateFor(ORDER, stage.name, job.stage, job.failedAt);
1024 return stageRow({
1025 name: stage.name,
1026 label: stage.label,
1027 state,
1028 detail: job.detail,
1029 });
10451030 })}
10461031 </ol>`;
10471032 }
src/web/pages/migrate.ts +21 −19
@@ -57,6 +57,7 @@ import {
5757 selectField,
5858 field as formField,
5959 } from "../ui.ts";
60 import { STAGE_BADGE_JS, stageRow, stageStateFor } from "../stage-row.ts";
6061 import { prefill, originHost } from "./migrate-prefill.ts";
6162 import { githubConnectable, hostLabel } from "./origins.ts";
6263
@@ -707,23 +708,20 @@ const stagesFor = (job: MirrorJob): typeof STAGES =>
707708
708709 function stageRows(job: MirrorJob): Html {
709710 const stages = stagesFor(job);
710 const at = stages.findIndex((s) => s.name === job.stage);
711 return html`<ol class="stages">
712 ${stages.map((stage, i) => {
713 const state =
714 job.stage === "failed"
715 ? "todo"
716 : i < at || job.stage === "done"
717 ? "done"
718 : i === at
719 ? "now"
720 : "todo";
721 const glyph = state === "done" ? "●" : state === "now" ? "◐" : "○";
722 return html`<li data-stage-name="${stage.name}" data-state="${state}">
723 <span class="stage-dot" aria-hidden="true">${glyph}</span>
724 <span class="stage-label">${stage.label}</span>
725 <span class="stage-detail">${state === "now" ? job.detail : ""}</span>
726 </li>`;
711 return html`<ol class="list stages" aria-live="polite">
712 ${stages.map((stage) => {
713 const state = stageStateFor(
714 stages.map((s) => s.name),
715 stage.name,
716 job.stage,
717 job.failedAt,
718 );
719 return stageRow({
720 name: stage.name,
721 label: stage.label,
722 state,
723 detail: job.detail,
724 });
727725 })}
728726 </ol>`;
729727 }
@@ -741,6 +739,7 @@ const migrateJobJs = /* js */ `
741739 var job = box && box.getAttribute('data-job');
742740 if (!job || typeof EventSource === 'undefined') return;
743741 var order = ['creating', 'fetching', 'issues', 'pulls', 'crew', 'done'];
742 var badges = ${STAGE_BADGE_JS};
744743 var source = new EventSource('/api/v1/migrate/' + encodeURIComponent(job) + '/stream');
745744 function patch(e) {
746745 var data;
@@ -753,8 +752,11 @@ const migrateJobJs = /* js */ `
753752 var state = data.stage === 'failed' ? 'todo'
754753 : data.stage === 'done' || idx < at ? 'done' : idx === at ? 'now' : 'todo';
755754 row.setAttribute('data-state', state);
756 row.querySelector('.stage-dot').textContent = state === 'done' ? '\\u25cf' : state === 'now' ? '\\u25d0' : '\\u25cb';
757 row.querySelector('.stage-detail').textContent = state === 'now' && data.detail ? data.detail : '';
755 var badge = row.querySelector('.badge');
756 badge.textContent = badges[state][0];
757 badge.setAttribute('data-variant', badges[state][1]);
758 var note = row.querySelector('.row-meta');
759 if (note) note.textContent = state === 'now' && data.detail ? data.detail : '';
758760 }
759761 if (data.stage === 'done' || data.stage === 'failed') { source.close(); location.reload(); }
outdated

row.querySelector('.row-meta') is null for any row that was not running at load; this throws and kills the live update. Guard it, and fix the markup so it exists.

760762 }
src/web/stage-row.ts added +58
@@ -0,0 +1,58 @@
1 /**
2 * One stage of a job in flight, drawn as §5's list row with a badge for its
3 * state. The migrate page, the mirror page and the live update's script all
4 * write this one shape: `[data-stage-name][data-state]` on the `<li>`, the
5 * state's `.badge` in `.row-trail`, the job's own words in `.row-meta`.
6 */
7 import { html, type Html } from "./html.ts";
8 import { badge, row, type Tone } from "./ui.ts";
9
10 export type StageState = "done" | "now" | "todo" | "failed";
11
12 /** What the badge says and how it is toned, by state. The live-update
13 * scripts are built from this one table (`STAGE_BADGE_JS`). */
14 export const STAGE_BADGE: Record<StageState, { text: string; tone: Tone; variant: string }> = {
15 done: { text: "Done", tone: "success", variant: "success" },
16 now: { text: "Running", tone: "attn", variant: "attn" },

No failed state: a stopped job paints its stages as Waiting. Add failed (tone danger, 'Failed') for the stage that was running when the job failed.

17 failed: { text: "Failed", tone: "danger", variant: "danger" },
18 todo: { text: "Waiting", tone: "default", variant: "outline" },
19 };
20
21 /** Where one stage stands, given the job's stage (and, once it failed, the
22 * stage it failed on). `order` is the stages as this job lists them. */
23 export function stageStateFor(
24 order: readonly string[],
25 name: string,
26 stage: string,
27 failedAt?: string,
28 ): StageState {
29 const here = order.indexOf(name);
outdated

row() renders .row-meta only when meta is truthy; an empty Html here drops the span for done/todo rows, so the live scripts have nothing to write the detail into. Make the element always present.

30 const at = order.indexOf(stage === "failed" ? (failedAt ?? "") : stage);
31 if (at < 0) return "todo";
32 if (here < at) return "done";
33 if (here > at) return "todo";
34 return stage === "failed" ? "failed" : stage === "done" ? "done" : "now";
outdated

The ternary duplicates what badge() does; derive variant from tone once and drop the variant field.

35 }
36
37 export function stageRow(o: {
38 name: string;
39 label: string;
40 state: StageState;
41 detail: string;
42 }): Html {
43 const b = STAGE_BADGE[o.state];
44 return row({
45 title: o.label,
46 meta: html`${o.state === "now" ? o.detail : ""}`,
47 metaSlot: true,
48 trail: badge(b.text, { tone: b.tone, variant: "outline" }),

Minor: variant: "outline" is passed for every state, and STAGE_BADGE.variant repeats what badge() already works out from tone. Keeping one rule here would keep STAGE_BADGE_JS honest without the ternary.

49 data: { "stage-name": o.name, state: o.state },
50 });
51 }
52
53 /** The table as a JS literal for the scripts, `{done:["Done","success"],…}`. */
54 export const STAGE_BADGE_JS = JSON.stringify(
55 Object.fromEntries(
56 Object.entries(STAGE_BADGE).map(([k, b]) => [k, [b.text, b.tone === "default" ? b.variant : b.tone]]),
57 ),
58 );
src/web/style/content.ts +2 −14
@@ -113,21 +113,9 @@ a.where-place:hover, button.where-place:hover {
113113 .live-note strong { color: var(--fg); }
114114
115115 /* A mirror in flight. One row per stage, in the order they happen; the row
116 says which of the three states it is in and nothing else moves. */
116 is a §5 row whose badge says which of the three states it is in. */
117117 .stages { list-style: none; margin: 0; padding: 0; }

The divider rule was removed above and the ol has no .list class, so nothing separates the rows. Give the ol class="list stages" in migrate.ts and browse.ts.

118 .stages li {
119 display: flex; align-items: center; gap: 10px;
120 padding: 8px 0; font-size: 13px; color: var(--faint);
121 }
122 .stages li + li { border-top: 1px solid var(--border); }
123 .stages li[data-state="done"] { color: var(--muted-fg); }
124 .stages li[data-state="done"] .stage-dot { color: var(--success); }
125 .stages li[data-state="now"] { color: var(--fg); font-weight: 560; }
126 .stages li[data-state="now"] .stage-dot { color: var(--accent); }
127 .stages li[data-state="failed"] { color: var(--danger); }
128 .stages li[data-state="failed"] .stage-dot { color: var(--danger); }
129 .stage-dot { flex: none; width: 16px; text-align: center; }
130 .stage-detail { margin-left: auto; color: var(--faint); font-size: 12.5px; font-weight: 400; }
118 .stages li[data-state="todo"] .row-title { color: var(--faint); }
131119 .origin-chip {
132120 display: inline-flex; align-items: center; gap: 6px;
133121 max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
src/web/ui.ts +3 −1
@@ -888,6 +888,8 @@ export interface RowOptions {
888888 href?: string;
889889 title: Html | string;
890890 meta?: Html;
891 /** Draw `.row-meta` even when `meta` is empty, for a script to write into. */
892 metaSlot?: boolean;
891893 lead?: Html;
892894 trail?: Html;
893895 state?: "open" | "closed" | "merged" | "draft";
@@ -920,7 +922,7 @@ export function row(o: RowOptions): Html {
920922 ${o.titleAttr ? html`title="${o.titleAttr}"` : null}
921923 >${o.title}</span
922924 >
923 ${o.meta ? html`<span class="row-meta">${o.meta}</span>` : null}
925 ${o.meta || o.metaSlot ? html`<span class="row-meta">${o.meta}</span>` : null}
924926 </span>
925927 ${o.trail && o.trail.text.trim() !== ""
926928 ? html`<span class="row-trail">${o.trail}</span>`
test/migrate.test.ts +29
@@ -62,6 +62,7 @@ import {
6262 type FakeForge,
6363 } from "./fake-origin.ts";
6464 import { gitClientEnv } from "./helpers/git-client.ts";
65 import { STAGE_BADGE_JS } from "../src/web/stage-row.ts";
6566 import { prefill } from "../src/web/pages/migrate-prefill.ts";
6667 import { quietLogs } from "./helpers/globals.ts";
6768 import { promote } from "./helpers/members.ts";
@@ -1405,6 +1406,34 @@ describe("migrate", () => {
14051406 target.stop(true);
14061407 }
14071408 });
1409
1410 test("the job page draws each stage as a row with a state badge, and no stage-dot", async () => {
1411 const job = createJob("", "octo/rows", { kind: "migrate", actorId: alice.id });
1412 advance(job.id, { stage: "fetching", detail: "receiving 61%" });
1413 const page = await (await browser(aliceCookie, `/migrate/jobs/${job.id}`)).text();
1414 expect(page).not.toContain("stage-dot");
1415 expect(page).not.toContain("stage-detail");
1416 // The page's script is built from the table the rows are drawn from.
1417 expect(page).toContain(`var badges = ${STAGE_BADGE_JS};`);
1418 expect(page).toMatch(/<li data-stage-name="creating" data-state="done">[\s\S]*?class="row"[\s\S]*?>Done</);
1419 expect(page).toMatch(/data-stage-name="fetching" data-state="now">[\s\S]*?receiving 61%[\s\S]*?>Running</);
1420 expect(page).toMatch(/data-stage-name="crew" data-state="todo">[\s\S]*?>Waiting</);
1421 expect(page).toMatch(/<ol class="list stages" aria-live="polite">/);
1422 // Every stage has a `.row-meta` for the live update to write into.
1423 const items = page.match(/<li data-stage-name=[\s\S]*?<\/li>/g) ?? [];
1424 expect(items.length).toBeGreaterThan(0);
1425 for (const li of items) expect(li).toContain('class="row-meta"');
1426 });
1427
1428 test("mark the stage it failed on, and keep the earlier ones done", async () => {
1429 const job = createJob("", "octo/broke", { kind: "migrate", actorId: alice.id });
1430 advance(job.id, { stage: "fetching", detail: "receiving 3%" });
1431 advance(job.id, { stage: "failed", error: "clone went wrong" });
1432 const page = await (await browser(aliceCookie, `/migrate/jobs/${job.id}`)).text();
1433 expect(page).toMatch(/data-stage-name="creating" data-state="done"/);
1434 expect(page).toMatch(/data-stage-name="fetching" data-state="failed">[\s\S]*?data-variant="danger"\s*>Failed</);
1435 expect(page).toMatch(/data-stage-name="crew" data-state="todo"/);
1436 });
14081437 });
14091438
14101439 describe("migrate prefill", () => {
test/web-browse.test.ts +12 −1
@@ -30,7 +30,8 @@ import { forgetProviders } from "../src/origins/providers/index.ts";
3030 import { clearViewCache } from "../src/view/cache.ts";
3131 import { quietLogs } from "./helpers/globals.ts";
3232 import { newFakeState, serveFakeForge, type FakeForge } from "./fake-origin.ts";
33 import { codeJs } from "../src/web/client.ts";
33 import { codeJs, mirrorJs } from "../src/web/client.ts";
34 import { STAGE_BADGE, STAGE_BADGE_JS } from "../src/web/stage-row.ts";
3435 import { adminCookie, seedAdmin } from "./helpers/admin.ts";
3536
3637 quietLogs();
@@ -547,6 +548,16 @@ describe("browsing an origin repository on the web", () => {
547548 `data-stream="/api/v1/user/origins/${originId}/mirror/`,
548549 );
549550 expect(running).toContain("new EventSource(url)");
551 // Each stage is a §5 row with a state badge, never a bare glyph.
552 expect(running).not.toContain("stage-dot");
553 expect(running).not.toContain("stage-detail");
554 expect(running).toMatch(/<ol\s+class="list stages"\s+aria-live="polite"/);
555 expect(count(running, "<li data-stage-name=")).toBe(7);
556 expect(running).toMatch(/<li data-stage-name="creating" data-state="(?:done|now)">\s*<div class="row"/);
557 expect(running).toMatch(/class="badge"\s+data-variant="(?:success|attn|outline)"\s*>(?:Done|Running|Waiting)</);
558 // The live update is built from the same table the markup is.
559 expect(mirrorJs).toContain(STAGE_BADGE_JS);
560 for (const b of Object.values(STAGE_BADGE)) expect(STAGE_BADGE_JS).toContain(b.text);
550561 expect(running).toMatch(/<script nonce="[^"]+">/);
551562 expect(running).not.toMatch(/<script[^>]*src="(?!\/assets\/(?:shell|beacon)-)/);
552563

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs