feat(workspace): add shared Drive browsing editing and Worker grants
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# T-724 browser fixture validation
|
||||
|
||||
These tests serve the real static production Web shell against a loopback, in-memory API fixture. They are **not actual Backend/DB/LocalFileSystem authorization or round-trip evidence**. The parent owns the separate real API checks.
|
||||
|
||||
## Runtime prerequisites
|
||||
|
||||
Deno and Playwright Chromium must be available. The server binds loopback only and owns/cleans up its shared Home fixture subprocess. Tests use an ephemeral port and independent fixture state; no real workspace data, credentials, git operations, environment test switches, or UI/CSS fixture branches are used.
|
||||
|
||||
The parent must confirm UI readiness before the production build and browser run. Tests intentionally never build automatically.
|
||||
|
||||
```sh
|
||||
# Narrow API-fixture compatibility checks (safe before UI readiness)
|
||||
deno test --config web/workspace/test/drive-browser/deno.json --allow-read web/workspace/test/drive-browser/fixture-api.test.ts
|
||||
|
||||
# After explicit parent UI-ready signal and successful production build only:
|
||||
deno test --config web/workspace/test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys web/workspace/test/drive-browser/drive.browser.test.ts
|
||||
|
||||
# Responsive audit, with optional frozen production site and artifact suffix:
|
||||
deno test --config web/workspace/test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys web/workspace/test/drive-browser/responsive.browser.test.ts -- web/workspace/.svelte-kit/t724/verified-build/site final-reviewed
|
||||
|
||||
# Official tools/web-ux bundles; no build is performed by this runner.
|
||||
deno run --config web/workspace/test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys web/workspace/test/drive-browser/capture-visual.ts before <unique-run-suffix>
|
||||
deno run --config web/workspace/test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys web/workspace/test/drive-browser/capture-visual.ts after <unique-run-suffix> [workdir-relative-production-site]
|
||||
```
|
||||
|
||||
## Fixture API
|
||||
|
||||
Start `tools/web-ux/drive-fixture/server.ts <port> <build-root>` with `--allow-net --allow-read --allow-run --allow-env`. It extends the shared Home/settings API fixture through a loopback proxy and implements Drive root/list/metadata/search/read-text/read-chunk/download/mutate/upload/request-status/grants APIs. Response wire types derive from generated Drive DTOs; current client parsers are exercised by the fixture compatibility tests.
|
||||
|
||||
- Root ID `9007199254740993` tests lossless decimal references.
|
||||
- `2`: folder 資料; `3`: Markdown README; `4`: text inside folder 2.
|
||||
- `5`: valid synthetic 1200×800 `landscape.png` (16,813 compressed bytes), generated as real binary PNG data by `tools/web-ux/drive-fixture/raster.ts`; `6`: empty text; `7`: undecodable PNG; `8`: SVG download-only; `9`: text exceeding 64 KiB; `10`: long Japanese name; `11`: valid 64 KiB NUL text whose escaped JSON exceeds 256 KiB but stays below the separate 512 KiB JSON bound.
|
||||
- `home-member` and `*readonly*`: reads succeed, mutations are typed HTTP 403. No metadata permission flag is invented. The UI reacts to a denied operation, not speculative permission inference.
|
||||
- `*denied*`: typed HTTP 403 root/read; `*offline*`/`home-error`: typed HTTP 503; `*empty*`: empty successful root list; `*paged*`: 240 additional nodes.
|
||||
- `POST /__fixture/fault` JSON `{workspace, mode}`: one-shot `conflict` or committed `unknown` mutation response; persistent `image_error` binary fetch failure.
|
||||
- `home-owner`/`home-member`/`home-long`: representative authoritative Worker catalogs; REST and subscription projections come from the same generated-shape records. `home-long` includes long display names and opaque Runtime/Worker IDs. `workers-error` intentionally fails catalog reads with HTTP 503.
|
||||
- `GET /__fixture/log`: observable API request IDs/methods/paths/node IDs/CAS revisions for identity and no-resend assertions.
|
||||
- `POST /__fixture/reset`: fresh in-memory state.
|
||||
|
||||
Request delay/loss tests intercept the API boundary with Playwright and synchronize on gates, not guessed sleeps. They let the fixture commit before dropping/holding the response, then verify receipt query without blind upload retry.
|
||||
|
||||
## Visual evidence
|
||||
|
||||
Preserved production baseline: `.svelte-kit/t724/before-build/{site,output,build.log}`. Official bundles live below `.svelte-kit/t724/visual/`. Each contains screenshots, console/request/page diagnostics, accessibility snapshots, screenshot hashes, source revision/dirty context, and `review-context.json`.
|
||||
|
||||
Home/sidebar and Workers are captured under identical owner/member/error/empty/long conditions, light/dark at 1440/768/390/320. After captures add Drive owner/member/empty/error/denied/paged/Markdown/image/image-error/truncated surfaces, pinned rename/replace revision forms, and owner grant selection/read-only/read-write/revoked states. Expected injected 503/403 diagnostics and superseded breadcrumb metadata aborts remain in official context; `diagnostic-disposition.json` explicitly separates unexpected diagnostics. CLI `completed-with-errors` is not relabeled a visual pass.
|
||||
|
||||
The production shell scrolls `main` internally, so `fullPage` does not expand that scroll panel. Capture points use ordinary keyboard focus to show the actual form or the top denial state; no CSS/DOM screenshot override is applied. The responsive audit separately records document/main/table-region geometry, visible keyboard focus, and page errors across the full theme/viewport matrix. Raster checks wait for actual decoding, assert natural dimensions 1200×800, preserve the 3:2 aspect ratio, and constrain the rendered box by both the available width and 60vh height at all four widths/both themes.
|
||||
|
||||
Parent and reviewer must open images and inspect context before asserting visual acceptance. In particular, a successful capture command is not sufficient and HTTP 200 does not excuse visible UI errors.
|
||||
@@ -0,0 +1,113 @@
|
||||
/// <reference lib="dom" />
|
||||
// Captures the preserved production baseline; no UI mutations or Backend connection.
|
||||
import { chromium } from "playwright";
|
||||
import { dirname, fromFileUrl, join } from "@std/path";
|
||||
const root = join(dirname(fromFileUrl(import.meta.url)), "../../../..");
|
||||
const artifactRoot = join(root, "web/workspace/.svelte-kit/t724/before-build");
|
||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
const port = (listener.addr as Deno.NetAddr).port;
|
||||
listener.close();
|
||||
const server = new Deno.Command(Deno.execPath(), {
|
||||
args: [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
join(root, "tools/web-ux/browser-tests/workspace_home_fixture_server.ts"),
|
||||
String(port),
|
||||
join(artifactRoot, "site"),
|
||||
],
|
||||
stdout: "null",
|
||||
stderr: "piped",
|
||||
}).spawn();
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
const evidence: unknown[] = [];
|
||||
try {
|
||||
await Deno.mkdir(join(artifactRoot, "screenshots"), { recursive: true });
|
||||
const deadline = Date.now() + 10000;
|
||||
while (true) {
|
||||
try {
|
||||
const response = await fetch(`${origin}/health`);
|
||||
await response.body?.cancel();
|
||||
if (response.ok) break;
|
||||
} catch { /* startup */ }
|
||||
if (Date.now() > deadline) throw new Error("Home fixture did not start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
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();
|
||||
// Shared Home server predates the sidebar's authoritative /workers fetch.
|
||||
// Complete that API fixture only, without changing the production shell.
|
||||
await page.route("**/api/w/*/workers", async (route) => {
|
||||
const workspaceId = new URL(route.request().url()).pathname.split("/")[3];
|
||||
await route.fulfill({
|
||||
json: {
|
||||
workspace_id: workspaceId,
|
||||
items: [],
|
||||
limit: 100,
|
||||
source: "fixture",
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
for (const state of ["owner", "member", "error", "empty"]) {
|
||||
const errors: string[] = [];
|
||||
const record = (error: Error) => errors.push(String(error));
|
||||
page.on("pageerror", record);
|
||||
await page.goto(`${origin}/w/home-${state}`);
|
||||
if (state === "error") {
|
||||
await page.locator("main [role=alert]").first().waitFor();
|
||||
} else {for (
|
||||
const feed of ["home-attention", "home-active", "home-recent"]
|
||||
) await page.locator(`[data-feed-ready="${feed}"]`).waitFor();}
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
const filename = `home-sidebar-workers-${state}-${colorScheme}-${width}.png`;
|
||||
await page.screenshot({
|
||||
path: join(artifactRoot, "screenshots", filename),
|
||||
fullPage: true,
|
||||
});
|
||||
evidence.push({
|
||||
filename,
|
||||
state,
|
||||
colorScheme,
|
||||
width,
|
||||
title: await page.locator("main h1").allTextContents(),
|
||||
sidebar: await page.locator("aside").allTextContents(),
|
||||
errors,
|
||||
overflow: await page.evaluate(() =>
|
||||
document.documentElement.scrollWidth > innerWidth
|
||||
),
|
||||
});
|
||||
page.off("pageerror", record);
|
||||
}
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
await Deno.writeTextFile(
|
||||
join(artifactRoot, "capture.json"),
|
||||
JSON.stringify(
|
||||
{ authority: "fixture, not actual Backend", evidence },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
`Captured ${evidence.length} baseline screenshots at ${artifactRoot}/screenshots`,
|
||||
);
|
||||
} finally {
|
||||
server.kill("SIGTERM");
|
||||
await server.status;
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
// Official tools/web-ux capture bundles (console, request, accessibility, context).
|
||||
// Does not build. Before always uses isolated saved site; after requires explicit parent readiness.
|
||||
import { dirname, fromFileUrl, join } from "@std/path";
|
||||
import type { RouteScenario } from "../../../../tools/web-ux/src/types.ts";
|
||||
const root = join(dirname(fromFileUrl(import.meta.url)), "../../../..");
|
||||
const stage = Deno.args[0];
|
||||
if (stage !== "before" && stage !== "after") {
|
||||
throw new Error("usage: capture-visual.ts before|after");
|
||||
}
|
||||
const output = join(root, "web/workspace/.svelte-kit/t724/visual");
|
||||
await Deno.mkdir(output, { recursive: true });
|
||||
const runSuffix = Deno.args[1] ? `-${Deno.args[1]}` : "";
|
||||
for (const scheme of ["light", "dark"]) {
|
||||
const runId = `t724-${stage}-${scheme}${runSuffix}`;
|
||||
const routes = ["owner", "member", "empty", "error", "long"].map((
|
||||
state,
|
||||
): RouteScenario => ({
|
||||
id: `home-${state}`,
|
||||
label: `Home/sidebar Workers ${state}`,
|
||||
path: `/w/home-${state}`,
|
||||
goal:
|
||||
"Compare unchanged Home/sidebar/Workers composition under identical API fixture conditions.",
|
||||
dataState: state === "error"
|
||||
? "Intentional 503 for Home merge-requests; visible scoped error expected."
|
||||
: `Shared Home ${state}; representative authoritative Workers (empty for new workspace).`,
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector:
|
||||
".workspace-home:has([data-feed-ready='home-active']):has([data-feed-ready='home-recent'])",
|
||||
},
|
||||
capturePoints: [{
|
||||
id: "initial",
|
||||
label: "Home and Workers sidebar",
|
||||
fullPage: true,
|
||||
}],
|
||||
}));
|
||||
for (const state of ["owner", "member", "empty", "error", "long"]) {
|
||||
routes.push({
|
||||
id: `workers-${state}`,
|
||||
label: `Workers ${state}`,
|
||||
path: state === "error" ? "/w/workers-error/workers" : `/w/home-${state}/workers`,
|
||||
goal: "Compare the existing Worker management surface with identical authoritative catalogs.",
|
||||
dataState:
|
||||
`Shared Home ${state} identity; representative Worker list (empty or 503 when requested). Owner controls follow workspace permission projection.`,
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: state === "empty"
|
||||
? ".workers-page:has-text('No Workers are visible.')"
|
||||
: state === "error"
|
||||
? ".workers-page .section-state.error"
|
||||
: ".workers-page .workers-table tbody tr",
|
||||
},
|
||||
capturePoints: [{ id: "initial", label: "Workers", fullPage: true }],
|
||||
});
|
||||
}
|
||||
if (stage === "after") {
|
||||
routes.push({
|
||||
id: "drive-form-revision",
|
||||
label: "Pinned rename and replace form revisions",
|
||||
path: "/w/home-owner/drive/3",
|
||||
goal: "Show the form's locked read revision and explicit destination/file controls.",
|
||||
dataState:
|
||||
"Revision 1 read from fixture metadata; no mutation is sent by these capture interactions.",
|
||||
ready: { kind: "selector", selector: '.drive-page[data-drive-ready="true"]' },
|
||||
capturePoints: [
|
||||
{
|
||||
id: "rename",
|
||||
label: "Rename / move pinned revision",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{ action: "click", selector: '.drive-toolbar button:has-text("Rename / move")' },
|
||||
{
|
||||
action: "wait",
|
||||
ready: { kind: "selector", selector: '.drive-form:has-text("Expected revision 1.")' },
|
||||
},
|
||||
{ action: "press", selector: ".drive-form input:not([inputmode])", key: "Shift" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "replace",
|
||||
label: "Replace file pinned revision",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{ action: "click", selector: '.drive-form button:has-text("Cancel form")' },
|
||||
{ action: "click", selector: '.drive-toolbar button:has-text("Replace file")' },
|
||||
{
|
||||
action: "wait",
|
||||
ready: { kind: "selector", selector: '.drive-form:has-text("Expected revision 1.")' },
|
||||
},
|
||||
{ action: "press", selector: ".drive-form input[type=file]", key: "Shift" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
routes.push({
|
||||
id: "worker-grants-owner",
|
||||
label: "Owner Worker Drive grant lifecycle",
|
||||
path: "/w/home-owner/workers",
|
||||
goal:
|
||||
"Use the authoritative Worker catalog to grant read-only/read-write access and revoke it without conflating Profile enablement.",
|
||||
dataState:
|
||||
"Representative REST catalog and matching protocol snapshot; in-memory grant API, not actual Backend.",
|
||||
ready: { kind: "selector", selector: ".drive-grants select" },
|
||||
capturePoints: [
|
||||
{
|
||||
id: "selected",
|
||||
label: "Selected Worker without grants",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{
|
||||
action: "press",
|
||||
selector: ".drive-grants .worker-select select",
|
||||
key: "ArrowDown",
|
||||
},
|
||||
{
|
||||
action: "press",
|
||||
selector: ".drive-grants .worker-select select",
|
||||
key: "Enter",
|
||||
},
|
||||
{
|
||||
action: "wait",
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: '.drive-grants button:has-text("Add Drive grant")',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "read-only",
|
||||
label: "Read-only grant",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{
|
||||
action: "click",
|
||||
selector: '.drive-grants button:has-text("Add Drive grant")',
|
||||
},
|
||||
{ action: "click", selector: ".drive-grants button[type=submit]" },
|
||||
{
|
||||
action: "wait",
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: '.drive-grants strong:has-text("Read only")',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "read-write",
|
||||
label: "Read-write grant after explicit revoke",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{
|
||||
action: "click",
|
||||
selector: '.drive-grants button[aria-label^="Revoke Drive grant"]',
|
||||
},
|
||||
{
|
||||
action: "wait",
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: '.drive-grants:has-text("No active Drive grants for this Worker.")',
|
||||
},
|
||||
},
|
||||
{
|
||||
action: "click",
|
||||
selector: '.drive-grants button:has-text("Add Drive grant")',
|
||||
},
|
||||
{
|
||||
action: "press",
|
||||
selector: ".drive-grants form select",
|
||||
key: "ArrowDown",
|
||||
},
|
||||
{
|
||||
action: "press",
|
||||
selector: ".drive-grants form select",
|
||||
key: "Enter",
|
||||
},
|
||||
{ action: "click", selector: ".drive-grants button[type=submit]" },
|
||||
{
|
||||
action: "wait",
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: '.drive-grants strong:has-text("Read and write")',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "revoked",
|
||||
label: "Grant revoked",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{
|
||||
action: "click",
|
||||
selector: '.drive-grants button[aria-label^="Revoke Drive grant"]',
|
||||
},
|
||||
{
|
||||
action: "wait",
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: '.drive-grants:has-text("No active Drive grants for this Worker.")',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
for (
|
||||
const [state, workspace, nodeId] of [
|
||||
["owner", "home-owner", ""],
|
||||
["member", "home-member", ""],
|
||||
["empty", "home-empty", ""],
|
||||
["error", "home-error", ""],
|
||||
["denied", "drive-denied", ""],
|
||||
["paged", "drive-paged", ""],
|
||||
["markdown", "home-owner", "/3"],
|
||||
["image", "home-owner", "/5"],
|
||||
["image-error", "home-owner", "/7"],
|
||||
["truncated", "home-owner", "/9"],
|
||||
]
|
||||
) {
|
||||
routes.push({
|
||||
id: `drive-${state}`,
|
||||
label: `Drive ${state}`,
|
||||
path: `/w/${workspace}/drive${nodeId}`,
|
||||
goal:
|
||||
"Review real production Drive layout, bounded safe preview and distinct failure states.",
|
||||
dataState: `In-memory Drive API ${state}, explicitly NOT actual Backend.`,
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: state === "image-error"
|
||||
? "main .drive-page [role=alert]"
|
||||
: 'main [data-drive-ready="true"]',
|
||||
},
|
||||
capturePoints: state === "member"
|
||||
? [
|
||||
{
|
||||
id: "initial",
|
||||
label: "Member before authority denial",
|
||||
fullPage: true,
|
||||
},
|
||||
{
|
||||
id: "denied-write",
|
||||
label: "Member after typed write denial",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{
|
||||
action: "click",
|
||||
selector: '.drive-page button:has-text("New folder")',
|
||||
},
|
||||
{
|
||||
action: "fill",
|
||||
selector: ".drive-form input:not([type=file])",
|
||||
value: "member-denied-folder",
|
||||
},
|
||||
{
|
||||
action: "click",
|
||||
selector: ".drive-form button[type=submit]",
|
||||
},
|
||||
{
|
||||
action: "wait",
|
||||
ready: {
|
||||
kind: "selector",
|
||||
selector: '.drive-page:has-text("Read only / access denied by Backend")',
|
||||
},
|
||||
},
|
||||
{
|
||||
action: "press",
|
||||
selector: ".drive-heading button",
|
||||
key: "Shift",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [{ id: "initial", label: `Drive ${state}`, fullPage: true }],
|
||||
});
|
||||
}
|
||||
}
|
||||
const port = scheme === "light" ? 17241 : 17242;
|
||||
const scenario = {
|
||||
schemaVersion: 1,
|
||||
id: runId,
|
||||
title: `T-724 fixture-only ${stage} ${scheme}`,
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
colorScheme: scheme,
|
||||
reducedMotion: "reduce",
|
||||
personas: [{
|
||||
id: "fixture",
|
||||
label: "Fixture account; Backend not connected",
|
||||
auth: { kind: "anonymous" },
|
||||
}],
|
||||
viewports: [1440, 768, 390, 320].map((width) => ({
|
||||
label: String(width),
|
||||
width,
|
||||
height: 900,
|
||||
})),
|
||||
routes,
|
||||
processes: [{
|
||||
id: "drive-fixture",
|
||||
command: Deno.execPath(),
|
||||
args: [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"--allow-run",
|
||||
"--allow-env",
|
||||
join(root, "tools/web-ux/drive-fixture/server.ts"),
|
||||
String(port),
|
||||
Deno.args[2]
|
||||
? join(root, Deno.args[2])
|
||||
: stage === "before"
|
||||
? join(root, "web/workspace/.svelte-kit/t724/before-build/site")
|
||||
: join(root, "web/workspace/build"),
|
||||
],
|
||||
readyUrl: `http://127.0.0.1:${port}/health`,
|
||||
}],
|
||||
};
|
||||
const path = join(output, `${stage}-${scheme}.json`);
|
||||
await Deno.writeTextFile(path, JSON.stringify(scenario, null, 2));
|
||||
const command = new Deno.Command(Deno.execPath(), {
|
||||
cwd: root,
|
||||
args: [
|
||||
"run",
|
||||
"--config",
|
||||
"tools/web-ux/deno.json",
|
||||
"--allow-env",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"--allow-write",
|
||||
"--allow-run",
|
||||
"--allow-sys",
|
||||
"tools/web-ux/cli.ts",
|
||||
"capture",
|
||||
"--scenario",
|
||||
path,
|
||||
"--output",
|
||||
output,
|
||||
"--run-id",
|
||||
runId,
|
||||
],
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
const result = await command.output();
|
||||
if (!result.success && result.code !== 2) {
|
||||
throw new Error(
|
||||
`Official ${stage}/${scheme} capture failed (${result.code}); inspect review-context.`,
|
||||
);
|
||||
}
|
||||
// CLI exit 2 retains intentional HTTP error-state diagnostics rather than hiding them.
|
||||
// Parent must inspect the contexts; report unexpected errors separately.
|
||||
const context = JSON.parse(
|
||||
await Deno.readTextFile(join(output, runId, "review-context.json")),
|
||||
);
|
||||
const unexpected = context.captures.flatMap((
|
||||
capture: {
|
||||
route: { id: string };
|
||||
errors: { kind: string; message: string; url?: string }[];
|
||||
},
|
||||
) =>
|
||||
capture.errors.filter((error) => {
|
||||
// Selection/read-text republishing entry metadata supersedes the in-flight
|
||||
// breadcrumb lookup. Its abort is intentional and must not become a UI error.
|
||||
if (
|
||||
[
|
||||
"drive-markdown",
|
||||
"drive-form-revision",
|
||||
"drive-image",
|
||||
"drive-image-error",
|
||||
"drive-truncated",
|
||||
].includes(capture.route.id) && error.kind === "request" &&
|
||||
error.message === "net::ERR_ABORTED" &&
|
||||
error.url?.includes("/home-owner/drive/metadata?")
|
||||
) return false;
|
||||
if (
|
||||
["drive-member", "drive-denied"].includes(capture.route.id) &&
|
||||
error.kind === "document" &&
|
||||
error.message ===
|
||||
"visible UI error: Drive request failed: denied. No empty content or success is inferred from this failure."
|
||||
) return false;
|
||||
if (
|
||||
capture.route.id === "drive-error" && error.kind === "document" &&
|
||||
error.message ===
|
||||
"visible UI error: Drive request failed: storage_unavailable. No empty content or success is inferred from this failure."
|
||||
) return false;
|
||||
if (capture.route.id === "home-error") {
|
||||
return !(error.kind === "console" && error.message.includes("503")) &&
|
||||
!error.url?.includes("/home-error/merge-requests?");
|
||||
}
|
||||
if (capture.route.id === "workers-error") {
|
||||
return !(error.kind === "console" && error.message.includes("503")) &&
|
||||
!(error.url?.includes("/workers-error/workers") &&
|
||||
/HTTP 503|net::ERR_ABORTED/.test(error.message));
|
||||
}
|
||||
if (capture.route.id === "drive-member") {
|
||||
return !(error.kind === "console" && error.message.includes("403")) &&
|
||||
!(error.url?.includes("/home-member/drive/mutate") &&
|
||||
/HTTP 403|net::ERR_ABORTED/.test(error.message));
|
||||
}
|
||||
if (
|
||||
capture.route.id === "drive-error" ||
|
||||
capture.route.id === "drive-denied"
|
||||
) {
|
||||
return !(error.kind === "console" && /403|503/.test(error.message)) &&
|
||||
!error.url?.includes("/drive/");
|
||||
}
|
||||
return true;
|
||||
}).map((error) => ({ route: capture.route.id, ...error }))
|
||||
);
|
||||
await Deno.writeTextFile(
|
||||
join(output, runId, "diagnostic-disposition.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
expected:
|
||||
"Home reviews/Worker catalog error: injected 503. Drive denied/member/error: injected typed 403/503 and their precise visible failure messages. File metadata ERR_ABORTED is a superseded breadcrumb read, fenced by the UI epoch with no visible breadcrumb failure. All retained in review-context; no diagnostics are removed from official evidence.",
|
||||
unexpected,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
if (unexpected.length) {
|
||||
throw new Error(
|
||||
`${unexpected.length} unexpected visual diagnostics; inspect disposition.json`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"nodeModulesDir": "auto",
|
||||
"imports": {
|
||||
"#lib/": "../../src/lib/",
|
||||
"@std/assert": "jsr:@std/assert@1.0.19",
|
||||
"@std/path": "jsr:@std/path@1.1.4",
|
||||
"playwright": "npm:playwright@1.59.1"
|
||||
},
|
||||
"lock": false,
|
||||
"tasks": {
|
||||
"test:fixture": "deno test --allow-read fixture-api.test.ts",
|
||||
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys drive.browser.test.ts",
|
||||
"test:responsive": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys responsive.browser.test.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
/// <reference lib="dom" />
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { dirname, fromFileUrl, join } from "@std/path";
|
||||
import { chromium, type Page, type Route } from "playwright";
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { FixtureLog } from "../../../../tools/web-ux/drive-fixture/api.ts";
|
||||
const root = join(dirname(fromFileUrl(import.meta.url)), "../../../..");
|
||||
const ready = (page: Page) => page.locator('[data-drive-ready="true"]').waitFor();
|
||||
const button = (page: Page, name: string) => page.getByRole("button", { name, exact: true });
|
||||
const draft = (page: Page) => page.getByRole("textbox", { name: /^Draft \(expected revision/ });
|
||||
const preview = (page: Page) => page.getByRole("article", { name: "Current saved content" });
|
||||
function gate() {
|
||||
let release!: () => void;
|
||||
const promise = new Promise<void>((resolve) => release = resolve);
|
||||
return {
|
||||
release,
|
||||
get promise() {
|
||||
return (async () => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise<void>((_resolve, reject) => {
|
||||
timer = setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
"Expected API gate was not observed/released within 30 seconds",
|
||||
),
|
||||
),
|
||||
30000,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
})();
|
||||
},
|
||||
};
|
||||
}
|
||||
async function harness(
|
||||
run: (
|
||||
origin: string,
|
||||
browser: Awaited<ReturnType<typeof chromium.launch>>,
|
||||
) => Promise<void>,
|
||||
) {
|
||||
// Intentionally no build here. Parent must authorize and build production UI first.
|
||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
const port = (listener.addr as Deno.NetAddr).port;
|
||||
listener.close();
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
const server = new Deno.Command(Deno.execPath(), {
|
||||
args: [
|
||||
"run",
|
||||
"--allow-read",
|
||||
"--allow-net",
|
||||
"--allow-run",
|
||||
"--allow-env",
|
||||
join(root, "tools/web-ux/drive-fixture/server.ts"),
|
||||
String(port),
|
||||
join(root, "web/workspace/build"),
|
||||
],
|
||||
stdout: "null",
|
||||
stderr: "inherit",
|
||||
}).spawn();
|
||||
try {
|
||||
const deadline = Date.now() + 10000;
|
||||
while (true) {
|
||||
try {
|
||||
const response = await fetch(`${origin}/health`);
|
||||
await response.body?.cancel();
|
||||
if (response.ok) break;
|
||||
} catch { /* startup */ }
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error("Drive fixture failed startup");
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
}
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
await run(origin, browser);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
} finally {
|
||||
server.kill("SIGTERM");
|
||||
await server.status;
|
||||
}
|
||||
}
|
||||
async function editor(page: Page, text: string) {
|
||||
await ready(page);
|
||||
await button(page, "Edit text").click();
|
||||
await draft(page).fill(text);
|
||||
}
|
||||
async function rootFolder(page: Page) {
|
||||
await page.getByRole("navigation", { name: "Drive folders" }).getByRole(
|
||||
"link",
|
||||
{
|
||||
name: "Drive",
|
||||
exact: true,
|
||||
},
|
||||
).click();
|
||||
await page.getByRole("heading", { name: "Drive", exact: true }).waitFor();
|
||||
await ready(page);
|
||||
}
|
||||
async function otherFile(page: Page) {
|
||||
await rootFolder(page);
|
||||
await page.getByRole("link", { name: "資料", exact: true }).click();
|
||||
await ready(page);
|
||||
await page.getByRole("link", { name: "notes.txt", exact: true }).click();
|
||||
await ready(page);
|
||||
}
|
||||
async function fault(origin: string, workspace: string, mode: string) {
|
||||
const response = await fetch(`${origin}/__fixture/fault`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ workspace, mode }),
|
||||
});
|
||||
await response.body?.cancel();
|
||||
}
|
||||
async function logs(origin: string) {
|
||||
return await (await fetch(`${origin}/__fixture/log`)).json() as FixtureLog[];
|
||||
}
|
||||
async function safeFulfill(
|
||||
route: Route,
|
||||
response: Awaited<ReturnType<Route["fetch"]>>,
|
||||
) {
|
||||
try {
|
||||
await route.fulfill({ response });
|
||||
} catch { /* selection intentionally canceled this old request */ }
|
||||
}
|
||||
|
||||
Deno.test("production Drive two tabs retain losing CAS draft and never retry with a newer revision", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const context = await browser.newContext();
|
||||
const first = await context.newPage(), second = await context.newPage();
|
||||
const errors: string[] = [];
|
||||
first.on("pageerror", (e) => errors.push(String(e)));
|
||||
second.on("pageerror", (e) => errors.push(String(e)));
|
||||
try {
|
||||
await first.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await second.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await editor(first, "First actor wins");
|
||||
await editor(second, "Second actor unsaved draft");
|
||||
await button(first, "Save").click();
|
||||
await first.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
await button(second, "Save").click();
|
||||
await second.getByText("Revision conflict — your draft is retained.", {
|
||||
exact: true,
|
||||
})
|
||||
.waitFor();
|
||||
assertEquals(
|
||||
await draft(second).inputValue(),
|
||||
"Second actor unsaved draft",
|
||||
);
|
||||
assertEquals(await button(second, "Save").isDisabled(), true);
|
||||
await button(second, "Refresh").click();
|
||||
await ready(second);
|
||||
assertEquals(
|
||||
await draft(second).inputValue(),
|
||||
"Second actor unsaved draft",
|
||||
);
|
||||
assert(
|
||||
(await second.getByText("Draft (expected revision 1)", { exact: false })
|
||||
.count()) > 0,
|
||||
);
|
||||
assert(
|
||||
(await preview(second).textContent())!.includes("First actor wins"),
|
||||
);
|
||||
await otherFile(second);
|
||||
await editor(second, "Independent second-file draft");
|
||||
await rootFolder(second);
|
||||
await second.getByRole("link", { name: "README.md", exact: true })
|
||||
.click();
|
||||
await ready(second);
|
||||
await button(second, "Edit text").click();
|
||||
assertEquals(
|
||||
await draft(second).inputValue(),
|
||||
"Second actor unsaved draft",
|
||||
);
|
||||
const casWrites = (await logs(origin)).filter((x) => x.path === "/mutate");
|
||||
assertEquals(casWrites.map((x) => [x.nodeId, x.expectedRevision]), [[
|
||||
"3",
|
||||
"1",
|
||||
], ["3", "1"]]);
|
||||
assertEquals(errors, []);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Drive delayed old read cannot replace another file or workspace content", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const received = gate(), release = gate();
|
||||
let handled: Promise<void> = Promise.resolve();
|
||||
await page.route("**/drive/read-text?**", (route) => {
|
||||
if (new URL(route.request().url()).searchParams.get("id") !== "3") {
|
||||
return route.continue();
|
||||
}
|
||||
handled = (async () => {
|
||||
const response = await route.fetch();
|
||||
received.release();
|
||||
await release.promise;
|
||||
await safeFulfill(route, response);
|
||||
})();
|
||||
return handled;
|
||||
});
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await received.promise;
|
||||
await otherFile(page);
|
||||
await editor(page, "Only current file draft");
|
||||
release.release();
|
||||
await handled;
|
||||
assertEquals(await page.locator("main h1").textContent(), "notes.txt");
|
||||
assertEquals(await draft(page).inputValue(), "Only current file draft");
|
||||
await button(page, "Save").click();
|
||||
await page.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
const mutation = (await logs(origin)).filter((x) => x.path === "/mutate");
|
||||
assertEquals(
|
||||
mutation.map((x) => [x.workspace, x.nodeId, x.expectedRevision]),
|
||||
[[
|
||||
"home-owner",
|
||||
"4",
|
||||
"1",
|
||||
]],
|
||||
);
|
||||
// Full workspace navigation must likewise never resurrect the old workspace.
|
||||
await page.goto(`${origin}/w/home-member/drive/4`);
|
||||
await ready(page);
|
||||
assert((await preview(page).textContent())!.includes("Second document"));
|
||||
assertEquals(
|
||||
await page.locator("main .drive-url a").getAttribute("href"),
|
||||
"/w/home-member/drive/4",
|
||||
);
|
||||
} finally {
|
||||
release.release();
|
||||
await handled;
|
||||
await context.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Drive delayed old save and status preserve destination drafts and save identity", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const sent = gate(), release = gate();
|
||||
let handled = Promise.resolve();
|
||||
await page.route("**/drive/mutate", (route) => {
|
||||
handled = (async () => {
|
||||
const response = await route.fetch();
|
||||
sent.release();
|
||||
await release.promise;
|
||||
await safeFulfill(route, response);
|
||||
})();
|
||||
return handled;
|
||||
});
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await editor(page, "Old file transmitted draft");
|
||||
await button(page, "Save").click();
|
||||
await sent.promise;
|
||||
await otherFile(page);
|
||||
await editor(page, "Destination draft never rebased");
|
||||
release.release();
|
||||
await handled;
|
||||
await page.unroute("**/drive/mutate");
|
||||
assertEquals(
|
||||
await draft(page).inputValue(),
|
||||
"Destination draft never rebased",
|
||||
);
|
||||
await button(page, "Save").click();
|
||||
await page.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
await rootFolder(page);
|
||||
await page.getByRole("link", { name: "README.md", exact: true }).click();
|
||||
await ready(page);
|
||||
await page.getByRole("button", { name: "Check request result" }).click();
|
||||
await page.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
assert(
|
||||
(await preview(page).textContent())!.includes(
|
||||
"Old file transmitted draft",
|
||||
),
|
||||
);
|
||||
await editor(page, "Another old request draft");
|
||||
await fault(origin, "home-owner", "unknown");
|
||||
await button(page, "Save").click();
|
||||
await button(page, "Check request result").waitFor();
|
||||
const statusSent = gate(), statusRelease = gate();
|
||||
let statusHandled = Promise.resolve();
|
||||
await page.route("**/drive/requests/*", (route) => {
|
||||
statusHandled = (async () => {
|
||||
const response = await route.fetch();
|
||||
statusSent.release();
|
||||
await statusRelease.promise;
|
||||
await safeFulfill(route, response);
|
||||
})();
|
||||
return statusHandled;
|
||||
});
|
||||
await button(page, "Check request result").click();
|
||||
await statusSent.promise;
|
||||
await otherFile(page);
|
||||
await editor(page, "Destination draft during old status");
|
||||
statusRelease.release();
|
||||
await statusHandled;
|
||||
assertEquals(
|
||||
await draft(page).inputValue(),
|
||||
"Destination draft during old status",
|
||||
);
|
||||
assert(
|
||||
(await preview(page).textContent())!.includes(
|
||||
"Destination draft never rebased",
|
||||
),
|
||||
);
|
||||
await button(page, "Save").click();
|
||||
await page.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
const pinnedWrites = (await logs(origin)).filter((x) => x.path === "/mutate");
|
||||
assertEquals(pinnedWrites.map((x) => [x.nodeId, x.expectedRevision]), [
|
||||
["3", "1"],
|
||||
["4", "1"],
|
||||
["3", "2"],
|
||||
["4", "2"],
|
||||
]);
|
||||
} finally {
|
||||
release.release();
|
||||
await handled;
|
||||
await context.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Drive rename move delete recreate preserve stable identity and missing old URL", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const page = await browser.newPage();
|
||||
page.on("dialog", (dialog) => dialog.accept());
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await ready(page);
|
||||
await button(page, "Rename / move").click();
|
||||
await page.getByRole("textbox", { name: "Name", exact: true }).fill(
|
||||
"renamed.md",
|
||||
);
|
||||
await page.getByRole("textbox", { name: "Destination folder node ID" })
|
||||
.fill("2");
|
||||
await button(page, "Apply rename / move").click();
|
||||
await page.getByRole("heading", { name: "renamed.md", exact: true })
|
||||
.waitFor();
|
||||
assertEquals(new URL(page.url()).pathname, "/w/home-owner/drive/3");
|
||||
await page.getByRole("navigation", { name: "Drive folders" }).getByRole(
|
||||
"link",
|
||||
{
|
||||
name: "資料",
|
||||
},
|
||||
).click();
|
||||
await ready(page);
|
||||
await page.getByRole("link", { name: "renamed.md", exact: true }).click();
|
||||
await ready(page);
|
||||
await button(page, "Delete").click();
|
||||
await page.getByRole("alert").filter({ hasText: /not[_ ]found/i })
|
||||
.waitFor();
|
||||
assertEquals(new URL(page.url()).pathname, "/w/home-owner/drive/3");
|
||||
await rootFolder(page);
|
||||
await page.getByRole("link", { name: "資料", exact: true }).click();
|
||||
await ready(page);
|
||||
await button(page, "New Markdown").click();
|
||||
await page.getByRole("textbox", { name: "Name", exact: true }).fill(
|
||||
"renamed.md",
|
||||
);
|
||||
await page.getByRole("textbox", { name: "Markdown", exact: true }).fill(
|
||||
"Recreated identity",
|
||||
);
|
||||
await button(page, "Create").click();
|
||||
await page.getByRole("link", { name: "renamed.md", exact: true })
|
||||
.waitFor();
|
||||
const href = await page.getByRole("link", {
|
||||
name: "renamed.md",
|
||||
exact: true,
|
||||
}).getAttribute(
|
||||
"href",
|
||||
);
|
||||
assert(href !== "/w/home-owner/drive/3");
|
||||
await page.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await ready(page);
|
||||
await page.getByRole("alert").filter({ hasText: /not[_ ]found/i })
|
||||
.waitFor();
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Drive upload response loss and navigation cancellation query receipts without resend", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-owner/drive`);
|
||||
await ready(page);
|
||||
await page.route("**/drive/upload?**", async (route) => {
|
||||
await route.fetch();
|
||||
await route.abort("failed");
|
||||
});
|
||||
await button(page, "Upload").click();
|
||||
await page.getByLabel("File (maximum 16 MiB)").setInputFiles({
|
||||
name: "lost-response.txt",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from("Published despite response loss"),
|
||||
});
|
||||
await button(page, "Upload file").click();
|
||||
await button(page, "Check request result").waitFor();
|
||||
assertEquals(
|
||||
(await logs(origin)).filter((x) => x.path === "/upload").length,
|
||||
1,
|
||||
);
|
||||
await button(page, "Check request result").click();
|
||||
await page.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
await page.unroute("**/drive/upload?**");
|
||||
await button(page, "Refresh").click();
|
||||
await ready(page);
|
||||
await page.getByRole("link", { name: "lost-response.txt" }).waitFor();
|
||||
const transmitted = gate(), release = gate();
|
||||
let handled = Promise.resolve();
|
||||
await page.route("**/drive/upload?**", (route) => {
|
||||
handled = (async () => {
|
||||
const response = await route.fetch();
|
||||
transmitted.release();
|
||||
await release.promise;
|
||||
await safeFulfill(route, response);
|
||||
})();
|
||||
return handled;
|
||||
});
|
||||
await button(page, "Upload").click();
|
||||
await page.getByLabel("File (maximum 16 MiB)").setInputFiles({
|
||||
name: "canceled-response.txt",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from("Transmitted before navigation abort"),
|
||||
});
|
||||
await button(page, "Upload file").click();
|
||||
await transmitted.promise;
|
||||
await button(page, "Cancel transfer / check outcome").click();
|
||||
await button(page, "Check request result").waitFor();
|
||||
await page.getByRole("link", { name: "資料", exact: true }).click();
|
||||
await page.getByRole("heading", { name: "資料", exact: true }).waitFor();
|
||||
await ready(page);
|
||||
release.release();
|
||||
await handled;
|
||||
await rootFolder(page);
|
||||
await button(page, "Check request result").waitFor();
|
||||
assertEquals(
|
||||
(await logs(origin)).filter((x) => x.path === "/upload").length,
|
||||
2,
|
||||
);
|
||||
await button(page, "Check request result").click();
|
||||
await page.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
assertEquals(
|
||||
(await logs(origin)).filter((x) => x.path === "/upload").length,
|
||||
2,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("UPLOAD FAILURE UI", await page.locator("main").innerText());
|
||||
console.log("UPLOAD FAILURE REQUESTS", await logs(origin));
|
||||
await page.screenshot({
|
||||
path: join(root, "web/workspace/.svelte-kit/t724/upload-failure.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Drive workspace switch fences a delayed previous workspace read", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const sent = gate(), release = gate();
|
||||
let handled = Promise.resolve();
|
||||
await page.route("**/home-owner/drive/read-text?**", (route) => {
|
||||
handled = (async () => {
|
||||
const response = await route.fetch();
|
||||
sent.release();
|
||||
await release.promise;
|
||||
await safeFulfill(route, response);
|
||||
})();
|
||||
return handled;
|
||||
});
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await sent.promise;
|
||||
await page.getByRole("button", {
|
||||
name: "Workspace Home Review",
|
||||
exact: true,
|
||||
}).first()
|
||||
.click();
|
||||
await page.getByRole("menuitem", {
|
||||
name: "Shared Workspace",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.waitForURL("**/w/home-member");
|
||||
await page.locator("aside").getByRole("link", {
|
||||
name: "Drive",
|
||||
exact: true,
|
||||
}).click();
|
||||
await ready(page);
|
||||
await page.getByRole("link", { name: "資料", exact: true }).click();
|
||||
await ready(page);
|
||||
await page.getByRole("link", { name: "notes.txt", exact: true }).click();
|
||||
await ready(page);
|
||||
await editor(page, "Member-only current draft");
|
||||
release.release();
|
||||
await handled;
|
||||
assertEquals(new URL(page.url()).pathname, "/w/home-member/drive/4");
|
||||
assertEquals(await draft(page).inputValue(), "Member-only current draft");
|
||||
assert((await preview(page).textContent())!.includes("Second document"));
|
||||
assertEquals(
|
||||
(await logs(origin)).filter((x) => x.path === "/mutate").length,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
release.release();
|
||||
await handled;
|
||||
await context.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Drive denial empty pages truncation unsafe content and image failure remain distinct", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const page = await browser.newPage();
|
||||
const external: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (new URL(request.url()).hostname === "example.invalid") {
|
||||
external.push(request.url());
|
||||
}
|
||||
});
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-member/drive/3`);
|
||||
await editor(page, "Denied member draft");
|
||||
await button(page, "Save").click();
|
||||
await page.getByText(
|
||||
"Read only / access denied by Backend. Editing and upload are unavailable.",
|
||||
).waitFor();
|
||||
assertEquals(await draft(page).inputValue(), "Denied member draft");
|
||||
assertEquals(await button(page, "Save").count(), 0);
|
||||
assertEquals(await draft(page).getAttribute("readonly"), "");
|
||||
for (
|
||||
const [workspace, text] of [["drive-denied", "denied"], [
|
||||
"drive-offline",
|
||||
"unavailable",
|
||||
]]
|
||||
) {
|
||||
await page.goto(`${origin}/w/${workspace}/drive`);
|
||||
await ready(page);
|
||||
await page.getByRole("alert").filter({ hasText: new RegExp(text, "i") })
|
||||
.waitFor();
|
||||
assertEquals(
|
||||
await page.getByText("This folder is empty.", { exact: true })
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
await page.goto(`${origin}/w/drive-empty/drive`);
|
||||
await ready(page);
|
||||
await page.getByText("This folder is empty.", { exact: true }).waitFor();
|
||||
await page.goto(`${origin}/w/drive-paged/drive`);
|
||||
await ready(page);
|
||||
const firstCount = await page.locator(".drive-table tbody tr").count();
|
||||
await button(page, "Load more").click();
|
||||
await ready(page);
|
||||
assert(
|
||||
(await page.locator(".drive-table tbody tr").count()) > firstCount,
|
||||
);
|
||||
await page.goto(`${origin}/w/home-owner/drive/9`);
|
||||
await ready(page);
|
||||
await page.getByText("Preview truncated at 64 KiB.", { exact: false })
|
||||
.waitFor();
|
||||
assertEquals(await button(page, "Edit text").count(), 0);
|
||||
await page.goto(`${origin}/w/home-owner/drive/6`);
|
||||
await ready(page);
|
||||
await page.getByText("Empty file.", { exact: true }).waitFor();
|
||||
await page.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await ready(page);
|
||||
assertEquals(
|
||||
await page.locator('main a[href^="javascript:"]').count(),
|
||||
0,
|
||||
);
|
||||
assertEquals(await page.locator("main img[onerror]").count(), 0);
|
||||
assertEquals(external, []);
|
||||
const local = page.getByRole("link", {
|
||||
name: "Local folder",
|
||||
exact: true,
|
||||
});
|
||||
assertEquals(await local.getAttribute("href"), "/w/home-owner/drive/2");
|
||||
await local.click();
|
||||
await page.waitForURL("**/w/home-owner/drive/2");
|
||||
await page.getByRole("heading", { name: "資料", exact: true }).waitFor();
|
||||
await ready(page);
|
||||
assertEquals(new URL(page.url()).pathname, "/w/home-owner/drive/2");
|
||||
await page.goto(`${origin}/w/home-owner/drive/8`);
|
||||
await ready(page);
|
||||
await page.getByText("This file type is download-only.", { exact: false })
|
||||
.waitFor();
|
||||
assertEquals(
|
||||
await page.locator("main svg, main iframe, main object").count(),
|
||||
0,
|
||||
);
|
||||
await page.goto(`${origin}/w/home-owner/drive/5`);
|
||||
await ready(page);
|
||||
await page.waitForFunction(() => {
|
||||
const image = document.querySelector("main .drive-image");
|
||||
return image instanceof HTMLImageElement && image.complete && image.naturalWidth === 1200 &&
|
||||
image.naturalHeight === 800;
|
||||
});
|
||||
assertEquals(
|
||||
await page.getByRole("img", { name: "landscape.png", exact: true }).evaluate((image) =>
|
||||
image instanceof HTMLImageElement ? [image.naturalWidth, image.naturalHeight] : []
|
||||
),
|
||||
[1200, 800],
|
||||
);
|
||||
await page.goto(`${origin}/w/home-owner/drive/7`);
|
||||
await ready(page);
|
||||
await page.getByRole("alert").filter({
|
||||
hasText: "Image could not be decoded",
|
||||
}).waitFor();
|
||||
await fault(origin, "home-owner", "image_error");
|
||||
await page.goto(`${origin}/w/home-owner/drive/5`);
|
||||
await ready(page);
|
||||
await page.getByRole("alert").filter({ hasText: /unavailable/i })
|
||||
.waitFor();
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Workers owner grants use the authoritative catalog while members have no grant controls", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const page = await browser.newPage();
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(String(error)));
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-owner/workers`);
|
||||
await page.getByRole("combobox", { name: "Worker", exact: true })
|
||||
.selectOption({
|
||||
label: "Documentation Worker · fixture-runtime / fixture-worker-a",
|
||||
});
|
||||
await button(page, "Add Drive grant").waitFor();
|
||||
await button(page, "Add Drive grant").click();
|
||||
await button(page, "Create Drive grant").click();
|
||||
const grants = page.getByRole("region", { name: "Worker Drive grants" });
|
||||
await grants.getByText("Read only", { exact: true }).waitFor();
|
||||
await page.getByRole("button", { name: /^Revoke Drive grant/ }).click();
|
||||
await grants.getByText("No active Drive grants for this Worker.", {
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
await button(page, "Add Drive grant").click();
|
||||
await page.getByRole("combobox", { name: "Access", exact: true })
|
||||
.selectOption("read_write");
|
||||
await button(page, "Create Drive grant").click();
|
||||
await grants.getByText("Read and write", { exact: true }).waitFor();
|
||||
await page.getByRole("button", { name: /^Revoke Drive grant/ }).click();
|
||||
await grants.getByText("No active Drive grants for this Worker.", {
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
const mutations = (await logs(origin)).filter((event) =>
|
||||
event.path.startsWith("/grants") && event.method !== "GET"
|
||||
);
|
||||
assertEquals(
|
||||
mutations.map((event) => [event.workspace, event.method, event.path]),
|
||||
[
|
||||
["home-owner", "POST", "/grants"],
|
||||
["home-owner", "DELETE", "/grants/1"],
|
||||
["home-owner", "POST", "/grants"],
|
||||
["home-owner", "DELETE", "/grants/2"],
|
||||
],
|
||||
);
|
||||
await page.goto(`${origin}/w/home-member/workers`);
|
||||
await page.locator(".workers-table tbody tr").first().waitFor();
|
||||
assertEquals(
|
||||
await page.getByRole("region", { name: "Worker Drive grants" }).count(),
|
||||
0,
|
||||
);
|
||||
assertEquals(await button(page, "Add Drive grant").count(), 0);
|
||||
assertEquals(errors, []);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}));
|
||||
|
||||
Deno.test("production Drive Markdown safe image placeholders preserve each image alt label", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const page = await browser.newPage();
|
||||
try {
|
||||
await page.goto(`${origin}/w/home-owner/drive/3`);
|
||||
await ready(page);
|
||||
const labels = await page.locator(".drive-inline-image")
|
||||
.allTextContents();
|
||||
assert(
|
||||
labels.some((label) => label.includes("Image: External")),
|
||||
`External image alt label lost: ${JSON.stringify(labels)}`,
|
||||
);
|
||||
assert(
|
||||
labels.some((label) => label.includes("Image: Image")),
|
||||
`Local image alt label lost: ${JSON.stringify(labels)}`,
|
||||
);
|
||||
assertEquals(await preview(page).locator("img").count(), 0);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}));
|
||||
@@ -0,0 +1,140 @@
|
||||
import { assert, assertEquals, assertRejects } from "@std/assert";
|
||||
import { createDriveFixture, fixtureRootId } from "../../../../tools/web-ux/drive-fixture/api.ts";
|
||||
import { createDriveClient, DriveRequestError } from "../../src/lib/workspace/drive/api.ts";
|
||||
function setup() {
|
||||
const fixture = createDriveFixture();
|
||||
const fetchFixture: typeof fetch = async (input, init) => {
|
||||
const request = new Request(new URL(String(input), "http://127.0.0.1"), init);
|
||||
const response = await fixture.handler(request);
|
||||
if (!response) throw new Error(`Unimplemented fixture ${request.url}`);
|
||||
return response;
|
||||
};
|
||||
return { fixture, client: createDriveClient(fetchFixture), fetchFixture };
|
||||
}
|
||||
const ref = (workspace_id: string, node_id: string) => ({ workspace_id, node_id });
|
||||
Deno.test("browser fixture preserves decimal identity, bounded reads, binary and page cursors", async () => {
|
||||
const { client, fetchFixture } = setup();
|
||||
const root = await client.root("drive-paged");
|
||||
assertEquals(root.entry.node_id, fixtureRootId);
|
||||
const first = await client.list("drive-paged", root.entry, { limit: 100 });
|
||||
const second = await client.list("drive-paged", root.entry, {
|
||||
limit: 100,
|
||||
after: first.next_after,
|
||||
});
|
||||
assertEquals(first.entries.length, 100);
|
||||
assertEquals(second.entries.length, 100);
|
||||
assert(
|
||||
!second.entries.some((entry) =>
|
||||
first.entries.some((other) => other.entry.node_id === entry.entry.node_id)
|
||||
),
|
||||
);
|
||||
const text = await client.readText("drive-paged", ref("drive-paged", "9"));
|
||||
assertEquals(text.truncated, true);
|
||||
assert(new TextEncoder().encode(text.text).length <= 65536);
|
||||
const image = await client.metadata("drive-paged", ref("drive-paged", "5"));
|
||||
const imageBytes = await client.image("drive-paged", image);
|
||||
assertEquals(imageBytes.size, image.size);
|
||||
assert(imageBytes.size < 256 * 1024);
|
||||
const png = new Uint8Array(await imageBytes.arrayBuffer());
|
||||
const dimensions = new DataView(png.buffer);
|
||||
assertEquals([dimensions.getUint32(16), dimensions.getUint32(20)], [1200, 800]);
|
||||
const escapedWire = await fetchFixture(
|
||||
"/api/w/drive-paged/drive/read-text?entry_workspace_id=drive-paged&id=11&max_bytes=65536",
|
||||
);
|
||||
const escapedLength = (await escapedWire.arrayBuffer()).byteLength;
|
||||
assert(escapedLength > 256 * 1024 && escapedLength < 512 * 1024);
|
||||
const escaped = await client.readText("drive-paged", ref("drive-paged", "11"));
|
||||
assertEquals(escaped.text, "\u0000".repeat(65536));
|
||||
assertEquals(escaped.truncated, false);
|
||||
});
|
||||
Deno.test("browser fixture two revisions conflict without committing the losing request", async () => {
|
||||
const { client } = setup();
|
||||
const entry = await client.metadata("home-owner", ref("home-owner", "3"));
|
||||
await client.mutate("home-owner", "tab-one", {
|
||||
operation: "update_text",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
text: "first actor",
|
||||
content_type: "text/markdown",
|
||||
});
|
||||
const failure = await assertRejects(
|
||||
() =>
|
||||
client.mutate("home-owner", "tab-two", {
|
||||
operation: "update_text",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
text: "second actor draft",
|
||||
content_type: "text/markdown",
|
||||
}),
|
||||
DriveRequestError,
|
||||
);
|
||||
assertEquals((failure as DriveRequestError).code, "conflict");
|
||||
assertEquals((await client.status("home-owner", "tab-one")).state, "committed");
|
||||
assertEquals((await client.status("home-owner", "tab-two")).state, "uncommitted");
|
||||
assertEquals((await client.readText("home-owner", entry.entry)).text, "first actor");
|
||||
});
|
||||
Deno.test("browser fixture upload unknown is reconciled by receipt and denied writes never commit", async () => {
|
||||
const { client, fetchFixture } = setup();
|
||||
await fetchFixture("/__fixture/fault", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ workspace: "home-owner", mode: "unknown" }),
|
||||
});
|
||||
const root = await client.root("home-owner");
|
||||
const failure = await assertRejects(
|
||||
() =>
|
||||
client.upload("home-owner", "upload-one", new Blob(["upload bytes"]), {
|
||||
operation: "create",
|
||||
parent: root.entry,
|
||||
name: "upload.txt",
|
||||
content_type: "text/plain",
|
||||
}),
|
||||
DriveRequestError,
|
||||
);
|
||||
assertEquals((failure as DriveRequestError).code, "outcome_unknown");
|
||||
assertEquals((await client.status("home-owner", "upload-one")).state, "committed");
|
||||
const memberRoot = await client.root("home-member");
|
||||
const denied = await assertRejects(
|
||||
() =>
|
||||
client.mutate("home-member", "member-one", {
|
||||
operation: "create_folder",
|
||||
parent: memberRoot.entry,
|
||||
name: "denied",
|
||||
}),
|
||||
DriveRequestError,
|
||||
);
|
||||
assertEquals((denied as DriveRequestError).code, "denied");
|
||||
assertEquals((await client.status("home-member", "member-one")).state, "uncommitted");
|
||||
});
|
||||
Deno.test("browser fixture rename and move retain identity while delete and recreate do not reuse links", async () => {
|
||||
const { client } = setup();
|
||||
let entry = await client.metadata("home-owner", ref("home-owner", "3"));
|
||||
const latest = entry.latest_url;
|
||||
const moved = await client.mutate("home-owner", "move", {
|
||||
operation: "relocate",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
parent: ref("home-owner", "2"),
|
||||
name: "renamed.md",
|
||||
});
|
||||
entry = moved.entry!;
|
||||
assertEquals(entry.latest_url, latest);
|
||||
await client.mutate("home-owner", "delete", {
|
||||
operation: "delete",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
});
|
||||
const replacement = await client.mutate("home-owner", "recreate", {
|
||||
operation: "create_text",
|
||||
parent: ref("home-owner", "2"),
|
||||
name: "renamed.md",
|
||||
text: "new entity",
|
||||
content_type: "text/markdown",
|
||||
});
|
||||
assert(replacement.entry!.entry.node_id !== entry.entry.node_id);
|
||||
const missing = await assertRejects(
|
||||
() => client.metadata("home-owner", entry.entry),
|
||||
DriveRequestError,
|
||||
);
|
||||
assertEquals((missing as DriveRequestError).code, "not_found");
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/// <reference lib="dom" />
|
||||
// Responsive DOM geometry audit over the frozen production shell and fake API.
|
||||
// Read-only observation: no UI/CSS mutation or actual Backend connection.
|
||||
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)), "../../../..");
|
||||
const auditId = Deno.args[1] ?? "verified";
|
||||
Deno.test("production Drive and Workers constrain page overflow and expose keyboard grant controls at every viewport", async () => {
|
||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
const port = (listener.addr as Deno.NetAddr).port;
|
||||
listener.close();
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
const server = new Deno.Command(Deno.execPath(), {
|
||||
args: [
|
||||
"run",
|
||||
"--allow-read",
|
||||
"--allow-net",
|
||||
"--allow-run",
|
||||
"--allow-env",
|
||||
join(root, "tools/web-ux/drive-fixture/server.ts"),
|
||||
String(port),
|
||||
join(root, Deno.args[0] ?? "web/workspace/build"),
|
||||
],
|
||||
stdout: "null",
|
||||
stderr: "inherit",
|
||||
}).spawn();
|
||||
const evidence: unknown[] = [];
|
||||
try {
|
||||
const deadline = Date.now() + 10000;
|
||||
while (true) {
|
||||
try {
|
||||
const response = await fetch(`${origin}/health`);
|
||||
await response.body?.cancel();
|
||||
if (response.ok) break;
|
||||
} catch { /* startup */ }
|
||||
if (Date.now() > deadline) throw new Error("fixture failed startup");
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
}
|
||||
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 pageErrors: string[] = [];
|
||||
page.on("pageerror", (e) => pageErrors.push(String(e)));
|
||||
for (
|
||||
const [workspace, surface] of [
|
||||
["home-owner", "workers"],
|
||||
["home-member", "workers"],
|
||||
["home-empty", "workers"],
|
||||
["workers-error", "workers"],
|
||||
["home-long", "workers"],
|
||||
["home-owner", "drive"],
|
||||
["home-member", "drive"],
|
||||
["home-empty", "drive"],
|
||||
["home-error", "drive"],
|
||||
["drive-paged", "drive"],
|
||||
["home-owner", "drive/10"],
|
||||
["home-owner", "drive/5"],
|
||||
]
|
||||
) {
|
||||
await page.goto(`${origin}/w/${workspace}/${surface}`);
|
||||
if (surface.startsWith("drive")) {
|
||||
await page.locator('[data-drive-ready="true"]').waitFor();
|
||||
} else if (workspace === "workers-error") {
|
||||
await page.locator(".workers-page .section-state.error")
|
||||
.waitFor();
|
||||
} else if (workspace === "home-empty") {
|
||||
await page.getByText("No Workers are visible.", { exact: true })
|
||||
.waitFor();
|
||||
} else {await page.locator(".workers-table tbody tr").first()
|
||||
.waitFor();}
|
||||
if (surface === "drive/5") {
|
||||
await page.waitForFunction(() => {
|
||||
const image = document.querySelector("main .drive-image");
|
||||
return image instanceof HTMLImageElement && image.complete &&
|
||||
image.naturalWidth === 1200 && image.naturalHeight === 800;
|
||||
});
|
||||
}
|
||||
if (workspace === "home-owner" && surface === "workers") {
|
||||
await page.getByRole("combobox", {
|
||||
name: "Worker",
|
||||
exact: true,
|
||||
}).selectOption({
|
||||
label: "Documentation Worker · fixture-runtime / fixture-worker-a",
|
||||
});
|
||||
await page.getByRole("button", {
|
||||
name: "Add Drive grant",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByRole("combobox", {
|
||||
name: "Access",
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
await page.getByRole("button", {
|
||||
name: "Refresh grants",
|
||||
exact: true,
|
||||
}).press("Tab");
|
||||
const focus = await page.evaluate(() => ({
|
||||
tag: document.activeElement?.tagName,
|
||||
outline: document.activeElement
|
||||
? getComputedStyle(document.activeElement).outlineStyle
|
||||
: "none",
|
||||
}));
|
||||
assert(
|
||||
focus.tag === "BUTTON" && focus.outline !== "none",
|
||||
JSON.stringify(focus),
|
||||
);
|
||||
}
|
||||
if (workspace === "workers-error" && width === 320 && colorScheme === "dark") {
|
||||
await page.screenshot({
|
||||
path: join(root, `web/workspace/.svelte-kit/t724/workers-error-${auditId}.png`),
|
||||
fullPage: false,
|
||||
});
|
||||
}
|
||||
const geometry = await page.evaluate(() => {
|
||||
const main = document.querySelector("main")! as HTMLElement;
|
||||
const grants = main.querySelector(".drive-grants");
|
||||
const boundedTables = [
|
||||
...main.querySelectorAll(".table-wrap, .drive-table-region"),
|
||||
].map((el) => ({
|
||||
width: el.clientWidth,
|
||||
scrollWidth: el.scrollWidth,
|
||||
overflowX: getComputedStyle(el).overflowX,
|
||||
}));
|
||||
const raster = main.querySelector(".drive-image");
|
||||
const image = raster instanceof HTMLImageElement
|
||||
? {
|
||||
naturalWidth: raster.naturalWidth,
|
||||
naturalHeight: raster.naturalHeight,
|
||||
renderedWidth: raster.getBoundingClientRect().width,
|
||||
renderedHeight: raster.getBoundingClientRect().height,
|
||||
availableWidth: raster.parentElement!.getBoundingClientRect().width,
|
||||
heightLimit: innerHeight * 0.6,
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
image,
|
||||
pageWidth: document.documentElement.scrollWidth,
|
||||
viewportWidth: innerWidth,
|
||||
mainWidth: main.clientWidth,
|
||||
mainScrollWidth: main.scrollWidth,
|
||||
documentScrollY: scrollY,
|
||||
mainScrollTop: main.scrollTop,
|
||||
mainTop: main.getBoundingClientRect().top,
|
||||
firstHeadingTop: main.querySelector("h1")?.getBoundingClientRect().top ?? null,
|
||||
grantsWidth: grants?.getBoundingClientRect().width ?? null,
|
||||
boundedTables,
|
||||
};
|
||||
});
|
||||
evidence.push({
|
||||
colorScheme,
|
||||
width,
|
||||
workspace,
|
||||
surface,
|
||||
...geometry,
|
||||
});
|
||||
assert(
|
||||
geometry.pageWidth <= width,
|
||||
JSON.stringify(evidence.at(-1)),
|
||||
);
|
||||
assert(
|
||||
geometry.mainScrollWidth <= geometry.mainWidth + 1,
|
||||
JSON.stringify(evidence.at(-1)),
|
||||
);
|
||||
if (surface === "drive/5") {
|
||||
const image = geometry.image;
|
||||
assert(image !== null);
|
||||
assertEquals([image.naturalWidth, image.naturalHeight], [1200, 800]);
|
||||
assert(image.renderedWidth > 100 && image.renderedHeight > 100);
|
||||
assert(image.renderedWidth <= image.availableWidth + 1);
|
||||
assert(image.renderedHeight <= image.heightLimit + 1);
|
||||
assert(Math.abs(image.renderedWidth / image.renderedHeight - 1.5) < 0.02);
|
||||
}
|
||||
if (workspace === "home-member" && surface === "workers") {
|
||||
assertEquals(
|
||||
await page.getByRole("region", {
|
||||
name: "Worker Drive grants",
|
||||
}).count(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
assertEquals(pageErrors, []);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
} finally {
|
||||
await Deno.writeTextFile(
|
||||
join(root, `web/workspace/.svelte-kit/t724/responsive-${auditId}.json`),
|
||||
JSON.stringify(
|
||||
{ authority: "fixture, not actual Backend", evidence },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
server.kill("SIGTERM");
|
||||
await server.status;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user