Files
yoi/tools/web-ux/browser-tests/console_pending_shell_test.ts
Hare c78f7f7d80 feat: replace revision counters with domain state contracts
Use content digests, key fingerprints, binding and trust identities, immutable mutation receipts, and observation tokens instead of state-update ordinals across API, storage, Runtime, and Web contracts.

Preserve historical replay and secret material through frozen migrations; preflight uncertain removals before cutover, resume provisioning receipts, validate encrypted migration candidates, and align fresh/upgraded schemas. Include reviewed regression fixes, generated artifacts, and validation evidence.
2026-10-10 15:55:20 +09:00

301 lines
15 KiB
TypeScript

/// <reference lib="dom" />
import { assert, assertEquals } from "@std/assert";
import { dirname, fromFileUrl, join } from "@std/path";
import { chromium } from "playwright";
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
Deno.test("Console pending scopes are independent and match Tasks row typography", async () => {
const build = await new Deno.Command(Deno.execPath(), {
args: ["task", "build"],
cwd: join(root, "web/workspace"),
stdout: "piped",
stderr: "piped",
}).output();
assert(build.success, new TextDecoder().decode(build.stderr));
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
const port = (listener.addr as Deno.NetAddr).port;
listener.close();
const url = `http://127.0.0.1:${port}`;
const server = new Deno.Command(Deno.execPath(), {
args: [
"run",
"--allow-net",
"--allow-read",
join(root, "tools/web-ux/browser-tests/console_history_fixture_server.ts"),
String(port),
join(root, "web/workspace/build"),
],
stdout: "null",
stderr: "null",
}).spawn();
try {
let started = false;
for (let attempt = 0; attempt < 100; attempt++) {
try {
const response = await fetch(`${url}/health`);
await response.body?.cancel();
if (response.ok) {
started = true;
break;
}
} catch { /* owned fixture is starting */ }
await new Promise((resolve) => setTimeout(resolve, 50));
}
assert(started);
const browser = await chromium.launch({ headless: true });
try {
for (const colorScheme of ["light", "dark"] as const) {
for (const width of [1440, 768, 390, 320]) {
const context = await browser.newContext({
viewport: { width, height: 900 },
colorScheme,
reducedMotion: "reduce",
});
try {
const page = await context.newPage();
const errors: string[] = [];
const supersededCatalogReads: string[] = [];
page.on("pageerror", (error) => errors.push(String(error)));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
page.on("requestfailed", (request) => {
// The subscription snapshot supersedes the bootstrap catalog GET.
// Keep this intentional AbortController cancellation separate from
// transport failures; each mode still awaits a completed catalog GET.
if (
request.url() === `${url}/api/w/console-history-review/workers` &&
request.method() === "GET" &&
request.failure()?.errorText === "net::ERR_ABORTED"
) supersededCatalogReads.push(request.url());
else errors.push(`${request.url()}: ${request.failure()?.errorText}`);
});
for (const mode of ["both", "queue", "notifications", "legacy", "empty"]) {
const catalogFinished = page.waitForEvent("requestfinished", {
predicate: (request) =>
request.url() === `${url}/api/w/console-history-review/workers` &&
request.method() === "GET",
timeout: 10_000,
});
await page.goto(
`${url}/w/console-history-review/workers/W-900-console-fixture/console?pending=${mode}`,
);
await catalogFinished;
await page.locator(".task-mini-row").waitFor();
const metadata = page.getByLabel("Worker model and context", { exact: true });
const statusGeometry = await metadata.evaluate((status) => {
const task = document.querySelector(".task-mini-row")!;
const composer = document.querySelector(".console-composer")!;
const metrics = (node: Element) => {
const style = getComputedStyle(node);
return [style.fontFamily, style.fontSize, style.lineHeight];
};
const rect = status.getBoundingClientRect();
return {
typography: metrics(status),
taskTypography: metrics(task),
height: rect.height,
belowComposer: rect.top >= composer.getBoundingClientRect().bottom,
alignedWithTasks: rect.left + parseFloat(getComputedStyle(status).paddingLeft) === task.getBoundingClientRect().left,
visible: rect.bottom <= innerHeight,
wrap: getComputedStyle(status).flexWrap,
pageOverflow: document.documentElement.scrollWidth > innerWidth,
};
});
assertEquals(statusGeometry.typography, statusGeometry.taskTypography);
assertEquals(statusGeometry.height, 24);
assertEquals(statusGeometry.belowComposer, true);
assertEquals(statusGeometry.alignedWithTasks, true);
assertEquals(statusGeometry.visible, true);
assertEquals(statusGeometry.wrap, "nowrap");
assertEquals(statusGeometry.pageOverflow, false);
const details = metadata.getByRole("button", { name: "Details", exact: true });
assertEquals(await page.locator(".console-header").count(), 0);
const detailsGeometry = await details.evaluate((button) => {
const row = button.closest(".worker-context-status")!;
const rect = button.getBoundingClientRect();
const rowRect = row.getBoundingClientRect();
return {
rightAligned: Math.abs(rect.right - (rowRect.right - parseFloat(getComputedStyle(row).paddingRight))) < 1,
centered: Math.abs(rect.top + rect.height / 2 - (rowRect.top + rowRect.height / 2)) < 1,
width: rect.width,
};
});
assertEquals(detailsGeometry.rightAligned, true);
assertEquals(detailsGeometry.centered, true);
assertEquals(detailsGeometry.width, 24);
const overview = metadata.getByRole("switch", { name: "Overview", exact: true });
const controls = await overview.evaluate((group) => {
const row = group.closest(".worker-context-status")!;
const rowRect = row.getBoundingClientRect();
const buttons = [...row.querySelectorAll("button")];
return {
buttonsInsideRow: buttons.every((button) => {
const rect = button.getBoundingClientRect();
return rect.left >= rowRect.left && rect.right <= rowRect.right &&
rect.top >= rowRect.top && rect.bottom <= rowRect.bottom;
}),
heights: buttons.map((button) => button.getBoundingClientRect().height),
modeFont: getComputedStyle(buttons[0]).fontFamily,
divider: getComputedStyle(buttons[1]).borderLeftWidth,
beforeDetails: group.nextElementSibling?.getAttribute("aria-label") === "Details",
};
});
assertEquals(controls.buttonsInsideRow, true);
assertEquals(controls.heights, [24, 24]);
assertEquals(controls.modeFont.includes("mono"), false);
assertEquals(controls.divider, "0px");
assertEquals(controls.beforeDetails, true);
const onSize = (await overview.boundingBox())!;
assert(onSize.width < 90, "Switch and fixed label should use less than 90px");
assertEquals(await overview.getAttribute("aria-checked"), "true");
await overview.focus();
assertEquals(await overview.evaluate((button) => getComputedStyle(button).outlineStyle), "solid");
await page.keyboard.press("Enter");
assertEquals(await overview.getAttribute("aria-checked"), "false");
assertEquals((await overview.textContent())!.trim(), "Overview");
assertEquals((await overview.boundingBox())!.width, onSize.width);
if (mode === "both") {
await Deno.mkdir(join(root, "target/web-ux/console-footer-switch"), { recursive: true });
await page.screenshot({ path: join(root, `target/web-ux/console-footer-switch/${colorScheme}-${width}-off.png`) });
}
await page.keyboard.press("Space");
assertEquals(await overview.getAttribute("aria-checked"), "true");
assertEquals((await overview.boundingBox())!.width, onSize.width);
await overview.locator("span").last().click();
assertEquals(await overview.getAttribute("aria-checked"), "false");
await overview.locator(".console-view-toggle-track").click();
assertEquals(await overview.getAttribute("aria-checked"), "true");
await details.focus();
await page.keyboard.press("Enter");
assertEquals(await details.getAttribute("aria-expanded"), "true");
await page.getByRole("button", { name: "Close", exact: true }).waitFor();
await page.keyboard.press("Space");
assertEquals(await details.getAttribute("aria-expanded"), "false");
await details.click();
assertEquals(await details.getAttribute("aria-expanded"), "true");
await page.getByRole("button", { name: "Close", exact: true }).click();
assertEquals(await details.getAttribute("aria-expanded"), "false");
if (mode === "both") {
await Deno.mkdir(join(root, "target/web-ux/console-footer-switch"), { recursive: true });
await page.screenshot({ path: join(root, `target/web-ux/console-footer-switch/${colorScheme}-${width}.png`) });
}
const queue = mode === "both" || mode === "queue";
const notifications = ["both", "notifications", "legacy"].includes(mode);
assertEquals(
await page.getByRole("group", { name: "Queued inputs", exact: true }).count(),
queue ? 1 : 0,
);
assertEquals(
await page.getByRole("group", { name: "Notifications", exact: true }).count(),
notifications ? 1 : 0,
);
assertEquals(await page.getByRole("heading", { name: "0 Queued" }).count(), 0);
const region = page.getByRole("region", { name: "Pending activations" });
assertEquals(await region.count(), mode === "empty" ? 0 : 1);
if (mode === "empty") continue;
const geometry = await region.evaluate((region) => {
const task = document.querySelector<HTMLElement>(".task-mini-row")!;
const taskStyle = getComputedStyle(task);
const metrics = (node: Element) => {
const style = getComputedStyle(node);
return {
height: node.getBoundingClientRect().height,
family: style.fontFamily,
size: style.fontSize,
line: style.lineHeight,
};
};
return {
task: metrics(task),
rows: [...region.querySelectorAll("h2, li")].map(metrics),
lists: [...region.querySelectorAll("ol")].map((list) => ({
margin: getComputedStyle(list).margin,
padding: getComputedStyle(list).padding,
gap: getComputedStyle(list).rowGap,
})),
columns: [...region.querySelectorAll(".pending-column")].map((column) =>
column.getBoundingClientRect().toJSON()
),
width: region.getBoundingClientRect().width,
pageOverflow: document.documentElement.scrollWidth > innerWidth,
mainOverflow: document.querySelector("main")!.scrollWidth >
document.querySelector("main")!.clientWidth,
previews: [...region.querySelectorAll(".pending-submission-preview")].map((
preview,
) => ({
title: preview.getAttribute("title"),
whitespace: getComputedStyle(preview).whiteSpace,
overflow: getComputedStyle(preview).textOverflow,
})),
expectedGap: getComputedStyle(task.closest(".task-mini")!).rowGap,
taskMono: taskStyle.fontFamily.includes("mono"),
};
});
assertEquals(geometry.task.height, 16);
assertEquals(geometry.taskMono, true);
for (const row of geometry.rows) {
assertEquals(
row,
geometry.task,
`${width}px/${mode}: heading and preview rows must match Tasks`,
);
}
for (const list of geometry.lists) {
assertEquals(list, { margin: "0px", padding: "0px", gap: geometry.expectedGap });
}
if (mode === "both") assertEquals(geometry.columns[0].y, geometry.columns[1].y);
else {assertEquals(
geometry.columns[0].width,
geometry.width,
"a lone scope must not reserve an empty column",
);}
assertEquals(geometry.pageOverflow, false);
assertEquals(geometry.mainOverflow, false);
for (const preview of geometry.previews) {
assert(preview.title);
assertEquals(preview.whitespace, "nowrap");
assertEquals(preview.overflow, "ellipsis");
}
if (queue) {
const cancel = page.getByRole("button", { name: "Cancel queued input 1" });
assertEquals((await cancel.boundingBox())!.height, 16);
assertEquals(
await cancel.isDisabled(),
true,
"retained sessions cannot mutate the queue",
);
await page.locator(".pending-queue li").first().hover();
assertEquals(
await cancel.evaluate((button) => getComputedStyle(button).opacity),
"1",
);
}
if (mode === "legacy") {
await page.getByText("2 pending · Preview unavailable", { exact: true }).waitFor();
}
}
assertEquals(await page.locator(".workspace-alert").count(), 0);
assertEquals(errors, []);
console.log(JSON.stringify({
colorScheme, width, supersededCatalogReads,
unexpectedErrors: errors,
}));
} finally {
await context.close();
}
}
}
} finally {
await browser.close();
}
} finally {
try {
server.kill("SIGTERM");
} catch (error) {
if (!(error instanceof Deno.errors.NotFound)) throw error;
}
await server.status;
}
});