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
|
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`.
|
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
|
## Adding a scenario
|
||||||
|
|
||||||
1. Name the concrete user task and expected data state; do not write “looks correct”.
|
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 ===
|
url.pathname ===
|
||||||
`/api/w/${workspaceId}/runtimes/${runtimeId}/workers/${workerId}/session`
|
`/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({
|
return json({
|
||||||
availability: "retained_snapshot",
|
availability: "retained_snapshot",
|
||||||
identity: { session_id: "session-a", segment_id: "segment-a", entry_count: 36 },
|
identity: { session_id: "session-a", segment_id: "segment-a", entry_count: 36 },
|
||||||
snapshot: {
|
snapshot: {
|
||||||
pending_submissions: {
|
pending_submissions: {
|
||||||
revision: 0,
|
revision: 0,
|
||||||
notification_count: 0,
|
notification_count: hasNotifications ? 2 : 0,
|
||||||
head_id: null,
|
...(hasNotifications && pendingMode !== "legacy" ? { notification_previews: ["First notification", "Long notification — " + "notification text ".repeat(30)] } : {}),
|
||||||
submissions: [],
|
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",
|
"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",
|
"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": "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": {
|
"fmt": {
|
||||||
"lineWidth": 100,
|
"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}
|
{#if pendingSubmissionItems.length > 0 || pendingSubmissions.notification_count > 0}
|
||||||
<section class="pending-submissions" aria-label="Pending activations">
|
<section class="pending-submissions" class:has-both={pendingSubmissionItems.length > 0 && pendingSubmissions.notification_count > 0} aria-label="Pending activations">
|
||||||
<div class="pending-column pending-queue" role="group" aria-label="Queued inputs">
|
{#if pendingSubmissionItems.length > 0}
|
||||||
<h2 class="pending-submissions-label">{pendingSubmissionItems.length} Queued</h2>
|
<div class="pending-column pending-queue" role="group" aria-label="Queued inputs">
|
||||||
{#if pendingSubmissionItems.length > 0}
|
<h2 class="pending-submissions-label">{pendingSubmissionItems.length} Queued</h2>
|
||||||
<ol aria-label="Queued inputs">
|
<ol aria-label="Queued inputs">
|
||||||
{#each pendingSubmissionItems as submission, index (submission.submission_id)}
|
{#each pendingSubmissionItems as submission, index (submission.submission_id)}
|
||||||
<li>
|
<li>
|
||||||
@@ -1923,11 +1923,11 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ol>
|
</ol>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
<div class="pending-column pending-notifications" role="group" aria-label="Notifications">
|
{#if pendingSubmissions.notification_count > 0}
|
||||||
<h2 class="pending-submissions-label">Notifications</h2>
|
<div class="pending-column pending-notifications" role="group" aria-label="Notifications">
|
||||||
{#if pendingSubmissions.notification_count > 0}
|
<h2 class="pending-submissions-label">Notifications</h2>
|
||||||
<ol aria-label="Pending notifications">
|
<ol aria-label="Pending notifications">
|
||||||
{#each pendingSubmissions.notification_previews ?? [] as preview}
|
{#each pendingSubmissions.notification_previews ?? [] as preview}
|
||||||
<li>
|
<li>
|
||||||
@@ -1941,8 +1941,8 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ol>
|
</ol>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -2309,24 +2309,34 @@
|
|||||||
|
|
||||||
.pending-submissions {
|
.pending-submissions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0 var(--space-3);
|
margin: 0 var(--space-3);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
font-size: var(--font-size-compact);
|
font-size: var(--font-size-compact);
|
||||||
line-height: var(--line-height-compact);
|
line-height: var(--line-height-compact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pending-submissions.has-both {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.pending-column {
|
.pending-column {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: var(--space-1);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pending-submissions li {
|
.pending-submissions li {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
min-height: var(--space-6);
|
height: var(--line-height-compact);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2340,16 +2350,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pending-submissions-label {
|
.pending-submissions-label {
|
||||||
margin: 0 0 var(--space-1);
|
margin: 0;
|
||||||
font-size: var(--font-size-compact);
|
padding: 0;
|
||||||
line-height: var(--line-height-compact);
|
font: inherit;
|
||||||
font-weight: 500;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pending-submissions ol {
|
.pending-submissions ol {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-1);
|
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;
|
overflow-y: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -2371,8 +2381,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: var(--space-6);
|
width: var(--space-6);
|
||||||
height: var(--space-6);
|
height: var(--line-height-compact);
|
||||||
padding: var(--space-2);
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius-soft);
|
border-radius: var(--radius-soft);
|
||||||
background: transparent;
|
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";
|
session.pending_submissions.head_id = "notification-head";
|
||||||
latestListener().onFrame(subscribedFrame(session));
|
latestListener().onFrame(subscribedFrame(session));
|
||||||
const region = await screen.findByRole("region", { name: "Pending activations" });
|
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.textContent).toContain("二件目の通知");
|
expect(region.textContent).toContain("二件目の通知");
|
||||||
expect(region.querySelectorAll("li")).toHaveLength(2);
|
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());
|
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" })));
|
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ availability: "live_protocol" })));
|
||||||
render(ConsolePage, { data: pageData() });
|
render(ConsolePage, { data: pageData() });
|
||||||
await waitFor(() => expect(multiplexer.subscribe).toHaveBeenCalledOnce());
|
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 }];
|
session.pending_submissions.submissions = [{ submission_id: "queue-1", preview: "送信内容", accepted_at_ms: 1, segment_count: 1, byte_len: 12 }];
|
||||||
latestListener().onFrame(subscribedFrame(session));
|
latestListener().onFrame(subscribedFrame(session));
|
||||||
await screen.findByText("送信内容");
|
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");
|
latestListener().onStatus?.("closed", "connection lost");
|
||||||
await waitFor(() => expect((screen.getByRole("button", { name: "Cancel queued input 1" }) as HTMLButtonElement).disabled).toBe(true));
|
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.getAllByText(text)).toHaveLength(1);
|
||||||
expect(screen.getByRole("button", { name: new RegExp("^Turn \\d+: " + text + "$") })).not.toBeNull();
|
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