fix: separate console pending groups and align row typography
This commit is contained in:
@@ -208,6 +208,15 @@ Build `web/workspace` before capture. No real keys, accounts, or deletion reques
|
||||
keyboard disclosure/focus return, full public bundle access, scoped errors, and exact-name deletion
|
||||
confirmation. Interaction screenshots are saved under `target/web-ux/settings-interactions`.
|
||||
|
||||
## Console pending input fixture
|
||||
|
||||
`scenarios/console-pending.json` uses the owned Console history fixture with `?pending=both`,
|
||||
`queue`, `notifications`, `legacy`, or `empty`. Each state includes a task for typography comparison.
|
||||
It captures 1440px, 768px, and 390px; `browser-tests/console_pending_shell_test.ts` additionally checks
|
||||
320px and both themes, independent group visibility, full-width single groups, uniform 16px rows,
|
||||
monospace inheritance, truncation, and retained-session cancellation gating. Live revision-fenced
|
||||
cancellation and state transitions are covered by Console component tests. No real Worker is used.
|
||||
|
||||
## Adding a scenario
|
||||
|
||||
1. Name the concrete user task and expected data state; do not write “looks correct”.
|
||||
|
||||
@@ -174,17 +174,30 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
|
||||
url.pathname ===
|
||||
`/api/w/${workspaceId}/runtimes/${runtimeId}/workers/${workerId}/session`
|
||||
) {
|
||||
const pendingMode = new URL(request.headers.get("referer") ?? request.url).searchParams.get("pending");
|
||||
const hasQueue = pendingMode === "both" || pendingMode === "queue";
|
||||
const hasNotifications = pendingMode === "both" || pendingMode === "notifications" || pendingMode === "legacy";
|
||||
const submissions = hasQueue ? [
|
||||
{ submission_id: "queued-1", preview: "First queued input", accepted_at_ms: 1, segment_count: 1, byte_len: 18 },
|
||||
{ submission_id: "queued-2", preview: "Long queued input — " + "preview text ".repeat(30), accepted_at_ms: 2, segment_count: 1, byte_len: 500 },
|
||||
] : [];
|
||||
const taskEntries = pendingMode ? [{
|
||||
kind: "tool_call", entry_id: "fixture-task", timestamp: 13, provenance: "model_output",
|
||||
call_id: "fixture-task", name: "TaskCreate",
|
||||
arguments: JSON.stringify({ subject: "Inspect pending input layout", description: "Synthetic visual fixture" }),
|
||||
}] : [];
|
||||
return json({
|
||||
availability: "retained_snapshot",
|
||||
identity: { session_id: "session-a", segment_id: "segment-a", entry_count: 36 },
|
||||
snapshot: {
|
||||
pending_submissions: {
|
||||
revision: 0,
|
||||
notification_count: 0,
|
||||
head_id: null,
|
||||
submissions: [],
|
||||
notification_count: hasNotifications ? 2 : 0,
|
||||
...(hasNotifications && pendingMode !== "legacy" ? { notification_previews: ["First notification", "Long notification — " + "notification text ".repeat(30)] } : {}),
|
||||
head_id: submissions[0]?.submission_id ?? null,
|
||||
submissions,
|
||||
},
|
||||
entries: [7, 8, 9, 10, 11, 12].flatMap(historyTurnEntries),
|
||||
entries: [...[7, 8, 9, 10, 11, 12].flatMap(historyTurnEntries), ...taskEntries],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/// <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[] = [];
|
||||
page.on("pageerror", (error) => errors.push(String(error)));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") errors.push(message.text());
|
||||
});
|
||||
page.on("requestfailed", (request) => errors.push(request.url()));
|
||||
for (const mode of ["both", "queue", "notifications", "legacy", "empty"]) {
|
||||
await page.goto(
|
||||
`${url}/w/console-history-review/workers/W-900-console-fixture/console?pending=${mode}`,
|
||||
);
|
||||
await page.locator(".task-mini-row").waitFor();
|
||||
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(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;
|
||||
}
|
||||
});
|
||||
@@ -11,7 +11,7 @@
|
||||
"web-ux": "deno run --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys cli.ts",
|
||||
"check": "deno check cli.ts src/*.ts tests/*.ts browser-tests/*.ts",
|
||||
"test": "deno test --allow-env --allow-read --allow-write --allow-run --allow-sys tests",
|
||||
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts browser-tests/workspace_settings_shell_test.ts"
|
||||
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts browser-tests/workspace_settings_shell_test.ts browser-tests/console_pending_shell_test.ts"
|
||||
},
|
||||
"fmt": {
|
||||
"lineWidth": 100,
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "console-pending",
|
||||
"title": "Console pending inputs and notifications",
|
||||
"baseUrl": "http://127.0.0.1:15283",
|
||||
"colorScheme": "light",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [
|
||||
{
|
||||
"id": "fixture",
|
||||
"label": "Retained Console fixture",
|
||||
"auth": {
|
||||
"kind": "anonymous"
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"label": "desktop",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "narrow",
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "mobile",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "both",
|
||||
"label": "both",
|
||||
"path": "/w/console-history-review/workers/W-900-console-fixture/console?pending=both",
|
||||
"goal": "Only populated scopes appear; headings and previews match Tasks typography and row height.",
|
||||
"dataState": "Synthetic retained Console: both; two long/short previews when present; one task. Cancellation is unavailable on retained sessions.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".task-mini-row"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Pending input layout"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "queue",
|
||||
"label": "queue",
|
||||
"path": "/w/console-history-review/workers/W-900-console-fixture/console?pending=queue",
|
||||
"goal": "Only populated scopes appear; headings and previews match Tasks typography and row height.",
|
||||
"dataState": "Synthetic retained Console: queue; two long/short previews when present; one task. Cancellation is unavailable on retained sessions.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".task-mini-row"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Pending input layout"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"label": "notifications",
|
||||
"path": "/w/console-history-review/workers/W-900-console-fixture/console?pending=notifications",
|
||||
"goal": "Only populated scopes appear; headings and previews match Tasks typography and row height.",
|
||||
"dataState": "Synthetic retained Console: notifications; two long/short previews when present; one task. Cancellation is unavailable on retained sessions.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".task-mini-row"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Pending input layout"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "legacy",
|
||||
"label": "legacy",
|
||||
"path": "/w/console-history-review/workers/W-900-console-fixture/console?pending=legacy",
|
||||
"goal": "Only populated scopes appear; headings and previews match Tasks typography and row height.",
|
||||
"dataState": "Synthetic retained Console: legacy; two long/short previews when present; one task. Cancellation is unavailable on retained sessions.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".task-mini-row"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Pending input layout"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "empty",
|
||||
"label": "empty",
|
||||
"path": "/w/console-history-review/workers/W-900-console-fixture/console?pending=empty",
|
||||
"goal": "Only populated scopes appear; headings and previews match Tasks typography and row height.",
|
||||
"dataState": "Synthetic retained Console: empty; two long/short previews when present; one task. Cancellation is unavailable on retained sessions.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".task-mini-row"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Pending input layout"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"processes": [
|
||||
{
|
||||
"id": "console-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"browser-tests/console_history_fixture_server.ts",
|
||||
"15283",
|
||||
"../../web/workspace/build"
|
||||
],
|
||||
"readyUrl": "http://127.0.0.1:15283/health"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1892,10 +1892,10 @@
|
||||
/>
|
||||
|
||||
{#if pendingSubmissionItems.length > 0 || pendingSubmissions.notification_count > 0}
|
||||
<section class="pending-submissions" aria-label="Pending activations">
|
||||
<div class="pending-column pending-queue" role="group" aria-label="Queued inputs">
|
||||
<h2 class="pending-submissions-label">{pendingSubmissionItems.length} Queued</h2>
|
||||
{#if pendingSubmissionItems.length > 0}
|
||||
<section class="pending-submissions" class:has-both={pendingSubmissionItems.length > 0 && pendingSubmissions.notification_count > 0} aria-label="Pending activations">
|
||||
{#if pendingSubmissionItems.length > 0}
|
||||
<div class="pending-column pending-queue" role="group" aria-label="Queued inputs">
|
||||
<h2 class="pending-submissions-label">{pendingSubmissionItems.length} Queued</h2>
|
||||
<ol aria-label="Queued inputs">
|
||||
{#each pendingSubmissionItems as submission, index (submission.submission_id)}
|
||||
<li>
|
||||
@@ -1923,11 +1923,11 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pending-column pending-notifications" role="group" aria-label="Notifications">
|
||||
<h2 class="pending-submissions-label">Notifications</h2>
|
||||
{#if pendingSubmissions.notification_count > 0}
|
||||
</div>
|
||||
{/if}
|
||||
{#if pendingSubmissions.notification_count > 0}
|
||||
<div class="pending-column pending-notifications" role="group" aria-label="Notifications">
|
||||
<h2 class="pending-submissions-label">Notifications</h2>
|
||||
<ol aria-label="Pending notifications">
|
||||
{#each pendingSubmissions.notification_previews ?? [] as preview}
|
||||
<li>
|
||||
@@ -1941,8 +1941,8 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -2309,24 +2309,34 @@
|
||||
|
||||
.pending-submissions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
margin: 0 var(--space-3);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-compact);
|
||||
line-height: var(--line-height-compact);
|
||||
}
|
||||
|
||||
.pending-submissions.has-both {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pending-column {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: var(--space-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pending-submissions li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-height: var(--space-6);
|
||||
height: var(--line-height-compact);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -2340,16 +2350,16 @@
|
||||
}
|
||||
|
||||
.pending-submissions-label {
|
||||
margin: 0 0 var(--space-1);
|
||||
font-size: var(--font-size-compact);
|
||||
line-height: var(--line-height-compact);
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.pending-submissions ol {
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
max-height: calc(var(--space-6) * 4);
|
||||
max-height: calc(var(--line-height-compact) * 4 + var(--space-1) * 3);
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -2371,8 +2381,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--space-6);
|
||||
height: var(--space-6);
|
||||
padding: var(--space-2);
|
||||
height: var(--line-height-compact);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--radius-soft);
|
||||
background: transparent;
|
||||
|
||||
+30
-3
@@ -369,7 +369,8 @@ test("notification-only state displays previews without actions and supports leg
|
||||
session.pending_submissions.head_id = "notification-head";
|
||||
latestListener().onFrame(subscribedFrame(session));
|
||||
const region = await screen.findByRole("region", { name: "Pending activations" });
|
||||
expect(region.textContent).toContain("0 Queued");
|
||||
expect(screen.queryByRole("group", { name: "Queued inputs" })).toBeNull();
|
||||
expect(screen.queryByRole("heading", { name: "0 Queued" })).toBeNull();
|
||||
expect(region.textContent).toContain("一件目の通知");
|
||||
expect(region.textContent).toContain("二件目の通知");
|
||||
expect(region.querySelectorAll("li")).toHaveLength(2);
|
||||
@@ -382,7 +383,7 @@ test("notification-only state displays previews without actions and supports leg
|
||||
await waitFor(() => expect(screen.queryByRole("region", { name: "Pending activations" })).toBeNull());
|
||||
});
|
||||
|
||||
test("queue-only state keeps the notification column empty and disables cancellation when disconnected", async () => {
|
||||
test("queue-only state hides notifications and disables cancellation when disconnected", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ availability: "live_protocol" })));
|
||||
render(ConsolePage, { data: pageData() });
|
||||
await waitFor(() => expect(multiplexer.subscribe).toHaveBeenCalledOnce());
|
||||
@@ -390,7 +391,8 @@ test("queue-only state keeps the notification column empty and disables cancella
|
||||
session.pending_submissions.submissions = [{ submission_id: "queue-1", preview: "送信内容", accepted_at_ms: 1, segment_count: 1, byte_len: 12 }];
|
||||
latestListener().onFrame(subscribedFrame(session));
|
||||
await screen.findByText("送信内容");
|
||||
expect(screen.getByRole("group", { name: "Notifications" }).querySelector("ol")).toBeNull();
|
||||
expect(screen.queryByRole("group", { name: "Notifications" })).toBeNull();
|
||||
expect(screen.queryByRole("heading", { name: "Notifications" })).toBeNull();
|
||||
latestListener().onStatus?.("closed", "connection lost");
|
||||
await waitFor(() => expect((screen.getByRole("button", { name: "Cancel queued input 1" }) as HTMLButtonElement).disabled).toBe(true));
|
||||
});
|
||||
@@ -1137,3 +1139,28 @@ function expectHistoryTurn(text: string) {
|
||||
expect(screen.getAllByText(text)).toHaveLength(1);
|
||||
expect(screen.getByRole("button", { name: new RegExp("^Turn \\d+: " + text + "$") })).not.toBeNull();
|
||||
}
|
||||
|
||||
test("pending groups independently follow authoritative changes, including zero count with stale previews", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ availability: "live_protocol" })));
|
||||
render(ConsolePage, { data: pageData() });
|
||||
await waitFor(() => expect(multiplexer.subscribe).toHaveBeenCalledOnce());
|
||||
const session = emptySession();
|
||||
session.pending_submissions.submissions = [{ submission_id: "queue-1", preview: "Queued", accepted_at_ms: 1, segment_count: 1, byte_len: 6 }];
|
||||
session.pending_submissions.notification_count = 1;
|
||||
session.pending_submissions.notification_previews = ["Notice"];
|
||||
latestListener().onFrame(subscribedFrame(session));
|
||||
await screen.findByRole("group", { name: "Queued inputs" });
|
||||
expect(screen.getByRole("group", { name: "Notifications" })).toBeTruthy();
|
||||
session.pending_submissions.notification_count = 0;
|
||||
latestListener().onFrame(subscribedFrame(session));
|
||||
await waitFor(() => expect(screen.queryByRole("group", { name: "Notifications" })).toBeNull());
|
||||
expect(screen.getByRole("group", { name: "Queued inputs" })).toBeTruthy();
|
||||
session.pending_submissions.submissions = [];
|
||||
session.pending_submissions.notification_count = 1;
|
||||
latestListener().onFrame(subscribedFrame(session));
|
||||
await waitFor(() => expect(screen.queryByRole("group", { name: "Queued inputs" })).toBeNull());
|
||||
expect(screen.getByRole("group", { name: "Notifications" })).toBeTruthy();
|
||||
session.pending_submissions.notification_count = 0;
|
||||
latestListener().onFrame(subscribedFrame(session));
|
||||
await waitFor(() => expect(screen.queryByRole("region", { name: "Pending activations" })).toBeNull());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user