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.
301 lines
15 KiB
TypeScript
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;
|
|
}
|
|
});
|