fix: synchronize native picker interception in Feature browser tests
This commit is contained in:
@@ -217,6 +217,50 @@ It captures 1440px, 768px, and 390px; `browser-tests/console_pending_shell_test.
|
||||
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.
|
||||
|
||||
## Console live Feature invocation fixture
|
||||
|
||||
`scenarios/console-live-light.json` and `console-live-dark.json` capture the **production Console**
|
||||
with a synthetic live Worker at 1440px, 768px, and 390px. The fixture provides metadata for `review`
|
||||
(enum, integer, boolean, and Worker-file arguments) and `attach` (the generic attachment client
|
||||
adapter), a real workspace protocol WebSocket, request-ID-preserving completion responses, and
|
||||
synthetic upload staging. It never executes a Worker or Feature invocation. No fixture CSS, shell,
|
||||
or editor overrides are used.
|
||||
|
||||
Build production sources first, optionally from an isolated copy under `target/web-ux`, then select
|
||||
the build explicitly. From `tools/web-ux`:
|
||||
|
||||
```sh
|
||||
export WEB_UX_BUILD_ROOT='<absolute-path-to-production-build>'
|
||||
deno task web-ux capture --scenario scenarios/console-live-light.json --output ../../target/web-ux --run-id console-live-light
|
||||
deno task web-ux capture --scenario scenarios/console-live-dark.json --output ../../target/web-ux --run-id console-live-dark
|
||||
deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/console_live_shell_test.ts browser-tests/console_live_admission_shell_test.ts
|
||||
```
|
||||
|
||||
`WEB_UX_LIFECYCLE_OUTPUT` optionally places browser interaction screenshots and JSON evidence at an
|
||||
explicit artifact path. Tests consume an existing build; they do not rebuild or mutate Web sources.
|
||||
The lifecycle test covers argument completion, editing/removing/undoing typed chips, native picker
|
||||
cancellation, generic-adapter staging, literal unselected slash prose, typed Submit values, and
|
||||
abandoned-file DELETE. Admission tests verify rejection preserves drafts and staged files,
|
||||
transport send alone cannot clear the draft, authoritative acceptance transfers ownership, and
|
||||
failed/in-flight-cancelled uploads cannot Submit. A retry/late-success matrix verifies stable upload
|
||||
IDs, deleted reservation non-resurrection, and abandoned resource cleanup at accepted draft boundaries.
|
||||
Intentional upload HTTP503 browser errors are recorded separately from unexpected failures.
|
||||
The Chromium picker helper awaits interception acknowledgement before real gestures (not a timeout
|
||||
extension or a replacement input); diagnostics retain user activation, input connectivity, and DOM
|
||||
lifecycle evidence.
|
||||
|
||||
The localhost-only fixture control endpoint sets deterministic `submit: accept | reject | hold`
|
||||
and `upload: normal | fail | hold` modes; `accept_pending: true` releases held admission responses and
|
||||
`release_uploads: true` delivers real successful responses for held uploads.
|
||||
`/fixture-state` exposes actual received protocol methods, HTTP requests, emitted admission events,
|
||||
upload attempts (including retry IDs), completed uploads, and DELETE IDs for assertions. Only synthetic data is recorded.
|
||||
|
||||
`console-live-baseline-light.json` and `console-live-baseline-dark.json` use the same production
|
||||
shell/data and capture initial plus unselected literal slash text. They support a pre-Feature HEAD
|
||||
build, where no implicit Feature completion or typed chip exists. Initial and `slash-plain` capture
|
||||
keys are shared with the current scenarios for like-for-like comparison; Feature-specific states
|
||||
are explicitly new behavior, not fabricated baseline controls.
|
||||
|
||||
## Adding a scenario
|
||||
|
||||
1. Name the concrete user task and expected data state; do not write “looks correct”.
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
/// <reference lib="dom" />
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { dirname, fromFileUrl, join } from "@std/path";
|
||||
import { chromium, type Page } from "playwright";
|
||||
import { Buffer } from "node:buffer";
|
||||
import {
|
||||
observePicker,
|
||||
pickerDiagnostics,
|
||||
prepareNativePicker,
|
||||
} from "./console_live_picker_diagnostics.ts";
|
||||
|
||||
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
|
||||
const route = "/w/console-live-review/workers/W-901-console-live-fixture/console";
|
||||
const output = Deno.env.get("WEB_UX_LIFECYCLE_OUTPUT") ??
|
||||
join(root, "target/web-ux/console-live-lifecycle");
|
||||
type State = {
|
||||
methods: {
|
||||
method: string;
|
||||
params?: {
|
||||
input?: import("../../../web/workspace/src/lib/generated/protocol.ts").Segment[];
|
||||
submission_request_id?: string;
|
||||
};
|
||||
}[];
|
||||
events: { event: string }[];
|
||||
requests: { method: string; path: string }[];
|
||||
uploads: string[];
|
||||
upload_attempts: string[];
|
||||
deletes: string[];
|
||||
held_uploads: string[];
|
||||
};
|
||||
const state = async (base: string): Promise<State> =>
|
||||
await (await fetch(base + "/fixture-state")).json();
|
||||
const control = async (base: string, value: unknown): Promise<void> => {
|
||||
const response = await fetch(base + "/fixture-control", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(value),
|
||||
});
|
||||
assert(response.ok);
|
||||
await response.body?.cancel();
|
||||
};
|
||||
async function eventually(base: string, predicate: (value: State) => boolean): Promise<State> {
|
||||
const deadline = Date.now() + 5000;
|
||||
do {
|
||||
const value = await state(base);
|
||||
if (predicate(value)) return value;
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
} while (Date.now() < deadline);
|
||||
throw new Error("Fixture evidence did not reach expected state");
|
||||
}
|
||||
type FixtureBody = (
|
||||
page: Page,
|
||||
base: string,
|
||||
errors: string[],
|
||||
caseOutput: string,
|
||||
) => Promise<void>;
|
||||
async function fixture(body: FixtureBody, matrix = false): Promise<void> {
|
||||
for (const theme of matrix ? ["light", "dark"] as const : ["light"] as const) {
|
||||
for (const width of matrix ? [1440, 768, 390] : [1440]) await fixtureCase(body, theme, width);
|
||||
}
|
||||
}
|
||||
async function fixtureCase(
|
||||
body: FixtureBody,
|
||||
theme: "light" | "dark",
|
||||
width: number,
|
||||
): Promise<void> {
|
||||
const caseOutput = join(output, `${theme}-${width}`);
|
||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
const port = (listener.addr as Deno.NetAddr).port;
|
||||
listener.close();
|
||||
const base = `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_live_fixture_server.ts"),
|
||||
String(port),
|
||||
Deno.env.get("WEB_UX_BUILD_ROOT") ?? join(root, "web/workspace/build"),
|
||||
],
|
||||
stdout: "null",
|
||||
stderr: "inherit",
|
||||
}).spawn();
|
||||
try {
|
||||
const deadline = Date.now() + 10000;
|
||||
let started = false;
|
||||
while (!started && Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(base + "/health");
|
||||
started = response.ok;
|
||||
await response.body?.cancel();
|
||||
} catch { /* bounded owned startup */ }
|
||||
if (!started) await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
assert(started);
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage({
|
||||
viewport: { width, height: width === 390 ? 844 : 900 },
|
||||
colorScheme: theme,
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(String(error)));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") errors.push(message.text());
|
||||
});
|
||||
await prepareNativePicker(page);
|
||||
await observePicker(page);
|
||||
await page.goto(base + route);
|
||||
await page.locator(".cm-content[contenteditable=true]").waitFor();
|
||||
await page.getByText("Ready. Choose a Feature", { exact: false }).waitFor();
|
||||
await Deno.mkdir(caseOutput, { recursive: true });
|
||||
try {
|
||||
await body(page, base, errors, caseOutput);
|
||||
await Deno.writeTextFile(
|
||||
join(caseOutput, `picker-${crypto.randomUUID()}.json`),
|
||||
JSON.stringify(await pickerDiagnostics(page), null, 2),
|
||||
);
|
||||
} catch (error) {
|
||||
const id = crypto.randomUUID();
|
||||
await page.screenshot({ path: join(caseOutput, `admission-failure-${id}.png`) });
|
||||
await Deno.writeTextFile(
|
||||
join(caseOutput, `admission-failure-${id}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
error: String(error),
|
||||
body: await page.locator("body").innerText(),
|
||||
errors,
|
||||
picker: await pickerDiagnostics(page),
|
||||
fixture: await state(base),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
} finally {
|
||||
server.kill("SIGTERM");
|
||||
await server.status;
|
||||
}
|
||||
}
|
||||
async function draft(page: Page, text: string): Promise<void> {
|
||||
await page.locator(".cm-content").click();
|
||||
await page.keyboard.press("Control+a");
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.type(text);
|
||||
}
|
||||
async function review(page: Page): Promise<void> {
|
||||
await draft(page, "/rev");
|
||||
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Escape");
|
||||
await page.keyboard.type('mode="brief", count=2, enabled=true)');
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
}
|
||||
async function attach(page: Page, name: string): Promise<void> {
|
||||
await draft(page, "/att");
|
||||
await page.getByRole("option").filter({ hasText: "attach" }).waitFor();
|
||||
const chooser = page.waitForEvent("filechooser");
|
||||
await page.keyboard.press("Enter");
|
||||
const nativeChooser = await chooser;
|
||||
const diagnostics = await pickerDiagnostics(page) as {
|
||||
kind: string;
|
||||
active: boolean;
|
||||
connected: boolean;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
const click = diagnostics.filter((record) => record.kind === "input.click").at(-1);
|
||||
assert(
|
||||
click?.active && click.connected && !click.disabled,
|
||||
"native picker must run synchronously with active real gesture and connected enabled input",
|
||||
);
|
||||
await nativeChooser.setFiles({
|
||||
name,
|
||||
mimeType: "text/markdown",
|
||||
buffer: Buffer.from("Synthetic fixture document"),
|
||||
});
|
||||
}
|
||||
Deno.test("production leading slash remains responsive after loop fix", () =>
|
||||
fixture(async (page, base, errors, caseOutput) => {
|
||||
await draft(page, "/");
|
||||
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
|
||||
assertEquals(
|
||||
(await state(base)).methods.filter((method) => method.method !== "list_completions"),
|
||||
[],
|
||||
);
|
||||
assertEquals((await state(base)).uploads, []);
|
||||
assertEquals(errors, []);
|
||||
await Deno.writeTextFile(
|
||||
join(caseOutput, "slash-fix-result.json"),
|
||||
JSON.stringify({ status: "pass", fixture: await state(base), errors }, null, 2),
|
||||
);
|
||||
}));
|
||||
Deno.test("production Submit rejection retains typed draft and only authoritative acceptance clears it", () =>
|
||||
fixture(async (page, base, errors, caseOutput) => {
|
||||
await review(page);
|
||||
await attach(page, "admission.md");
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
// Keep the uploaded atom and add a completion-selected Feature next to it.
|
||||
await page.keyboard.press("Control+End");
|
||||
await page.keyboard.type(" /rev");
|
||||
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Escape");
|
||||
await page.keyboard.type('mode="brief", count=2, enabled=true)');
|
||||
await page.waitForFunction(() =>
|
||||
document.querySelectorAll(".composer-typed-chip").length === 2
|
||||
);
|
||||
await control(base, { submit: "reject" });
|
||||
await page.keyboard.press("Control+Enter");
|
||||
await page.getByText("Synthetic admission rejection; draft was not accepted.", { exact: false })
|
||||
.waitFor();
|
||||
const rejected = await eventually(
|
||||
base,
|
||||
(value) => value.events.some((event) => event.event === "submission_rejected"),
|
||||
);
|
||||
assertEquals(await page.locator(".composer-typed-chip").count(), 2);
|
||||
assertEquals(rejected.deletes, [], "rejection must keep staged file ownership");
|
||||
await page.screenshot({ path: join(caseOutput, "admission-rejected.png") });
|
||||
await control(base, { submit: "hold" });
|
||||
await page.locator(".cm-content[contenteditable=true]").waitFor();
|
||||
await page.locator(".cm-content").focus();
|
||||
await page.keyboard.press("Control+Enter");
|
||||
const pending = await eventually(
|
||||
base,
|
||||
(value) => value.methods.filter((method) => method.method === "submit").length === 2,
|
||||
);
|
||||
assertEquals(
|
||||
await page.locator(".composer-typed-chip").count(),
|
||||
2,
|
||||
"transport send is not admission",
|
||||
);
|
||||
assertEquals(pending.deletes, []);
|
||||
await page.screenshot({ path: join(caseOutput, "admission-pending.png") });
|
||||
await control(base, { accept_pending: true });
|
||||
await page.waitForFunction(() =>
|
||||
document.querySelectorAll(".composer-typed-chip").length === 0
|
||||
);
|
||||
const accepted = await eventually(
|
||||
base,
|
||||
(value) => value.events.some((event) => event.event === "submission_accepted"),
|
||||
);
|
||||
assertEquals(accepted.deletes, [], "accepted file ownership transfers; no staging DELETE");
|
||||
const submissions = accepted.methods.filter((method) => method.method === "submit");
|
||||
assertEquals(submissions.length, 2);
|
||||
assert(
|
||||
submissions.every((method) =>
|
||||
method.params?.input?.some((segment) => segment.kind === "uploaded_file")
|
||||
),
|
||||
);
|
||||
assert(
|
||||
submissions.every((method) =>
|
||||
method.params?.input?.some((segment) => segment.kind === "feature_invoke")
|
||||
),
|
||||
);
|
||||
assertEquals(
|
||||
submissions[0].params?.input,
|
||||
submissions[1].params?.input,
|
||||
"retry preserves structured invocation IDs and values",
|
||||
);
|
||||
assertEquals(errors, []);
|
||||
await page.screenshot({ path: join(caseOutput, "admission-accepted.png") });
|
||||
await Deno.writeTextFile(
|
||||
join(caseOutput, "admission-result.json"),
|
||||
JSON.stringify({ status: "pass", rejected, pending, accepted, errors }, null, 2),
|
||||
);
|
||||
}, true));
|
||||
Deno.test("production attachment upload failure and in-flight cancellation never Submit and record DELETE", () =>
|
||||
fixture(async (page, base, errors, caseOutput) => {
|
||||
await control(base, { upload: "fail" });
|
||||
await attach(page, "failure.md");
|
||||
await page.getByText("Upload failed (503).", { exact: true }).waitFor();
|
||||
await page.screenshot({ path: join(caseOutput, "upload-failed.png") });
|
||||
await page.getByRole("button", { name: "Remove failure.md", exact: true }).click();
|
||||
const failed = await state(base);
|
||||
assertEquals(failed.uploads, []);
|
||||
assertEquals(failed.methods.filter((method) => method.method === "submit"), []);
|
||||
await control(base, { upload: "hold" });
|
||||
await attach(page, "cancel.md");
|
||||
await eventually(base, (value) => value.held_uploads.length === 1);
|
||||
await page.screenshot({ path: join(caseOutput, "upload-pending.png") });
|
||||
await page.getByRole("button", { name: "Remove cancel.md", exact: true }).click();
|
||||
const cancelled = await eventually(
|
||||
base,
|
||||
(value) => value.deletes.length > failed.deletes.length && value.held_uploads.length === 0,
|
||||
);
|
||||
assertEquals(cancelled.uploads, []);
|
||||
assertEquals(cancelled.methods.filter((method) => method.method === "submit"), []);
|
||||
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
|
||||
await page.screenshot({ path: join(caseOutput, "upload-cancelled.png") });
|
||||
assert(
|
||||
cancelled.requests.filter((request) => request.method === "DELETE").every((request) =>
|
||||
request.path.includes("/attachment-uploads/")
|
||||
),
|
||||
);
|
||||
// The fixture's deliberate HTTP503 emits a browser resource error, not a page exception.
|
||||
assert(errors.every((error) => /503/.test(error)), JSON.stringify(errors));
|
||||
await Deno.writeTextFile(
|
||||
join(caseOutput, "upload-failure-cancellation-result.json"),
|
||||
JSON.stringify({ status: "pass", failed, cancelled, expectedErrors: errors }, null, 2),
|
||||
);
|
||||
}, true));
|
||||
|
||||
Deno.test("production upload retry retains upload ID and late completion cannot resurrect deleted reservation", () =>
|
||||
fixture(async (page, base, errors, caseOutput) => {
|
||||
await control(base, { upload: "fail" });
|
||||
await attach(page, "retry.md");
|
||||
await page.getByText("Upload failed (503).", { exact: true }).waitFor();
|
||||
const failed = await state(base);
|
||||
assertEquals(failed.upload_attempts.length, 1);
|
||||
await control(base, { upload: "normal" });
|
||||
await page.getByRole("button", { name: "Retry", exact: true }).click();
|
||||
await page.locator(".composer-typed-chip").filter({ hasText: "Attached retry.md" }).waitFor();
|
||||
const retried = await state(base);
|
||||
assertEquals(
|
||||
retried.upload_attempts,
|
||||
[failed.upload_attempts[0], failed.upload_attempts[0]],
|
||||
"retry keeps staging identity",
|
||||
);
|
||||
assertEquals(retried.uploads, [failed.upload_attempts[0]]);
|
||||
await page.screenshot({ path: join(caseOutput, "upload-retried.png") });
|
||||
// Submit authoritative acceptance is covered separately; this draft is abandoned.
|
||||
await draft(page, "deleted upload");
|
||||
await page.keyboard.press("Escape");
|
||||
// Clearing/accepting this text boundary releases the abandoned staged resource.
|
||||
await page.keyboard.press("Control+Enter");
|
||||
const deleted = await eventually(base, (value) => value.deletes.includes(retried.uploads[0]));
|
||||
await control(base, { upload: "hold" });
|
||||
await attach(page, "late.md");
|
||||
const held = await eventually(base, (value) => value.held_uploads.length === 1);
|
||||
const lateId = held.held_uploads[0];
|
||||
await page.locator(".composer-typed-chip").filter({ hasText: "Uploading late.md" }).waitFor();
|
||||
// Delete only the reservation, without aborting its real pending upload.
|
||||
await page.locator(".cm-content").focus();
|
||||
await page.keyboard.press("Control+End");
|
||||
await page.keyboard.press("Backspace");
|
||||
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
|
||||
await page.screenshot({ path: join(caseOutput, "late-upload-deleted.png") });
|
||||
const response = page.waitForResponse((response) =>
|
||||
response.request().method() === "PUT" && response.url().endsWith(lateId)
|
||||
);
|
||||
await control(base, { release_uploads: true });
|
||||
assertEquals((await response).status(), 200);
|
||||
const late = await eventually(
|
||||
base,
|
||||
(value) => value.uploads.includes(lateId) && value.held_uploads.length === 0,
|
||||
);
|
||||
assertEquals(
|
||||
await page.locator(".composer-typed-chip").count(),
|
||||
0,
|
||||
"late success cannot resurrect deleted atom",
|
||||
);
|
||||
assertEquals(
|
||||
late.methods.filter((method) => method.method === "submit").length,
|
||||
1,
|
||||
"upload/retry/late completion never submits",
|
||||
);
|
||||
// Deleted resources remain undo-owned until the next accepted draft boundary.
|
||||
await page.screenshot({ path: join(caseOutput, "late-upload-completed.png") });
|
||||
await draft(page, "finish late deletion");
|
||||
await page.keyboard.press("Control+Enter");
|
||||
const completed = await eventually(base, (value) => value.deletes.includes(lateId));
|
||||
assertEquals(completed.methods.filter((method) => method.method === "submit").length, 2);
|
||||
assertEquals(
|
||||
completed.methods.filter((method) => method.method === "submit").at(-1)?.params?.input,
|
||||
[{ kind: "text", content: "finish late deletion" }],
|
||||
);
|
||||
assert(errors.every((error) => /503/.test(error)), JSON.stringify(errors));
|
||||
await Deno.writeTextFile(
|
||||
join(caseOutput, "retry-late-result.json"),
|
||||
JSON.stringify(
|
||||
{ status: "pass", failed, retried, deleted, held, late, completed, expectedErrors: errors },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}, true));
|
||||
|
||||
Deno.test("production selected invocation preserves multiple positional typed arguments", () =>
|
||||
fixture(async (page, base, errors, caseOutput) => {
|
||||
await draft(page, "/rev");
|
||||
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Escape");
|
||||
await page.keyboard.type('"brief", 2, true)');
|
||||
await page.locator(".composer-typed-chip").waitFor({ timeout: 2000 });
|
||||
assertEquals((await state(base)).methods.filter((method) => method.method === "submit"), []);
|
||||
assertEquals(errors, []);
|
||||
await Deno.writeTextFile(
|
||||
join(caseOutput, "positional-result.json"),
|
||||
JSON.stringify({ status: "pass", fixture: await state(base), errors }, null, 2),
|
||||
);
|
||||
}));
|
||||
@@ -0,0 +1,470 @@
|
||||
import { extname, join, normalize } from "jsr:@std/path@1.1.4";
|
||||
|
||||
// Synthetic authority, real production shell and protocol transport. No UI overrides.
|
||||
const port = Number(Deno.args[0]);
|
||||
const buildRoot = Deno.args[1] ?? Deno.env.get("WEB_UX_BUILD_ROOT");
|
||||
if (!Number.isInteger(port) || !buildRoot) {
|
||||
throw new Error("usage: console_live_fixture_server.ts <port> <build-root>");
|
||||
}
|
||||
const workspaceId = "console-live-review";
|
||||
const runtimeId = "runtime-a";
|
||||
const workerId = "worker-a";
|
||||
const workerPath = `/api/w/${workspaceId}/runtimes/${runtimeId}/workers/${workerId}`;
|
||||
const json = (value: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(value), {
|
||||
status,
|
||||
headers: { "content-type": "application/json; charset=utf-8" },
|
||||
});
|
||||
const invocations:
|
||||
import("../../../web/workspace/src/lib/generated/protocol.ts").FeatureInvocationDescriptor[] = [
|
||||
{
|
||||
identity: "fixture.review",
|
||||
name: "review",
|
||||
aliases: ["rv"],
|
||||
display_name: "Review",
|
||||
description: "Review a synthetic document without running a real Worker.",
|
||||
syntax: "parenthesized",
|
||||
arguments: [
|
||||
{
|
||||
name: "mode",
|
||||
position: 0,
|
||||
required: true,
|
||||
value_type: { kind: "enum", values: ["brief", "thorough"] },
|
||||
completion: { kind: "static", values: ["brief", "thorough"] },
|
||||
description: "Review detail",
|
||||
},
|
||||
{
|
||||
name: "count",
|
||||
position: 1,
|
||||
required: false,
|
||||
value_type: { kind: "integer" },
|
||||
completion: { kind: "none" },
|
||||
description: "Number of examples",
|
||||
},
|
||||
{
|
||||
name: "enabled",
|
||||
position: 2,
|
||||
required: false,
|
||||
value_type: { kind: "boolean" },
|
||||
completion: { kind: "static", values: ["true", "false"] },
|
||||
description: "Enable review",
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
position: 3,
|
||||
required: false,
|
||||
value_type: { kind: "worker_file" },
|
||||
completion: { kind: "worker_file" },
|
||||
description: "Worker document",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
identity: "fixture.attach",
|
||||
name: "attach",
|
||||
aliases: [],
|
||||
display_name: "Attach file",
|
||||
description: "Stage a client file using the attachment adapter.",
|
||||
syntax: "parenthesized",
|
||||
arguments: [{
|
||||
name: "file",
|
||||
position: 0,
|
||||
required: true,
|
||||
value_type: { kind: "client_file" },
|
||||
completion: { kind: "client_file" },
|
||||
}],
|
||||
client_adapter: "attachment",
|
||||
},
|
||||
];
|
||||
const entries = [
|
||||
{
|
||||
kind: "message",
|
||||
entry_id: "user-1",
|
||||
timestamp: 1,
|
||||
provenance: "human_input",
|
||||
role: "user",
|
||||
content: [{ kind: "text", text: "Review the release notes before sending." }],
|
||||
},
|
||||
{
|
||||
kind: "message",
|
||||
entry_id: "assistant-1",
|
||||
timestamp: 2,
|
||||
provenance: "model_output",
|
||||
role: "assistant",
|
||||
content: [{
|
||||
kind: "text",
|
||||
text:
|
||||
"Ready. Choose a Feature and inspect its arguments, or attach a document. Nothing runs until Submit.",
|
||||
}],
|
||||
},
|
||||
];
|
||||
const session = {
|
||||
pending_submissions: { revision: 0, notification_count: 0, head_id: null, submissions: [] },
|
||||
entries,
|
||||
};
|
||||
const state = { last_command_id: 0, state: { kind: "idle" } };
|
||||
const worker = {
|
||||
worker_id: workerId,
|
||||
runtime_id: runtimeId,
|
||||
resource_key: "W-901",
|
||||
display_name: "Console Live Fixture",
|
||||
label: "Console Live Fixture",
|
||||
availability: "observed",
|
||||
state: "idle",
|
||||
worker_state: state,
|
||||
retention_state: "active",
|
||||
host_id: "fixture-host",
|
||||
profile: "default",
|
||||
tags: [],
|
||||
diagnostics: [],
|
||||
workspace: { workspace_id: workspaceId, visibility: "workspace", identity: workspaceId },
|
||||
implementation: { kind: "builtin", display_hint: "fixture" },
|
||||
workdir_attachments: [],
|
||||
};
|
||||
const snapshot = {
|
||||
event: "snapshot",
|
||||
data: {
|
||||
session,
|
||||
state,
|
||||
greeting: {
|
||||
worker_name: worker.label,
|
||||
cwd: "/synthetic/workdir",
|
||||
provider: "fixture",
|
||||
model: "synthetic",
|
||||
scope_summary: "synthetic read-only files",
|
||||
tools: [],
|
||||
context_window: 200000,
|
||||
context_tokens: 2400,
|
||||
},
|
||||
},
|
||||
};
|
||||
const methods: unknown[] = [];
|
||||
const requests: { method: string; path: string }[] = [];
|
||||
const events: unknown[] = [];
|
||||
let submitMode: "accept" | "reject" | "hold" = "accept";
|
||||
let uploadMode: "normal" | "fail" | "hold" = "normal";
|
||||
const pendingAcceptances: (() => void)[] = [];
|
||||
const cancelledUploads = new Set<string>();
|
||||
const heldUploads = new Map<string, (response: Response) => void>();
|
||||
const releaseHeldUploads = new Map<string, () => void>();
|
||||
const grants = new Map<string, { file_name: string; media_type: string }>();
|
||||
const uploads: string[] = [];
|
||||
const uploadAttempts: string[] = [];
|
||||
const deletes: string[] = [];
|
||||
let historyRequests = 0;
|
||||
let nextSubscription = 0;
|
||||
const mime: Record<string, string> = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".json": "application/json",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".woff2": "font/woff2",
|
||||
".wasm": "application/wasm",
|
||||
};
|
||||
async function staticResponse(pathname: string): Promise<Response> {
|
||||
const relative = normalize(pathname.replace(/^\/+/, "") || "index.html");
|
||||
if (relative.startsWith("..")) return new Response("not found", { status: 404 });
|
||||
let path = join(buildRoot, relative);
|
||||
try {
|
||||
if ((await Deno.stat(path)).isDirectory) path = join(path, "index.html");
|
||||
return new Response(await Deno.readFile(path), {
|
||||
headers: { "content-type": mime[extname(path)] ?? "application/octet-stream" },
|
||||
});
|
||||
} catch {
|
||||
return new Response(await Deno.readFile(join(buildRoot, "index.html")), {
|
||||
headers: { "content-type": mime[".html"] },
|
||||
});
|
||||
}
|
||||
}
|
||||
Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname === "/health") return new Response("ok");
|
||||
if (url.pathname === "/fixture-state") {
|
||||
return json({
|
||||
methods,
|
||||
events,
|
||||
requests,
|
||||
uploads,
|
||||
upload_attempts: uploadAttempts,
|
||||
deletes,
|
||||
held_uploads: [...heldUploads.keys()],
|
||||
history_requests: historyRequests,
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/fixture-control" && request.method === "POST") {
|
||||
const control = await request.json();
|
||||
if (["accept", "reject", "hold"].includes(control.submit)) submitMode = control.submit;
|
||||
if (["normal", "fail", "hold"].includes(control.upload)) uploadMode = control.upload;
|
||||
if (control.accept_pending) { for (const accept of pendingAcceptances.splice(0)) accept(); }
|
||||
if (control.release_uploads) {
|
||||
for (const release of [...releaseHeldUploads.values()]) release();
|
||||
}
|
||||
return json({ submitMode, uploadMode });
|
||||
}
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
requests.push({ method: request.method, path: url.pathname });
|
||||
}
|
||||
if (url.pathname === "/api/workspaces") {
|
||||
return json([{
|
||||
workspace_id: workspaceId,
|
||||
owner_account_id: "owner",
|
||||
display_name: "Console Live Review",
|
||||
state: "active",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-02T00:00:00Z",
|
||||
}]);
|
||||
}
|
||||
if (url.pathname === `/api/w/${workspaceId}/workspace`) {
|
||||
return json({
|
||||
workspace_id: workspaceId,
|
||||
display_name: "Console Live Review",
|
||||
record_authority: "fixture",
|
||||
schema_version: 1,
|
||||
auth: {
|
||||
Passkey: {
|
||||
rp_id: "127.0.0.1",
|
||||
origin: url.origin,
|
||||
public_base_url: url.origin,
|
||||
cookie_name: "fixture",
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
manage_repositories: false,
|
||||
manage_secrets: false,
|
||||
manage_runtimes: false,
|
||||
delete_workspace: false,
|
||||
},
|
||||
extension_points: {
|
||||
store: "fixture",
|
||||
event_stream: { status: "ready", note: "fixture", diagnostics: [] },
|
||||
host_worker_bridge: { status: "ready", note: "fixture", diagnostics: [] },
|
||||
companion_console: { status: "ready", note: "fixture", diagnostics: [] },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.pathname === `/api/w/${workspaceId}/repositories`) {
|
||||
return json({ workspace_id: workspaceId, items: [], source: "fixture", diagnostics: [] });
|
||||
}
|
||||
if (url.pathname === `/api/w/${workspaceId}/working-directories`) {
|
||||
return json({ workspace_id: workspaceId, items: [], diagnostics: [] });
|
||||
}
|
||||
if (url.pathname === `/api/w/${workspaceId}/workers/W-901` || url.pathname === workerPath) {
|
||||
return json(worker);
|
||||
}
|
||||
if (url.pathname === `${workerPath}/session`) return json({ availability: "live_protocol" });
|
||||
if (url.pathname === `${workerPath}/session/history`) {
|
||||
historyRequests++;
|
||||
return json({
|
||||
availability: "page",
|
||||
page: {
|
||||
session_id: "session-a",
|
||||
lineage_id: "lineage-a",
|
||||
compact_ancestor_lineage_ids: [],
|
||||
turns: [{ turn_id: "user-1", entries }],
|
||||
next_cursor: null,
|
||||
has_more: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.pathname === `${workerPath}/attachment-upload-grants` && request.method === "POST") {
|
||||
const id = url.searchParams.get("upload_id")!;
|
||||
grants.set(id, {
|
||||
file_name: url.searchParams.get("file_name")!,
|
||||
media_type: url.searchParams.get("media_type")!,
|
||||
});
|
||||
return json({ upload_id: id, expires_at_ms: Date.now() + 60000 });
|
||||
}
|
||||
if (url.pathname.startsWith(`${workerPath}/attachment-uploads/`)) {
|
||||
const id = url.pathname.split("/").at(-1)!;
|
||||
if (request.method === "DELETE") {
|
||||
deletes.push(id);
|
||||
grants.delete(id);
|
||||
cancelledUploads.add(id);
|
||||
heldUploads.get(id)?.(new Response(null, { status: 204 }));
|
||||
heldUploads.delete(id);
|
||||
releaseHeldUploads.delete(id);
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
const grant = grants.get(id);
|
||||
if (!grant || request.method !== "PUT") return json({ error: "unknown_upload" }, 404);
|
||||
uploadAttempts.push(id);
|
||||
if (uploadMode === "fail") {
|
||||
await request.body?.cancel();
|
||||
return json({ error: "synthetic_upload_failure" }, 503);
|
||||
}
|
||||
const bytes = await request.arrayBuffer();
|
||||
const hash = Array.from(
|
||||
new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)),
|
||||
(byte) => byte.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
const complete = () => {
|
||||
uploads.push(id);
|
||||
return json({
|
||||
file: {
|
||||
artifact_id: id,
|
||||
...grant,
|
||||
created_at_ms: Date.now(),
|
||||
availability: "available",
|
||||
byte_len: bytes.byteLength,
|
||||
sha256: hash,
|
||||
},
|
||||
});
|
||||
};
|
||||
if (uploadMode === "hold") {
|
||||
if (cancelledUploads.has(id)) return new Response(null, { status: 204 });
|
||||
return await new Promise<Response>((resolve) => {
|
||||
heldUploads.set(id, resolve);
|
||||
releaseHeldUploads.set(id, () => {
|
||||
heldUploads.delete(id);
|
||||
releaseHeldUploads.delete(id);
|
||||
resolve(complete());
|
||||
});
|
||||
});
|
||||
}
|
||||
return complete();
|
||||
}
|
||||
if (url.pathname.startsWith(`${workerPath}/attachments/`) && request.method === "DELETE") {
|
||||
deletes.push(url.pathname.split("/").at(-1)!);
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
if (
|
||||
url.pathname === `/api/w/${workspaceId}/protocol/ws` &&
|
||||
request.headers.get("upgrade") === "websocket"
|
||||
) {
|
||||
const { socket, response } = Deno.upgradeWebSocket(request);
|
||||
const subscriptions = new Map<string, string>();
|
||||
const send = (frame: unknown) => socket.send(JSON.stringify(frame));
|
||||
socket.onmessage = (event) => {
|
||||
const frame = JSON.parse(String(event.data));
|
||||
if (frame.frame === "request" && frame.message.method === "subscribe_events") {
|
||||
const { request_id, selector } = frame.message.params;
|
||||
const subscription_id = `fixture-${++nextSubscription}`;
|
||||
subscriptions.set(subscription_id, selector.topic);
|
||||
send({
|
||||
protocol_version: 2,
|
||||
frame: "response",
|
||||
message: {
|
||||
result: "subscribed",
|
||||
payload: {
|
||||
request_id,
|
||||
subscription_id,
|
||||
selector,
|
||||
snapshot: selector.topic === "worker_protocol"
|
||||
? { topic: "worker_protocol", data: { worker_id: workerId, events: [snapshot] } }
|
||||
: { topic: "workers", data: { workers: [] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (frame.frame === "worker_protocol") {
|
||||
const method = frame.message.method;
|
||||
methods.push(method);
|
||||
const subscription_id = frame.message.subscription_id;
|
||||
if (subscriptions.get(subscription_id) !== "worker_protocol") return;
|
||||
if (method.method === "list_completions") {
|
||||
const { kind, prefix, request_id, context } = method.params;
|
||||
let candidates: unknown[] = [];
|
||||
if (kind === "feature") {
|
||||
candidates = invocations.filter((descriptor) =>
|
||||
descriptor.name.startsWith(prefix) ||
|
||||
descriptor.aliases.some((alias) => alias.startsWith(prefix))
|
||||
).map((invocation) => ({
|
||||
value: invocation.name,
|
||||
is_dir: false,
|
||||
description: invocation.description,
|
||||
invocation,
|
||||
}));
|
||||
} else if (kind === "file") {
|
||||
candidates = ["docs/", "docs/release-notes.md"].filter((value) =>
|
||||
value.startsWith(prefix)
|
||||
).map((value) => ({ value, is_dir: value.endsWith("/") }));
|
||||
} else if (context?.invocation === "fixture.review") {
|
||||
const descriptor = invocations[0].arguments.find((argument) =>
|
||||
argument.name === context.argument
|
||||
);
|
||||
const values = !context.argument
|
||||
? invocations[0].arguments.map((argument) => argument.name + "=")
|
||||
: descriptor?.completion.kind === "static"
|
||||
? descriptor.completion.values ?? []
|
||||
: context.argument === "path"
|
||||
? ["docs/release-notes.md", "docs/long-release-notes-for-responsive-validation.md"]
|
||||
: [];
|
||||
candidates = values.filter((value) => value.startsWith(prefix)).map((value) => ({
|
||||
value,
|
||||
is_dir: false,
|
||||
}));
|
||||
}
|
||||
send({
|
||||
protocol_version: 2,
|
||||
frame: "event",
|
||||
message: {
|
||||
event: "event",
|
||||
data: {
|
||||
subscription_id,
|
||||
payload: {
|
||||
event: "worker_protocol",
|
||||
data: {
|
||||
worker_id: workerId,
|
||||
event: {
|
||||
event: "completions",
|
||||
data: {
|
||||
kind,
|
||||
prefix,
|
||||
...(request_id ? { request_id } : {}),
|
||||
...(context ? { context } : {}),
|
||||
entries: candidates,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (method.method === "submit") {
|
||||
const emit = (event: unknown) => {
|
||||
events.push(event);
|
||||
if (socket.readyState !== WebSocket.OPEN) return;
|
||||
send({
|
||||
protocol_version: 2,
|
||||
frame: "event",
|
||||
message: {
|
||||
event: "event",
|
||||
data: {
|
||||
subscription_id,
|
||||
payload: { event: "worker_protocol", data: { worker_id: workerId, event } },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const submission_request_id = method.params.submission_request_id;
|
||||
const accept = () =>
|
||||
emit({
|
||||
event: "submission_accepted",
|
||||
data: {
|
||||
submission_request_id,
|
||||
submission_id: `accepted-${submission_request_id}`,
|
||||
disposition: "started",
|
||||
},
|
||||
});
|
||||
if (submitMode === "reject") {
|
||||
emit({
|
||||
event: "submission_rejected",
|
||||
data: {
|
||||
submission_request_id,
|
||||
message: "Synthetic admission rejection; draft was not accepted.",
|
||||
},
|
||||
});
|
||||
} else if (submitMode === "hold") pendingAcceptances.push(accept);
|
||||
else accept();
|
||||
}
|
||||
// Admission responses are synthetic authority only. Never execute a Feature or Worker.
|
||||
}
|
||||
};
|
||||
return response;
|
||||
}
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return json({ error: "unhandled_fixture_request", path: url.pathname }, 404);
|
||||
}
|
||||
return await staticResponse(url.pathname);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/// <reference lib="dom" />
|
||||
import type { Page } from "playwright";
|
||||
|
||||
// Playwright's listener subscription enables Chromium interception asynchronously.
|
||||
// Keep it enabled for the Page lifetime and await the browser acknowledgement
|
||||
// before any real gesture; otherwise keyboard.press can overtake interception.
|
||||
export async function prepareNativePicker(page: Page): Promise<void> {
|
||||
page.on("filechooser", () => {});
|
||||
const session = await page.context().newCDPSession(page);
|
||||
await session.send("Page.setInterceptFileChooserDialog", { enabled: true });
|
||||
}
|
||||
|
||||
// Observe native controls without substituting them or synthesizing activation.
|
||||
export async function observePicker(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
const records: unknown[] = [];
|
||||
Object.assign(window, { __pickerDiagnostics: records });
|
||||
const record = (kind: string, element: Element | null, extra = {}) =>
|
||||
records.push({
|
||||
kind,
|
||||
time: performance.now(),
|
||||
active: navigator.userActivation.isActive,
|
||||
hasBeenActive: navigator.userActivation.hasBeenActive,
|
||||
connected: element?.isConnected,
|
||||
tag: element?.tagName,
|
||||
className: element?.className,
|
||||
editor: document.querySelector(".cm-content")?.textContent,
|
||||
focused: document.activeElement?.className,
|
||||
...extra,
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (["Enter", "Tab", "Escape"].includes(event.key)) {
|
||||
record("keydown", event.target as Element, { key: event.key, trusted: event.isTrusted });
|
||||
queueMicrotask(() => record("after-keydown", event.target as Element, { key: event.key }));
|
||||
}
|
||||
}, true);
|
||||
const nativeClick = HTMLInputElement.prototype.click;
|
||||
HTMLInputElement.prototype.click = function () {
|
||||
if (this.type === "file") record("input.click", this, { disabled: this.disabled });
|
||||
nativeClick.call(this);
|
||||
if (this.type === "file") record("after-input.click", this);
|
||||
};
|
||||
document.addEventListener("click", (event) => {
|
||||
if ((event.target as HTMLInputElement)?.type === "file") {
|
||||
record("input-click-event", event.target as Element, { trusted: event.isTrusted });
|
||||
}
|
||||
}, true);
|
||||
const observer = new MutationObserver((changes) => {
|
||||
for (const change of changes) {
|
||||
for (
|
||||
const [kind, nodes] of [
|
||||
["added", change.addedNodes],
|
||||
["removed", change.removedNodes],
|
||||
] as const
|
||||
) {
|
||||
for (const node of Array.from(nodes)) {
|
||||
if (
|
||||
node instanceof Element && (node.matches("input[type=file], .cm-editor") ||
|
||||
node.querySelector("input[type=file], .cm-editor"))
|
||||
) record(kind, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document, { childList: true, subtree: true });
|
||||
});
|
||||
}
|
||||
export async function pickerDiagnostics(page: Page): Promise<unknown> {
|
||||
return await page.evaluate(() =>
|
||||
(window as unknown as { __pickerDiagnostics: unknown }).__pickerDiagnostics
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/// <reference lib="dom" />
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { dirname, fromFileUrl, join } from "@std/path";
|
||||
import { chromium, type Page } from "playwright";
|
||||
import { Buffer } from "node:buffer";
|
||||
import {
|
||||
observePicker,
|
||||
pickerDiagnostics,
|
||||
prepareNativePicker,
|
||||
} from "./console_live_picker_diagnostics.ts";
|
||||
|
||||
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
|
||||
const route = "/w/console-live-review/workers/W-901-console-live-fixture/console";
|
||||
// Build outside this test. Allows isolated snapshots without writes to production sources.
|
||||
const buildRoot = Deno.env.get("WEB_UX_BUILD_ROOT") ?? join(root, "web/workspace/build");
|
||||
const outputRoot = Deno.env.get("WEB_UX_LIFECYCLE_OUTPUT") ??
|
||||
join(root, "target/web-ux/console-live-lifecycle");
|
||||
async function ready(url: string): Promise<void> {
|
||||
const deadline = Date.now() + 10000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
await response.body?.cancel();
|
||||
if (response.ok) return;
|
||||
} catch { /* owned server startup */ }
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
throw new Error("Owned live fixture did not start");
|
||||
}
|
||||
type Method = {
|
||||
method: string;
|
||||
params?: {
|
||||
input?: Array<{ kind: string; invocation?: { identity: string; arguments: unknown[] } }>;
|
||||
};
|
||||
};
|
||||
type FixtureState = { methods: Method[]; uploads: string[]; deletes: string[] };
|
||||
async function fixtureState(base: string): Promise<FixtureState> {
|
||||
return await (await fetch(`${base}/fixture-state`)).json();
|
||||
}
|
||||
async function replaceDraft(page: Page, text: string): Promise<void> {
|
||||
await page.locator(".cm-content").click();
|
||||
await page.keyboard.press("Control+a");
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.type(text);
|
||||
}
|
||||
async function chooseReview(page: Page): Promise<void> {
|
||||
await replaceDraft(page, "/rev");
|
||||
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
|
||||
await page.keyboard.press("Tab");
|
||||
}
|
||||
Deno.test("production live Console completes, edits/removes typed drafts and stages/cancels attachment adapter", async () => {
|
||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
const port = (listener.addr as Deno.NetAddr).port;
|
||||
listener.close();
|
||||
const base = `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_live_fixture_server.ts"),
|
||||
String(port),
|
||||
buildRoot,
|
||||
],
|
||||
stdout: "null",
|
||||
stderr: "inherit",
|
||||
}).spawn();
|
||||
await Deno.mkdir(outputRoot, { recursive: true });
|
||||
try {
|
||||
await ready(`${base}/health`);
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
for (const theme of ["light", "dark"] as const) {
|
||||
for (const width of [1440, 768, 390]) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width, height: width === 390 ? 844 : 900 },
|
||||
colorScheme: theme,
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await prepareNativePicker(page);
|
||||
await observePicker(page);
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors: string[] = [];
|
||||
const acknowledgedDeletes = new Set<string>();
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") errors.push(message.text());
|
||||
});
|
||||
page.on("pageerror", (error) => errors.push(String(error)));
|
||||
page.on(
|
||||
"requestfailed",
|
||||
(request) =>
|
||||
errors.push(
|
||||
`failed ${request.method()} ${request.url()} ${request.failure()?.errorText}`,
|
||||
),
|
||||
);
|
||||
page.on("response", (response) => {
|
||||
if (response.request().method() === "DELETE" && response.status() === 204) {
|
||||
acknowledgedDeletes.add(response.url());
|
||||
}
|
||||
if (response.status() >= 400) {
|
||||
errors.push(`HTTP ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
try {
|
||||
assertEquals((await page.goto(base + route))?.status(), 200);
|
||||
await page.locator(".cm-content[contenteditable=true]").waitFor();
|
||||
await page.getByText("Ready. Choose a Feature", { exact: false }).waitFor();
|
||||
const start = await fixtureState(base);
|
||||
await chooseReview(page);
|
||||
await page.getByRole("option").filter({ hasText: "brief" }).waitFor();
|
||||
await page.keyboard.type("m");
|
||||
await page.getByRole("option").filter({ hasText: "mode=" }).click();
|
||||
await page.getByRole("option").filter({ hasText: "brief" }).click();
|
||||
await page.keyboard.type(", count=2, enabled=");
|
||||
await page.getByRole("option").filter({ hasText: "true" }).click();
|
||||
await page.keyboard.type(', path="docs/');
|
||||
await page.getByRole("option").filter({ hasText: "docs/release-notes.md" }).click();
|
||||
await page.keyboard.type(")");
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-invocation.png`) });
|
||||
let current = await fixtureState(base);
|
||||
assertEquals(current.uploads, start.uploads, "Feature discovery must not stage files");
|
||||
assertEquals(
|
||||
current.methods.filter((method) => method.method !== "list_completions"),
|
||||
start.methods.filter((method) => method.method !== "list_completions"),
|
||||
"completion must not submit, notify or execute controls",
|
||||
);
|
||||
// Pointer chip edit opens readable syntax, keyboard editing stays unexecuted.
|
||||
await page.locator(".composer-typed-chip").click();
|
||||
await page.waitForFunction(() =>
|
||||
document.querySelector(".cm-content")?.textContent?.includes("count=2")
|
||||
);
|
||||
for (
|
||||
let index = 0;
|
||||
index < ', enabled=true, path="docs/release-notes.md"'.length;
|
||||
index++
|
||||
) {
|
||||
await page.keyboard.press("ArrowLeft");
|
||||
}
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.type("3");
|
||||
await page.waitForFunction(() =>
|
||||
document.querySelector(".cm-content")?.textContent?.includes("count=3")
|
||||
);
|
||||
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-editing.png`) });
|
||||
await page.keyboard.press("Escape");
|
||||
await page.keyboard.press("Control+End");
|
||||
await page.keyboard.type(" ");
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.press("Backspace");
|
||||
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
|
||||
await page.keyboard.press("Control+z");
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
// Cancel a native picker; no upload and no accidental invocation remains.
|
||||
await replaceDraft(page, "before /att");
|
||||
await page.getByRole("option").filter({ hasText: "attach" }).waitFor();
|
||||
const picker = page.waitForEvent("filechooser");
|
||||
await page.keyboard.press("Enter");
|
||||
await (await picker).setFiles([]);
|
||||
assertEquals((await fixtureState(base)).uploads, start.uploads);
|
||||
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
|
||||
assertEquals((await page.locator(".cm-content").innerText()).trim(), "before");
|
||||
await page.screenshot({
|
||||
path: join(outputRoot, `${theme}-${width}-picker-cancelled.png`),
|
||||
});
|
||||
// Same generic adapter, this time choose a synthetic local file.
|
||||
await page.keyboard.type("/att");
|
||||
await page.getByRole("option").filter({ hasText: "attach" }).waitFor();
|
||||
const uploadPicker = page.waitForEvent("filechooser");
|
||||
await page.keyboard.press("Enter");
|
||||
await (await uploadPicker).setFiles({
|
||||
name: "release-notes.md",
|
||||
mimeType: "text/markdown",
|
||||
buffer: Buffer.from("Synthetic release notes"),
|
||||
});
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-attachment.png`) });
|
||||
current = await fixtureState(base);
|
||||
assertEquals(current.uploads.length, start.uploads.length + 1);
|
||||
await page.keyboard.press("Control+End");
|
||||
await page.keyboard.press("Backspace");
|
||||
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
|
||||
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-deleted.png`) });
|
||||
// Staged resource is retained through undo, then released when draft is reset.
|
||||
await page.keyboard.press("Control+z");
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
await replaceDraft(page, "literal /review(mode=brief)");
|
||||
assertEquals(
|
||||
await page.locator(".composer-typed-chip").count(),
|
||||
0,
|
||||
"unselected slash prose stays literal",
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
// Explicit selection becomes the sole structured Feature payload on Submit.
|
||||
await chooseReview(page);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.keyboard.type('mode="thorough", count=3, enabled=false)');
|
||||
await page.locator(".composer-typed-chip").waitFor();
|
||||
const release = page.waitForResponse((response) =>
|
||||
response.request().method() === "DELETE" && response.url().includes("/attachments/")
|
||||
);
|
||||
await page.keyboard.press("Control+Enter");
|
||||
await page.waitForFunction(() => !document.querySelector(".composer-typed-chip"));
|
||||
const releasedResponse = await release;
|
||||
assertEquals(releasedResponse.status(), 204);
|
||||
current = await fixtureState(base);
|
||||
assert(
|
||||
current.deletes.includes(current.uploads.at(-1)!),
|
||||
"accepted draft boundary releases the abandoned uploaded atom",
|
||||
);
|
||||
const submissions = current.methods.slice(start.methods.length).filter((method) =>
|
||||
method.method === "submit"
|
||||
);
|
||||
assertEquals(submissions.length, 1);
|
||||
assertEquals(submissions[0].params?.input?.[0].kind, "feature_invoke");
|
||||
assertEquals(submissions[0].params?.input?.[0].invocation?.identity, "fixture.review");
|
||||
assertEquals(submissions[0].params?.input?.[0].invocation?.arguments, [
|
||||
{ name: "mode", value: { kind: "string", value: "thorough" } },
|
||||
{ name: "count", value: { kind: "integer", value: 3 } },
|
||||
{ name: "enabled", value: { kind: "boolean", value: false } },
|
||||
]);
|
||||
// Chromium may report ERR_ABORTED after receiving the empty 204 cleanup
|
||||
// response. Retain that diagnostic, but verify delivery by both response
|
||||
// status and server DELETE evidence instead of claiming a lost operation.
|
||||
assert(
|
||||
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
|
||||
"no page horizontal overflow",
|
||||
);
|
||||
const pickerEvidence = await pickerDiagnostics(page);
|
||||
// Snapshot diagnostics only after the final browser awaits; classification
|
||||
// and serialization must use the same immutable set of observed errors.
|
||||
const recordedErrors = [...errors];
|
||||
const acknowledgedDeleteDiagnostics = recordedErrors.filter((error) =>
|
||||
[...acknowledgedDeletes].some((url) =>
|
||||
error === `failed DELETE ${url} net::ERR_ABORTED`
|
||||
)
|
||||
);
|
||||
assertEquals(
|
||||
recordedErrors.filter((error) => !acknowledgedDeleteDiagnostics.includes(error)),
|
||||
[],
|
||||
);
|
||||
await Deno.writeTextFile(
|
||||
join(outputRoot, `${theme}-${width}-result.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
theme,
|
||||
width,
|
||||
errors: recordedErrors,
|
||||
acknowledgedDeleteDiagnostics,
|
||||
acknowledgedDeleteResponseStatus: releasedResponse.status(),
|
||||
picker: pickerEvidence,
|
||||
status: "pass",
|
||||
before: start,
|
||||
after: current,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-failure.png`) });
|
||||
await Deno.writeTextFile(
|
||||
join(outputRoot, `${theme}-${width}-failure.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
error: String(error),
|
||||
errors,
|
||||
body: await page.locator("body").innerText(),
|
||||
fixture: await fixtureState(base),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
} finally {
|
||||
server.kill("SIGTERM");
|
||||
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 browser-tests/console_pending_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 browser-tests/console_live_shell_test.ts browser-tests/console_live_admission_shell_test.ts"
|
||||
},
|
||||
"fmt": {
|
||||
"lineWidth": 100,
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "console-live-baseline-dark",
|
||||
"title": "Production Console Feature invocation draft lifecycle",
|
||||
"baseUrl": "http://127.0.0.1:15286",
|
||||
"colorScheme": "dark",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [
|
||||
{
|
||||
"id": "fixture",
|
||||
"label": "Live synthetic Worker",
|
||||
"auth": {
|
||||
"kind": "anonymous"
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"label": "desktop",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "narrow",
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "mobile",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "console",
|
||||
"label": "Live Console",
|
||||
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
|
||||
"goal": "Inspect the unchanged production Console shell and literal slash text before Feature completion existed.",
|
||||
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Idle live Console"
|
||||
},
|
||||
{
|
||||
"id": "slash-plain",
|
||||
"label": "Literal unselected slash text",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": ".cm-content"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "v"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "w"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "("
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "b"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "f"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": ")"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Escape"
|
||||
}
|
||||
],
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"processes": [
|
||||
{
|
||||
"id": "console-live-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"--allow-env=WEB_UX_BUILD_ROOT",
|
||||
"browser-tests/console_live_fixture_server.ts",
|
||||
"15286"
|
||||
],
|
||||
"env": {
|
||||
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
|
||||
},
|
||||
"readyUrl": "http://127.0.0.1:15286/health"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "console-live-baseline-light",
|
||||
"title": "Production Console Feature invocation draft lifecycle",
|
||||
"baseUrl": "http://127.0.0.1:15286",
|
||||
"colorScheme": "light",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [
|
||||
{
|
||||
"id": "fixture",
|
||||
"label": "Live synthetic Worker",
|
||||
"auth": {
|
||||
"kind": "anonymous"
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"label": "desktop",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "narrow",
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "mobile",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "console",
|
||||
"label": "Live Console",
|
||||
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
|
||||
"goal": "Inspect the unchanged production Console shell and literal slash text before Feature completion existed.",
|
||||
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Idle live Console"
|
||||
},
|
||||
{
|
||||
"id": "slash-plain",
|
||||
"label": "Literal unselected slash text",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": ".cm-content"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "v"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "w"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "("
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "b"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "f"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": ")"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Escape"
|
||||
}
|
||||
],
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"processes": [
|
||||
{
|
||||
"id": "console-live-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"--allow-env=WEB_UX_BUILD_ROOT",
|
||||
"browser-tests/console_live_fixture_server.ts",
|
||||
"15286"
|
||||
],
|
||||
"env": {
|
||||
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
|
||||
},
|
||||
"readyUrl": "http://127.0.0.1:15286/health"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "console-live-dark",
|
||||
"title": "Production Console Feature invocation draft lifecycle",
|
||||
"baseUrl": "http://127.0.0.1:15286",
|
||||
"colorScheme": "dark",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [
|
||||
{
|
||||
"id": "fixture",
|
||||
"label": "Live synthetic Worker",
|
||||
"auth": {
|
||||
"kind": "anonymous"
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"label": "desktop",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "narrow",
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "mobile",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "console",
|
||||
"label": "Live Console",
|
||||
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
|
||||
"goal": "Discover Feature metadata, complete typed arguments and inspect a draft chip without submitting or staging files.",
|
||||
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Idle live Console"
|
||||
},
|
||||
{
|
||||
"id": "slash-plain",
|
||||
"label": "Literal unselected slash text",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": ".cm-content"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "v"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "w"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "("
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "b"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "f"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": ")"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Escape"
|
||||
}
|
||||
],
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "feature-menu",
|
||||
"label": "Feature names",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": ".cm-content"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+Space"
|
||||
},
|
||||
{
|
||||
"action": "wait",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[role=option]"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "argument-menu",
|
||||
"label": "Argument values",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "v"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+Space"
|
||||
},
|
||||
{
|
||||
"action": "wait",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[role=option]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Tab"
|
||||
},
|
||||
{
|
||||
"action": "wait",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[role=option]"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "typed-chip",
|
||||
"label": "Selected typed invocation",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+End"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "b"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "f"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": ")"
|
||||
}
|
||||
],
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".composer-typed-chip"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"processes": [
|
||||
{
|
||||
"id": "console-live-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"--allow-env=WEB_UX_BUILD_ROOT",
|
||||
"browser-tests/console_live_fixture_server.ts",
|
||||
"15286"
|
||||
],
|
||||
"env": {
|
||||
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
|
||||
},
|
||||
"readyUrl": "http://127.0.0.1:15286/health"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "console-live-light",
|
||||
"title": "Production Console Feature invocation draft lifecycle",
|
||||
"baseUrl": "http://127.0.0.1:15286",
|
||||
"colorScheme": "light",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [
|
||||
{
|
||||
"id": "fixture",
|
||||
"label": "Live synthetic Worker",
|
||||
"auth": {
|
||||
"kind": "anonymous"
|
||||
}
|
||||
}
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"label": "desktop",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "narrow",
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"label": "mobile",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "console",
|
||||
"label": "Live Console",
|
||||
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
|
||||
"goal": "Discover Feature metadata, complete typed arguments and inspect a draft chip without submitting or staging files.",
|
||||
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
},
|
||||
"capturePoints": [
|
||||
{
|
||||
"id": "initial",
|
||||
"label": "Idle live Console"
|
||||
},
|
||||
{
|
||||
"id": "slash-plain",
|
||||
"label": "Literal unselected slash text",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": ".cm-content"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "v"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "w"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "("
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "b"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "f"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": ")"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Escape"
|
||||
}
|
||||
],
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".cm-content[contenteditable=true]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "feature-menu",
|
||||
"label": "Feature names",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "click",
|
||||
"selector": ".cm-content"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+Space"
|
||||
},
|
||||
{
|
||||
"action": "wait",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[role=option]"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "argument-menu",
|
||||
"label": "Argument values",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+a"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Backspace"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "/"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "v"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+Space"
|
||||
},
|
||||
{
|
||||
"action": "wait",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[role=option]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Tab"
|
||||
},
|
||||
{
|
||||
"action": "wait",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[role=option]"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "typed-chip",
|
||||
"label": "Selected typed invocation",
|
||||
"interaction": [
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Escape"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "Control+End"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "b"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "r"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "i"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "e"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": "f"
|
||||
},
|
||||
{
|
||||
"action": "press",
|
||||
"selector": ".cm-content",
|
||||
"key": ")"
|
||||
}
|
||||
],
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": ".composer-typed-chip"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"processes": [
|
||||
{
|
||||
"id": "console-live-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"--allow-env=WEB_UX_BUILD_ROOT",
|
||||
"browser-tests/console_live_fixture_server.ts",
|
||||
"15286"
|
||||
],
|
||||
"env": {
|
||||
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
|
||||
},
|
||||
"readyUrl": "http://127.0.0.1:15286/health"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user