Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
id: 01M4T3BMJ8379Q75HDMYS12M5Z
anchor: useState<number | null>(null)
created: 2026-10-13T07:41:44Z
norm: '1'
---

Zoom null means fit-to-stage in CSS (min(100%, authored px)), so the first frame already fits and resizes track it; a number is a user choice that survives resize. Spec changes remount (keyed by fingerprint) and return to fit.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
id: 01M4T3BMKG5MRBYK5WEGD0MZPW
anchor: .plan-content .plan-evidence table
created: 2026-10-13T07:41:44Z
norm: '1'
---

Investigation evidence tables sit inside .plan-content, so they inherit the authored-table display:block and 7rem cell floor unless reset here; that floor pushed the 4th column off a phone.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ id: 01M4T8QPPHVX0JGCQT2YE672VG
anchor: function answer
created: 2026-10-13T09:15:43Z
norm: '1'
sig: 18ca2a2469c3ff47
body_hash: 0c130b62b8c9c7d7
raw_hash: 93e9001c5aa26725
lines: 83-89
---

An open question's empty anchor set gets persisted by Questions.rebase as {anchors:[], pending:false}, indistinguishable from a reviewed-empty set. Answering must convert empty sets to pending/missing (Anchors.answer) or a blank-document ask reads as 'No related text' forever.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ id: 01M4TE639KWKCMD26E81A3NV5C
anchor: function decide
created: 2026-10-13T10:50:57Z
norm: '1'
sig: 8507735bfb3c3993
body_hash: 1491cac8014b219f
raw_hash: a3ef7a4a36fbfca5
lines: 61-75
---

A blank document automatically opens new questions in Decisions. These prose tests explicitly return to Document before answering so their inline card, marker, and collapse assertions exercise the document rather than the hidden pane.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: 01M4TJYVPVDXRJQRMYATXF9R1R
anchor: function DiagramView
created: 2026-10-13T12:14:23Z
norm: '1'
sig: 7681c7b78f8383bc
body_hash: 43ceeed9d2f21cfc
raw_hash: 9834f5214d3d1a8e
lines: 83-507
supersedes: 01M4T3BMJ8379Q75HDMYS12M5Z
---

Zoom null means fit-to-stage in CSS (min(100%, authored px)), so the first frame already fits and resizes track it; a number is a user choice that survives resize. Spec changes remount (keyed by fingerprint) and return to fit.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ id: 01M4TGSVAG4K3CADXT761BB7SY
anchor: function $caretAfterRun
created: 2026-10-13T11:36:42Z
norm: '1'
sig: 3b527488d113a979
body_hash: 9eaec0e0612244a8
raw_hash: c35fb166b171e126
lines: 17-27
---

Hidden accepted-comment Decision projections are decorator nodes and can follow a questionnaire run. Continue to the next editable element when handing off the caret; stopping at a hidden decorator strands focus on the page.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: 01M4TJYVQKV9KEAZV30B3PSVEM
anchor: '@file'
created: 2026-10-13T12:14:23Z
norm: '1'
sig: e3b0c44298fc1c14
body_hash: 6b17cdb9d33cec85
raw_hash: f2e3cb960902c3f8
lines: 1-5839
supersedes: 01M4T3BMKG5MRBYK5WEGD0MZPW
---

Investigation evidence tables sit inside .plan-content, so they inherit the authored-table display:block and 7rem cell floor unless reset here; that floor pushed the 4th column off a phone.
224 changes: 134 additions & 90 deletions e2e/diagram-gallery/gallery.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ test("gallery has no page-level horizontal overflow", async ({ page }) => {

test("a narrow diagram exposes keyboard horizontal scrolling", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "narrow", "The document-width view fits at wide widths.");
let stage = page.locator('[data-specimen-diagram="first"] .ch-diagram__stage');
let first = page.locator('[data-specimen-diagram="first"]');
let stage = first.locator(".ch-diagram__stage");
// A wide diagram opens fitted; at actual size it overflows and scrolls.
await expect(stage).not.toHaveAttribute("tabindex", "0");
await first.getByRole("button", { name: "Actual size", exact: true }).click();
await expect(stage).toHaveAttribute("tabindex", "0");
await expect(stage).toHaveAttribute("aria-label", /scroll horizontally/i);
await stage.focus();
Expand All @@ -99,96 +103,136 @@ test("a narrow diagram exposes keyboard horizontal scrolling", async ({ page },
await expect.poll(() => stage.evaluate(element => element.scrollLeft)).toBeGreaterThan(0);
});

test("reduced motion opens on the whole diagram and plays only when asked", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await reload(page);
let first = page.locator('[data-specimen-diagram="first"]');
let svg = first.locator("svg.sc-svg");
await expect(svg).toBeVisible();
let total = Number(await svg.getAttribute("data-sc-steps"));
let controls = first.getByRole("group", { name: "Diagram controls", exact: true });
let counter = (step: number) => controls.getByText(`${step} / ${total}`, { exact: true });
await expect(counter(total)).toBeVisible();
await page.waitForTimeout(1000);
await expect(counter(total)).toBeVisible();
await expect(controls.getByRole("button", { name: "Next step", exact: true })).toBeDisabled();

await controls.getByRole("button", { name: "Play diagram", exact: true }).click();
await expect(counter(1)).toBeVisible();
await expect(svg).toHaveClass(/sc-stepping/);
await expect(controls.getByRole("button", { name: "Pause diagram", exact: true })).toBeVisible();
let shown = controls.locator(".ch-diagram__step");
// Each step shows for one stagger; poll faster than expect's backoff.
await expect.poll(async () => Number((await shown.textContent())!.split(" / ")[0]), {
intervals: [50],
}).toBeGreaterThan(1);
await controls.getByRole("button", { name: "Restart diagram", exact: true }).click();
await expect(counter(1)).toBeVisible();
await expect(first.locator(".ch-diagram__status")).toBeEmpty();
});
test(
"reduced motion opens on the whole diagram and plays only when asked",
async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "wide", "A narrow bar has no step counter or Restart.");
await page.emulateMedia({ reducedMotion: "reduce" });
await reload(page);
let first = page.locator('[data-specimen-diagram="first"]');
let svg = first.locator("svg.sc-svg");
await expect(svg).toBeVisible();
let total = Number(await svg.getAttribute("data-sc-steps"));
let controls = first.getByRole("group", { name: "Diagram controls", exact: true });
let counter = (step: number) => controls.getByText(`${step} / ${total}`, { exact: true });
await expect(counter(total)).toBeVisible();
await page.waitForTimeout(1000);
await expect(counter(total)).toBeVisible();
await expect(controls.getByRole("button", { name: "Next step", exact: true })).toBeDisabled();

test("diagram controls sit centred below the drawing and step to either end", async ({ page }) => {
let first = page.locator('[data-specimen-diagram="first"]');
let stage = first.locator(".ch-diagram__stage");
let controls = first.getByRole("group", { name: "Diagram controls", exact: true });
await expect(controls).toBeVisible();
let [stageBox, controlsBox] = [await stage.boundingBox(), await controls.boundingBox()];
expect(controlsBox!.y).toBeGreaterThanOrEqual(stageBox!.y + stageBox!.height);
expect(
Math.abs(
controlsBox!.x + controlsBox!.width / 2 - (stageBox!.x + stageBox!.width / 2),
),
).toBeLessThanOrEqual(1);

let toggle = (name: "Play diagram" | "Pause diagram") =>
controls.getByRole("button", { name, exact: true });
let previous = controls.getByRole("button", { name: "Previous step", exact: true });
let next = controls.getByRole("button", { name: "Next step", exact: true });
let restart = controls.getByRole("button", { name: "Restart diagram", exact: true });
let shown = controls.locator(".ch-diagram__step");
let total = Number(await first.locator("svg.sc-svg").getAttribute("data-sc-steps"));
expect(total).toBeGreaterThan(3);
let counter = (step: number) => controls.getByText(`${step} / ${total}`, { exact: true });
let at = async () => Number((await shown.textContent())!.split(" / ")[0]);

// Playing counts the step being played, and the arrows wait for a pause.
await restart.click();
await expect(counter(1)).toBeVisible();
await expect(previous).toBeDisabled();
await expect(next).toBeDisabled();
await expect.poll(at, { intervals: [50] }).toBeGreaterThan(1);
await toggle("Pause diagram").click();
await expect(toggle("Play diagram")).toBeFocused();
let paused = await at();
expect(paused).toBeLessThan(total);
await page.waitForTimeout(1000);
await expect(counter(paused)).toBeVisible();

await next.click();
await expect(counter(paused + 1)).toBeVisible();
for (let step = paused; step >= 1; step--) {
await previous.click();
await expect(counter(step)).toBeVisible();
}
// A disabled arrow keeps focus, so the keyboard never drops out of the bar.
await expect(previous).toBeDisabled();
await expect(previous).toBeFocused();

// Play resumes from the step on screen and stops at the last one.
await toggle("Play diagram").click();
await expect(toggle("Pause diagram")).toBeVisible();
await expect.poll(at, { intervals: [50] }).toBeGreaterThan(1);
await expect(counter(total)).toBeVisible({ timeout: 10_000 });
await expect(toggle("Play diagram")).toBeVisible();
await expect(next).toBeDisabled();
await expect(previous).toBeEnabled();

await restart.click();
await expect(counter(1)).toBeVisible();
await expect(toggle("Pause diagram")).toBeVisible();
await expect(controls.getByRole("button", { name: "Replay diagram" })).toHaveCount(0);
await expect(controls.getByRole("button", { name: "Reset diagram" })).toHaveCount(0);
});
await controls.getByRole("button", { name: "Play diagram", exact: true }).click();
await expect(counter(1)).toBeVisible();
await expect(svg).toHaveClass(/sc-stepping/);
await expect(controls.getByRole("button", { name: "Pause diagram", exact: true }))
.toBeVisible();
let shown = controls.locator(".ch-diagram__step");
// Each step shows for one stagger; poll faster than expect's backoff.
await expect.poll(async () => Number((await shown.textContent())!.split(" / ")[0]), {
intervals: [50],
}).toBeGreaterThan(1);
await controls.getByRole("button", { name: "Restart diagram", exact: true }).click();
await expect(counter(1)).toBeVisible();
await expect(first.locator(".ch-diagram__status")).toBeEmpty();
},
);

test(
"diagram controls sit centred below the drawing and step to either end",
async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "wide", "A narrow bar has no step counter or Restart.");
let first = page.locator('[data-specimen-diagram="first"]');
let stage = first.locator(".ch-diagram__stage");
let controls = first.getByRole("group", { name: "Diagram controls", exact: true });
await expect(controls).toBeVisible();
let [stageBox, controlsBox] = [await stage.boundingBox(), await controls.boundingBox()];
expect(controlsBox!.y).toBeGreaterThanOrEqual(stageBox!.y + stageBox!.height);
expect(
Math.abs(
controlsBox!.x + controlsBox!.width / 2 - (stageBox!.x + stageBox!.width / 2),
),
).toBeLessThanOrEqual(1);

let toggle = (name: "Play diagram" | "Pause diagram") =>
controls.getByRole("button", { name, exact: true });
let previous = controls.getByRole("button", { name: "Previous step", exact: true });
let next = controls.getByRole("button", { name: "Next step", exact: true });
let restart = controls.getByRole("button", { name: "Restart diagram", exact: true });
let shown = controls.locator(".ch-diagram__step");
let total = Number(await first.locator("svg.sc-svg").getAttribute("data-sc-steps"));
expect(total).toBeGreaterThan(3);
let counter = (step: number) => controls.getByText(`${step} / ${total}`, { exact: true });
let at = async () => Number((await shown.textContent())!.split(" / ")[0]);

// Playing counts the step being played, and the arrows wait for a pause.
await restart.click();
await expect(counter(1)).toBeVisible();
await expect(previous).toBeDisabled();
await expect(next).toBeDisabled();
await expect.poll(at, { intervals: [50] }).toBeGreaterThan(1);
await toggle("Pause diagram").click();
await expect(toggle("Play diagram")).toBeFocused();
let paused = await at();
expect(paused).toBeLessThan(total);
await page.waitForTimeout(1000);
await expect(counter(paused)).toBeVisible();

await next.click();
await expect(counter(paused + 1)).toBeVisible();
for (let step = paused; step >= 1; step--) {
await previous.click();
await expect(counter(step)).toBeVisible();
}
// A disabled arrow keeps focus, so the keyboard never drops out of the bar.
await expect(previous).toBeDisabled();
await expect(previous).toBeFocused();

// Play resumes from the step on screen and stops at the last one.
await toggle("Play diagram").click();
await expect(toggle("Pause diagram")).toBeVisible();
await expect.poll(at, { intervals: [50] }).toBeGreaterThan(1);
await expect(counter(total)).toBeVisible({ timeout: 10_000 });
await expect(toggle("Play diagram")).toBeVisible();
await expect(next).toBeDisabled();
await expect(previous).toBeEnabled();

await restart.click();
await expect(counter(1)).toBeVisible();
await expect(toggle("Pause diagram")).toBeVisible();
await expect(controls.getByRole("button", { name: "Replay diagram" })).toHaveCount(0);
await expect(controls.getByRole("button", { name: "Reset diagram" })).toHaveCount(0);
},
);

test(
"a narrow bar keeps one row, and Play restarts a finished diagram",
async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "narrow", "The whole bar fits at wide widths.");
await page.emulateMedia({ reducedMotion: "reduce" });
await reload(page);
let first = page.locator('[data-specimen-diagram="first"]');
let svg = first.locator("svg.sc-svg");
await expect(svg).toBeVisible();
let total = Number(await svg.getAttribute("data-sc-steps"));
let controls = first.getByRole("group", { name: "Diagram controls", exact: true });
let status = first.locator(".ch-diagram__status");
await expect(controls.getByRole("button", { name: "Restart diagram", exact: true }))
.toBeHidden();
await expect(controls.locator(".ch-diagram__step")).toBeHidden();
let tops = await controls.locator("button:visible").evaluateAll(buttons =>
new Set(buttons.map(button => Math.round(button.getBoundingClientRect().top))).size
);
expect(tops).toBe(1);
// Reduced motion opens finished, on the last step.
await expect(status).toHaveText(`Step ${total} of ${total}`);
await controls.getByRole("button", { name: "Play diagram", exact: true }).click();
// Playing again starts over: the last step waits for its turn.
await expect(controls.getByRole("button", { name: "Pause diagram", exact: true }))
.toBeVisible();
await expect(svg.locator(`[data-sc-step="${total}"][aria-hidden="true"]`).first())
.toBeAttached();
await expect(status).toHaveText(`Step ${total} of ${total}`, { timeout: 10_000 });
},
);

test("a wireframe fence is drawn chromeless, and an invalid one stays code", async ({ page }) => {
let blocks = page.locator("[data-wireframe-document] .planCode");
Expand Down
Loading
Loading