clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/184-task-notice-migration
lofi / package / testing / fixture_integration.ts
fixture_integration.ts 5.9K
1import {2 BrowserUnavailableError,3 createTwoClientFixture,4 type TwoClientFixture,5} from "./fixture.ts";6import { waitForReady } from "./readiness.ts";7import { readTraceArchive } from "./trace.ts";89function assert(condition: unknown, message: string): asserts condition {10 if (!condition) throw new Error(message);11}1213Deno.test("real Chromium fixture isolates or shares identity in memory and captures safe failures", async () => {14 const artifacts = await Deno.makeTempDir({ dir: "." });15 const server = Deno.serve(16 { hostname: "127.0.0.1", port: 0, onListen() {} },17 () =>18 new Response(19 `<!doctype html>20 <html><body data-ready="yes"><main>fixture</main></body></html>`,21 { headers: { "content-type": "text/html; charset=utf-8" } },22 ),23 );24 const address = server.addr as Deno.NetAddr;25 const baseURL = `http://127.0.0.1:${address.port}/`;26 let shared: TwoClientFixture | undefined;27 let isolated: TwoClientFixture | undefined;2829 try {30 try {31 shared = await createTwoClientFixture({32 baseURL,33 identity: {34 mode: "shared",35 async preparePrimary(client) {36 await client.page.evaluate(() => localStorage.setItem("identity", "shared-person"));37 },38 },39 artifacts: { directory: artifacts, secretValues: ["literal-secret"] },40 });41 } catch (error) {42 if (error instanceof BrowserUnavailableError) return;43 throw error;44 }4546 await Promise.all(47 shared.clients.map((client) =>48 waitForReady(49 client.page,50 (value) => document.body.dataset.ready === value,51 "yes",52 { description: `${client.name} boot` },53 )54 ),55 );56 const sharedIdentities = await Promise.all(57 shared.clients.map((client) => client.page.evaluate(() => localStorage.getItem("identity"))),58 );59 assert(60 sharedIdentities.join(",") === "shared-person,shared-person",61 `shared state was not cloned: ${sharedIdentities}`,62 );6364 await shared.first.reloadPage();65 assert(66 await shared.first.page.evaluate(() => localStorage.getItem("identity")) === "shared-person",67 "page reload lost identity",68 );6970 await shared.goOffline();71 await Promise.all(72 shared.clients.map((client) => client.page.waitForFunction(() => navigator.onLine === false)),73 );74 await shared.goOnline();75 await Promise.all(76 shared.clients.map((client) => client.page.waitForFunction(() => navigator.onLine === true)),77 );7879 await shared.first.restartPage();80 assert(81 await shared.first.page.evaluate(() => localStorage.getItem("identity")) === "shared-person",82 "page restart lost identity",83 );84 await shared.second.restartClient();85 assert(86 await shared.second.page.evaluate(() => localStorage.getItem("identity")) === "shared-person",87 "client restart lost in-memory identity",88 );8990 isolated = await createTwoClientFixture({91 baseURL,92 browser: shared.browser,93 identity: {94 mode: "isolated",95 async prepare(client) {96 await client.page.evaluate(97 (identity) => localStorage.setItem("identity", identity),98 `${client.name}-person`,99 );100 },101 },102 traceOnFailure: false,103 });104 const isolatedIdentities = await Promise.all(105 isolated.clients.map((client) =>106 client.page.evaluate(() => localStorage.getItem("identity"))107 ),108 );109 assert(110 isolatedIdentities.join(",") === "first-person,second-person",111 `isolated identities crossed: ${isolatedIdentities}`,112 );113 await isolated.close();114 isolated = undefined;115116 const consoleEvent = shared.first.page.waitForEvent("console", {117 predicate: (message) => message.type() === "error",118 });119 await shared.first.page.evaluate(() => console.error("token=literal-secret"));120 await consoleEvent;121 const pageError = shared.first.page.waitForEvent("pageerror");122 await shared.first.page.evaluate(() => {123 queueMicrotask(() => {124 throw new Error("secret=literal-secret");125 });126 });127 await pageError;128 const requestFailed = shared.first.page.waitForEvent("requestfailed");129 await shared.first.page.evaluate(() =>130 fetch("http://127.0.0.1:1/private?token=literal-secret").catch(() => undefined)131 );132 await requestFailed;133134 const captured = await shared.captureFailure(135 "integration failure",136 (client) => Promise.resolve({ online: !client.offline, itemCount: 0 }),137 );138 assert(captured, "failure artifacts were not created");139 assert(captured.files.some((path) => path.endsWith(".png")), "screenshots were omitted");140 assert(141 captured.files.filter((path) => path.endsWith(".trace.zip")).length === 2,142 "one trace per client was not retained",143 );144 assert(145 captured.files.every((path) => !path.toLowerCase().includes("storage")),146 `storage state leaked into artifact names: ${captured.files}`,147 );148 const diagnostics = await Deno.readTextFile(`${captured.directory}/diagnostics.json`);149 assert(!diagnostics.includes("literal-secret"), diagnostics);150 assert(diagnostics.includes("[redacted]"), diagnostics);151 for (const tracePath of captured.files.filter((path) => path.endsWith(".trace.zip"))) {152 const entries = await readTraceArchive(tracePath);153 for (const bytes of Object.values(entries)) {154 assert(155 !new TextDecoder().decode(bytes).includes("literal-secret"),156 `${tracePath} retained a secret`,157 );158 }159 }160161 const browser = shared.browser;162 await shared.close();163 shared = undefined;164 assert(!browser.isConnected(), "owned Chromium process remained connected after close");165 } finally {166 await isolated?.close().catch(() => undefined);167 await shared?.close().catch(() => undefined);168 await server.shutdown().catch(() => undefined);169 await Deno.remove(artifacts, { recursive: true });170 }171});