123import { basename, dirname, join, resolve } from "node:path";4import { fileURLToPath, pathToFileURL } from "node:url";5import { type Browser, type BrowserContext, chromium, type Page } from "playwright";6import { deploy, startLocalJazzServer } from "jazz-tools/testing";7import { LOFI_VERSION } from "../package/version.ts";8import { waitForReady } from "../package/testing/readiness.ts";9import { redactDiagnosticText } from "../package/testing/safety.ts";10import { sanitizeTraceArchive } from "../package/testing/trace.ts";11import {12 type VirtualAuthenticatorCredential,13 withVirtualAuthenticator,14} from "../package/testing/webauthn.ts";1516type PackageSource = "local" | "registry";17type StageLog = { command?: string; stdout: string; stderr: string };1819const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");20const artifactsRoot = join(repositoryRoot, "test-results", "golden");21const privateValues = ["golden-online-task", "golden-offline-task", "golden-synced-task"];22const logs = new Map<string, StageLog>();23const timings = new Map<string, number>();24let activeStage = "setup";2526class GoldenStageError extends Error {27 override readonly name = "GoldenStageError";2829 constructor(readonly stage: string, cause: unknown) {30 super(31 `golden stage ${JSON.stringify(stage)} failed: ${32 cause instanceof Error ? cause.message : String(cause)33 }`,34 { cause },35 );36 }37}3839function assert(condition: unknown, message: string): asserts condition {40 if (!condition) throw new Error(message);41}4243function parseArgs(args: readonly string[]): { source: PackageSource; version: string } {44 let source: PackageSource = "local";45 let version = LOFI_VERSION;46 for (let index = 0; index < args.length; index++) {47 const argument = args[index];48 if (argument === "--source") {49 const value = args[++index];50 if (value !== "local" && value !== "registry") {51 throw new Error("--source must be local or registry");52 }53 source = value;54 } else if (argument === "--version") {55 version = args[++index] ?? "";56 if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {57 throw new Error("--version must be an exact published semantic version");58 }59 } else {60 throw new Error(`unknown golden-runner argument ${JSON.stringify(argument)}`);61 }62 }63 if (source === "local" && version !== LOFI_VERSION) {64 throw new Error(`local source is workspace version ${LOFI_VERSION}, not ${version}`);65 }66 return { source, version };67}6869async function stage<T>(name: string, action: () => Promise<T>): Promise<T> {70 activeStage = name;71 const started = performance.now();72 try {73 const value = await action();74 timings.set(name, Math.round(performance.now() - started));75 console.log(`golden ${name}: passed (${timings.get(name)}ms)`);76 return value;77 } catch (error) {78 throw error instanceof GoldenStageError ? error : new GoldenStageError(name, error);79 }80}8182async function runCommand(83 stageName: string,84 command: string,85 args: readonly string[],86 cwd: string,87 environment: Readonly<Record<string, string>> = {},88 expectSuccess = true,89): Promise<Deno.CommandOutput> {90 const output = await new Deno.Command(command, {91 args: [...args],92 cwd,93 env: { ...environment },94 stdout: "piped",95 stderr: "piped",96 }).output();97 const stdout = new TextDecoder().decode(output.stdout);98 const stderr = new TextDecoder().decode(output.stderr);99 logs.set(stageName, { command: [command, ...args].join(" "), stdout, stderr });100 if (output.success !== expectSuccess) {101 throw new Error(102 `${basename(command)} ${args.join(" ")} ${103 expectSuccess ? "exited unsuccessfully" : "unexpectedly succeeded"104 }\n${stderr || stdout}`,105 );106 }107 return output;108}109110function timeout<T>(promise: Promise<T>, milliseconds: number, description: string): Promise<T> {111 return new Promise((resolvePromise, reject) => {112 const timer = setTimeout(113 () => reject(new Error(`${description} timed out after ${milliseconds}ms`)),114 milliseconds,115 );116 promise.then(117 (value) => {118 clearTimeout(timer);119 resolvePromise(value);120 },121 (error) => {122 clearTimeout(timer);123 reject(error);124 },125 );126 });127}128129class ManagedProcess {130 readonly child: Deno.ChildProcess;131 readonly status: Promise<Deno.CommandStatus>;132 #stdout = "";133 #stderr = "";134 #listeners = new Set<() => void>();135136 constructor(137 readonly name: string,138 args: readonly string[],139 readonly cwd: string,140 environment: Readonly<Record<string, string>> = {},141 ) {142 this.child = new Deno.Command(Deno.execPath(), {143 args: [...args],144 cwd,145 env: { ...environment },146 stdout: "piped",147 stderr: "piped",148 }).spawn();149 this.status = this.child.status;150 void this.#consume(this.child.stdout, "stdout");151 void this.#consume(this.child.stderr, "stderr");152 }153154 get output(): StageLog {155 return { command: `deno (${this.name})`, stdout: this.#stdout, stderr: this.#stderr };156 }157158 async #consume(stream: ReadableStream<Uint8Array>, destination: "stdout" | "stderr") {159 const decoder = new TextDecoder();160 for await (const chunk of stream) {161 const text = decoder.decode(chunk, { stream: true });162 if (destination === "stdout") this.#stdout += text;163 else this.#stderr += text;164 for (const listener of this.#listeners) listener();165 }166 }167168 waitForOutput(pattern: RegExp, milliseconds = 30_000): Promise<void> {169 return timeout(170 new Promise<void>((resolveReady, reject) => {171 const inspect = () => {172 if (!pattern.test(`${this.#stdout}\n${this.#stderr}`)) return;173 this.#listeners.delete(inspect);174 resolveReady();175 };176 this.#listeners.add(inspect);177 inspect();178 void this.status.then((status) => {179 if (this.#listeners.delete(inspect)) {180 reject(181 new Error(182 `${this.name} exited with code ${status.code} before readiness\n${this.#stderr}`,183 ),184 );185 }186 });187 }),188 milliseconds,189 `${this.name} readiness`,190 );191 }192193 async stop(): Promise<void> {194 try {195 this.child.kill("SIGTERM");196 } catch (error) {197 if (error instanceof Deno.errors.NotFound) {198 logs.set(this.name, this.output);199 return;200 }201 throw error;202 }203 try {204 await timeout(this.status, 8_000, `${this.name} graceful shutdown`);205 } catch {206 try {207 this.child.kill("SIGKILL");208 } catch (error) {209 if (!(error instanceof Deno.errors.NotFound)) throw error;210 }211 await timeout(this.status, 5_000, `${this.name} forced termination`);212 } finally {213 logs.set(this.name, this.output);214 }215 }216}217218async function configureGoldenSync(projectRoot: string) {219 const server = await startLocalJazzServer({ allowLocalFirstAuth: true });220 const cacheBuster = `?golden=${Date.now()}`;221 const [{ app }, { default: permissions }] = await Promise.all([222 import(`${pathToFileURL(join(projectRoot, "src", "schema.ts")).href}${cacheBuster}`),223 import(`${pathToFileURL(join(projectRoot, "src", "permissions.ts")).href}${cacheBuster}`),224 ]);225 await deploy({226 appId: server.appId,227 serverUrl: server.url,228 adminSecret: server.adminSecret,229 schema: app,230 permissions,231 });232 return server;233}234235function allocatePort(): number {236 const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });237 const port = (listener.addr as Deno.NetAddr).port;238 listener.close();239 return port;240}241242function waitForListening(process: ManagedProcess, port: number): Promise<void> {243 return timeout(244 new Promise<void>((resolveReady, reject) => {245 let active = true;246 const attempt = async () => {247 if (!active) return;248 try {249 const connection = await Deno.connect({ hostname: "127.0.0.1", port });250 connection.close();251 active = false;252 resolveReady();253 } catch {254 if (active) setTimeout(() => void attempt(), 20);255 }256 };257 void process.status.then((status) => {258 if (!active) return;259 active = false;260 reject(261 new Error(`${process.name} exited with code ${status.code} before accepting connections`),262 );263 });264 void attempt();265 }),266 30_000,267 `${process.name} listening on allocated port`,268 );269}270271async function waitForRegistryVersion(version: string): Promise<void> {272 let lastError = "version not visible";273 for (let attempt = 1; attempt <= 12; attempt++) {274 try {275 const response = await fetch("https://jsr.io/@nzip/lofi/meta.json", { cache: "no-store" });276 if (!response.ok) throw new Error(`JSR metadata returned ${response.status}`);277 const metadata = await response.json() as { versions?: Record<string, unknown> };278 if (metadata.versions && version in metadata.versions) return;279 lastError = `@nzip/lofi@${version} is not in JSR metadata`;280 } catch (error) {281 lastError = error instanceof Error ? error.message : String(error);282 }283 if (attempt < 12) await new Promise((resolveDelay) => setTimeout(resolveDelay, 5_000));284 }285 throw new Error(`registry availability retry exhausted: ${lastError}`);286}287288async function generateProject(289 source: PackageSource,290 version: string,291 temporaryRoot: string,292): Promise<string> {293 if (source === "registry") await waitForRegistryVersion(version);294 const projectName = "golden-app";295 const args = source === "registry"296 ? [297 "run",298 "-A",299 "--minimum-dependency-age=0",300 `jsr:@nzip/lofi@${version}/create`,301 projectName,302 ]303 : ["run", "-A", join(repositoryRoot, "package", "create.ts"), projectName];304 const environment: Record<string, string> = source === "local"305 ? {306 LOFI_CREATE_DEVELOPMENT: "1",307 LOFI_CREATE_PACKAGE_PREFIX: `${pathToFileURL(join(repositoryRoot, "package")).href}/`,308 }309 : {};310 const output = await runCommand("create", Deno.execPath(), args, temporaryRoot, environment);311 const stdout = new TextDecoder().decode(output.stdout).replaceAll("\r\n", "\n");312 assert(313 stdout.includes("Next:\n cd golden-app\n deno task dev\n"),314 "generator did not print the exact next commands",315 );316 const projectRoot = join(temporaryRoot, projectName);317 const marker = join(projectRoot, "golden-do-not-overwrite.txt");318 await Deno.writeTextFile(marker, "preserved\n");319 const refusal = await runCommand(320 "create-refusal",321 Deno.execPath(),322 args,323 temporaryRoot,324 environment,325 false,326 );327 const refusalOutput = `${new TextDecoder().decode(refusal.stdout)}\n${328 new TextDecoder().decode(refusal.stderr)329 }`;330 assert(331 refusalOutput.includes("already exists and is not empty"),332 "generator refusal was not actionable",333 );334 assert(335 await Deno.readTextFile(marker) === "preserved\n",336 "generator overwrote a non-empty project",337 );338 await Deno.remove(marker);339 if (source === "registry") {340 const config = JSON.parse(await Deno.readTextFile(join(projectRoot, "deno.json"))) as {341 imports: Record<string, string>;342 };343 const packageEntrypoints = [344 ...new Set(345 Object.entries(config.imports)346 .filter(([name]) =>347 name === "@nzip/lofi" || name.startsWith("@nzip/lofi/") &&348 !name.endsWith("/")349 )350 .map(([, target]) => target),351 ),352 ];353 await runCommand(354 "registry-cache",355 Deno.execPath(),356 ["cache", "--minimum-dependency-age=0", ...packageEntrypoints],357 projectRoot,358 );359 }360 return projectRoot;361}362363async function filesUnder(root: string): Promise<string[]> {364 const files: string[] = [];365 async function visit(directory: string, prefix = "") {366 for await (const entry of Deno.readDir(directory)) {367 const relative = prefix ? `${prefix}/${entry.name}` : entry.name;368 if (entry.isDirectory) await visit(join(directory, entry.name), relative);369 else if (entry.isFile) files.push(relative);370 }371 }372 await visit(root);373 return files.sort();374}375376async function assertGeneratedBoundary(377 projectRoot: string,378 source: PackageSource,379 version: string,380) {381 for (const forbidden of ["src/_lofi", "public/sw.js", "astro.config.ts"]) {382 try {383 await Deno.stat(join(projectRoot, forbidden));384 throw new Error(`generated project contains forbidden ${forbidden}`);385 } catch (error) {386 if (!(error instanceof Deno.errors.NotFound)) throw error;387 }388 }389 const config = JSON.parse(await Deno.readTextFile(join(projectRoot, "deno.json"))) as {390 imports: Record<string, string>;391 };392 const lofiImports = Object.entries(config.imports).filter(([name]) =>393 name === "@nzip/lofi" || name.startsWith("@nzip/lofi/")394 );395 assert(lofiImports.length === 20, `expected 20 lofi imports, received ${lofiImports.length}`);396 const expected = source === "registry"397 ? `jsr:@nzip/lofi@${version}`398 : pathToFileURL(join(repositoryRoot, "package")).href;399 assert(400 lofiImports.every(([, target]) => target.startsWith(expected)),401 `generated imports do not resolve through exactly one ${source} package ${version}`,402 );403 for (404 const icon of [405 "public/apple-touch-icon.png",406 "public/icon-192.png",407 "public/icon-512.png",408 "public/icon-maskable-512.png",409 ]410 ) await Deno.stat(join(projectRoot, icon));411}412413function pngDimensions(bytes: Uint8Array): [number, number] {414 const signature = [137, 80, 78, 71, 13, 10, 26, 10];415 assert(signature.every((byte, index) => bytes[index] === byte), "icon is not PNG data");416 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);417 return [view.getUint32(16), view.getUint32(20)];418}419420async function assertProductionOutput(projectRoot: string) {421 const dist = join(projectRoot, "dist");422 const files = await filesUnder(dist);423 for (424 const required of [425 "index.html",426 "launch/index.html",427 "share/index.html",428 "manifest.webmanifest",429 "sw.js",430 "lofi-precache.json",431 "apple-touch-icon.png",432 "icon-192.png",433 "icon-512.png",434 "icon-maskable-512.png",435 "screenshot-narrow.png",436 "screenshot-wide.png",437 ]438 ) assert(files.includes(required), `production output is missing ${required}`);439440 const manifest = JSON.parse(await Deno.readTextFile(join(dist, "manifest.webmanifest"))) as {441 icons: Array<{ src: string; sizes: string; type: string; purpose: string }>;442 screenshots: Array<{443 src: string;444 sizes: string;445 type: string;446 form_factor: string;447 label: string;448 }>;449 share_target: {450 action: string;451 method: string;452 enctype: string;453 params: { title: string; text: string; url: string };454 };455 launch_handler: { client_mode: string[] };456 };457 assert(manifest.share_target.action === "./share/", "share target action drifted");458 assert(manifest.share_target.method === "GET", "share target method drifted");459 assert(460 manifest.launch_handler.client_mode.join(",") === "focus-existing,auto",461 "launch handler client mode drifted",462 );463 const expected = new Map([464 [465 "icon-192.png",466 { sizes: "192x192", type: "image/png", purpose: "any", dimensions: 192 } as const,467 ],468 [469 "icon-512.png",470 { sizes: "512x512", type: "image/png", purpose: "any", dimensions: 512 } as const,471 ],472 [473 "icon-maskable-512.png",474 { sizes: "512x512", type: "image/png", purpose: "maskable", dimensions: 512 } as const,475 ],476 [477 "icon-monochrome.svg",478 { sizes: "any", type: "image/svg+xml", purpose: "monochrome" } as const,479 ],480 ]);481 for (const icon of manifest.icons) {482 const file = basename(icon.src);483 const contract = expected.get(file);484 assert(contract, `manifest references unexpected ${file}`);485 assert(icon.type === contract.type, `${file} has incorrect manifest type`);486 assert(icon.sizes === contract.sizes, `${file} has incorrect manifest size`);487 assert(icon.purpose === contract.purpose, `${file} has incorrect manifest purpose`);488 await Deno.stat(join(dist, file));489 if ("dimensions" in contract) {490 const [width, height] = pngDimensions(await Deno.readFile(join(dist, file)));491 assert(492 width === contract.dimensions && height === contract.dimensions,493 `${file} dimensions are incorrect`,494 );495 }496 }497 assert(manifest.icons.length === expected.size, "manifest does not contain the exact icon set");498 const screenshots = new Map([499 ["narrow", ["screenshot-narrow.png", 540, 720] as const],500 ["wide", ["screenshot-wide.png", 1280, 720] as const],501 ]);502 for (const screenshot of manifest.screenshots) {503 const contract = screenshots.get(screenshot.form_factor);504 assert(contract, `manifest references unexpected ${screenshot.form_factor} screenshot`);505 const file = basename(screenshot.src);506 assert(file === contract[0], `${screenshot.form_factor} screenshot file drifted`);507 assert(screenshot.type === "image/png", `${file} has incorrect manifest type`);508 assert(screenshot.sizes === `${contract[1]}x${contract[2]}`, `${file} size drifted`);509 assert(screenshot.label.trim().length > 0, `${file} has no label`);510 const [width, height] = pngDimensions(await Deno.readFile(join(dist, file)));511 assert(width === contract[1] && height === contract[2], `${file} dimensions are incorrect`);512 }513 assert(manifest.screenshots.length === screenshots.size, "manifest screenshot set is incomplete");514 const [appleWidth, appleHeight] = pngDimensions(515 await Deno.readFile(join(dist, "apple-touch-icon.png")),516 );517 assert(appleWidth === 180 && appleHeight === 180, "iOS icon is not 180x180");518519 const precache = JSON.parse(520 await Deno.readTextFile(join(dist, "lofi-precache.json")),521 ) as string[];522 for (523 const shellAsset of [524 "./",525 "./manifest.webmanifest",526 "./apple-touch-icon.png",527 "./icon-192.png",528 "./icon-512.png",529 "./icon-maskable-512.png",530 ]531 ) assert(precache.includes(shellAsset), `precache is missing ${shellAsset}`);532 for (const screenshot of ["screenshot-narrow.png", "screenshot-wide.png"]) {533 assert(!precache.includes(`./${screenshot}`), `${screenshot} leaked into the required shell`);534 }535 const worker = await Deno.readTextFile(join(dist, "sw.js"));536 assert(537 !worker.includes("__LOFI_BUILD_REVISION__"),538 "service worker build revision was not replaced",539 );540 const html = await Deno.readTextFile(join(dist, "index.html"));541 assert(html.includes("apple-touch-icon.png"), "production shell omitted the iOS icon");542543 const forbidden = [544 "lofi-development-inspector",545 "__LOFI_INSPECTOR__",546 "__LOFI_TEST_PROBE__",547 "data-lofi-test-probe",548 ];549 for (const file of files) {550 if (!/\.(?:css|html|js|json|map|svg|webmanifest)$/.test(file)) continue;551 const contents = await Deno.readTextFile(join(dist, file));552 for (const marker of forbidden) {553 assert(!contents.includes(marker), `production output ${file} contains ${marker}`);554 }555 }556}557558async function captureFailure(559 error: unknown,560 projectRoot: string | undefined,561 page: Page | undefined,562 context: BrowserContext | undefined,563 browserDiagnostics: readonly string[],564) {565 const stageName = error instanceof GoldenStageError ? error.stage : activeStage;566 const directory = join(artifactsRoot, stageName.replaceAll(/[^a-z0-9._-]+/gi, "-"));567 await Deno.mkdir(directory, { recursive: true });568 const redact = (value: string) =>569 redactDiagnosticText(value, [570 ...privateValues,571 ...(projectRoot ? [projectRoot, dirname(projectRoot)] : []),572 ]);573 if (page && !page.isClosed()) {574 await page.screenshot({575 path: join(directory, "page.png"),576 fullPage: true,577 mask: [page.locator("#new-task"), page.locator(".task")],578 }).catch(() => undefined);579 }580 if (context) {581 const trace = join(directory, "browser.trace.zip");582 try {583 await context.tracing.stop({ path: trace });584 await sanitizeTraceArchive(trace, redact);585 } catch {586 await Deno.remove(trace).catch(() => undefined);587 }588 }589 for (const [name, log] of logs) {590 const content = [log.command ? `$ ${log.command}` : "", log.stdout, log.stderr]591 .filter(Boolean)592 .join("\n");593 await Deno.writeTextFile(join(directory, `${name}.log`), `${redact(content)}\n`);594 }595 await Deno.writeTextFile(596 join(directory, "browser.log"),597 `${browserDiagnostics.map(redact).join("\n")}\n`,598 );599 await Deno.writeTextFile(600 join(directory, "error.log"),601 `${redact(error instanceof Error ? error.stack ?? error.message : String(error))}\n`,602 );603 console.error(`golden failure artifacts: ${directory}`);604}605606const taskAppReady = () => {607 const status = document.querySelector('[role="status"]');608 return status !== null && /item\(s\)/.test(status.textContent ?? "");609};610611async function selectVirtualCredential(page: Page, credentialId: string): Promise<void> {612 await page.evaluate((encodedId) => {613 const normalized = encodedId.replaceAll("-", "+").replaceAll("_", "/");614 const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);615 const bytes = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));616 const credentials = navigator.credentials;617 const original = credentials.get.bind(credentials);618 Object.defineProperty(credentials, "preventSilentAccess", {619 configurable: true,620 value: () => Promise.resolve(),621 });622 Object.defineProperty(credentials, "get", {623 configurable: true,624 value: (options?: CredentialRequestOptions) => {625 if (!options?.publicKey) return original(options);626 return original({627 ...options,628 publicKey: {629 ...options.publicKey,630 allowCredentials: [{ id: bytes, type: "public-key", transports: ["internal"] }],631 },632 });633 },634 });635 }, credentialId);636}637638// Every golden-recipe installer patches the same authored manifest; one639// helper owns the read/patch/write cycle so the seven installers cannot640// drift in how they load or serialize it.641async function patchManifest(642 projectRoot: string,643 // deno-lint-ignore no-explicit-any644 patch: (manifest: any) => void,645): Promise<void> {646 const manifestPath = join(projectRoot, "public", "manifest.webmanifest");647 const manifest = JSON.parse(await Deno.readTextFile(manifestPath));648 patch(manifest);649 await Deno.writeTextFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);650}651652async function installWebShareRecipe(projectRoot: string) {653 await patchManifest(projectRoot, (manifest) => {654 manifest.share_target = {655 action: "./share/",656 method: "GET",657 enctype: "application/x-www-form-urlencoded",658 params: { title: "title", text: "text", url: "url" },659 };660 });661 await Deno.writeTextFile(662 join(projectRoot, "src", "pages", "share.astro"),663 `---664import ShareReceiver from "../islands/ShareReceiver.tsx";665import Shell from "../layouts/Shell.astro";666import "../styles/global.css";667---668669<Shell title="Review shared content">670 <ShareReceiver client:load />671</Shell>672`,673 );674 await Deno.writeTextFile(675 join(projectRoot, "src", "islands", "ShareReceiver.tsx"),676 `import { useEffect, useState } from "preact/hooks";677import {678 parseTextShareTarget,679 shareOrFallback,680 type TextShareTargetResult,681 type WebShareOutcome,682} from "@nzip/lofi/recipes/web-share";683684export default function ShareReceiver() {685 const [result, setResult] = useState<TextShareTargetResult>();686 const [confirmed, setConfirmed] = useState(false);687 const [outcome, setOutcome] = useState<WebShareOutcome>();688689 useEffect(() => setResult(parseTextShareTarget(location.search)), []);690 if (!result) return <p role="status">Opening shared draft…</p>;691 if (!result.ok) return <p role="alert">This shared content is not valid.</p>;692 const draft = result.draft;693 return (694 <section class="island" data-share-draft>695 <h1>Review shared content</h1>696 {draft.title && <p data-share-title>{draft.title}</p>}697 {draft.text && <p data-share-text>{draft.text}</p>}698 {draft.url && <a data-share-url href={draft.url}>{draft.url}</a>}699 <button type="button" onClick={() => setConfirmed(true)}>Add to app</button>700 {confirmed && <p role="status">Draft confirmed.</p>}701 <button702 type="button"703 onClick={() => void shareOrFallback(draft, {704 fallback: () => setOutcome("fallback"),705 }).then(setOutcome)}706 >707 Share this draft708 </button>709 {outcome && <output data-share-outcome>{outcome}</output>}710 </section>711 );712}713`,714 );715}716717async function installLaunchHandlerRecipe(projectRoot: string) {718 await patchManifest(projectRoot, (manifest) => {719 manifest.launch_handler = { client_mode: ["focus-existing", "auto"] };720 });721 await Deno.writeTextFile(722 join(projectRoot, "src", "pages", "launch.astro"),723 `---724import LaunchReceiver from "../islands/LaunchReceiver.tsx";725import Shell from "../layouts/Shell.astro";726import "../styles/global.css";727---728729<Shell title="Installed app launch">730 <LaunchReceiver client:load />731</Shell>732`,733 );734 await Deno.writeTextFile(735 join(projectRoot, "src", "islands", "LaunchReceiver.tsx"),736 `import { useEffect, useState } from "preact/hooks";737import {738 installInstalledAppLaunchConsumer,739 type InstalledAppLaunchIssue,740} from "@nzip/lofi/recipes/launch-handler";741742export default function LaunchReceiver() {743 const [supported, setSupported] = useState<boolean>();744 const [target, setTarget] = useState("");745 const [issue, setIssue] = useState<InstalledAppLaunchIssue>();746 useEffect(() => {747 const consumer = installInstalledAppLaunchConsumer({748 scope: new URL(import.meta.env.BASE_URL, location.origin),749 onLaunch: (next) => {750 setIssue(undefined);751 setTarget(next.url);752 },753 onRejected: (next) => {754 setTarget("");755 setIssue(next);756 },757 });758 setSupported(consumer.supported);759 return consumer.dispose;760 }, []);761 return (762 <section class="island">763 <h1>Installed app launch</h1>764 <output data-launch-support>{supported === undefined ? "checking" : supported ? "supported" : "unsupported"}</output>765 {target && <p data-launch-target>{target}</p>}766 {issue && <p role="alert">The requested app destination is not valid.</p>}767 </section>768 );769}770`,771 );772}773774async function installFileHandlerRecipe(projectRoot: string) {775 await patchManifest(projectRoot, (manifest) => {776 manifest.file_handlers = [{777 action: "./import/",778 accept: { "application/json": [".json"] },779 }];780 });781 await Deno.writeTextFile(782 join(projectRoot, "src", "pages", "import.astro"),783 `---784import FileImport from "../islands/FileImport.tsx";785import Shell from "../layouts/Shell.astro";786import "../styles/global.css";787---788789<Shell title="Import field notes">790 <FileImport client:load />791</Shell>792`,793 );794 await Deno.writeTextFile(795 join(projectRoot, "src", "islands", "FileImport.tsx"),796 `import { useEffect, useState } from "preact/hooks";797import {798 installFileLaunchConsumer,799 prepareFileImportDrafts,800 type FileImportDraft,801 type FileImportIssue,802} from "@nzip/lofi/recipes/file-handler";803804type Preview = { title: string };805const options = {806 accept: { "application/json": [".json"] },807 maxFiles: 1,808 maxFileBytes: 128,809 async parse(file: File): Promise<Preview> {810 const value: unknown = JSON.parse(await file.text());811 if (!value || typeof value !== "object" || typeof (value as { title?: unknown }).title !== "string") {812 throw new Error("invalid field notes export");813 }814 return { title: (value as { title: string }).title };815 },816} as const;817818export default function FileImport() {819 const [supported, setSupported] = useState<boolean>();820 const [drafts, setDrafts] = useState<readonly FileImportDraft<Preview>[]>([]);821 const [issue, setIssue] = useState<FileImportIssue>();822 const [confirmed, setConfirmed] = useState(false);823824 const receive = (next: readonly FileImportDraft<Preview>[]) => {825 setConfirmed(false);826 setIssue(undefined);827 setDrafts(next);828 };829 useEffect(() => {830 const consumer = installFileLaunchConsumer({831 ...options,832 onDrafts: receive,833 onRejected: (next) => {834 setDrafts([]);835 setIssue(next);836 },837 });838 setSupported(consumer.supported);839 return consumer.dispose;840 }, []);841842 async function pick(files: FileList | null) {843 const result = await prepareFileImportDrafts(files ? [...files] : [], options);844 if (result.ok) receive(result.drafts);845 else {846 setDrafts([]);847 setIssue(result.issue);848 }849 }850851 return (852 <section class="island">853 <h1>Import field notes</h1>854 <output data-file-support>{supported === undefined ? "checking" : supported ? "supported" : "unsupported"}</output>855 <label>Choose an export <input type="file" accept="application/json,.json" onChange={(event) => void pick(event.currentTarget.files)} /></label>856 {drafts.map((draft) => <p data-file-title>{draft.parsed.title}</p>)}857 {drafts.length > 0 && <button type="button" onClick={() => setConfirmed(true)}>Import these notes</button>}858 {confirmed && <p role="status">Import confirmed.</p>}859 {issue && <p role="alert">This file cannot be imported.</p>}860 </section>861 );862}863`,864 );865}866867async function installProtocolHandlerRecipe(projectRoot: string) {868 await patchManifest(projectRoot, (manifest) => {869 manifest.protocol_handlers = [{ protocol: "web+lofi", url: "./open-item/?url=%s" }];870 });871 await Deno.writeTextFile(872 join(projectRoot, "src", "pages", "open-item.astro"),873 `---874import ProtocolItem from "../islands/ProtocolItem.tsx";875import Shell from "../layouts/Shell.astro";876import "../styles/global.css";877---878879<Shell title="Open collaborative-list item">880 <ProtocolItem client:load />881</Shell>882`,883 );884 await Deno.writeTextFile(885 join(projectRoot, "src", "islands", "ProtocolItem.tsx"),886 `import { useEffect, useState } from "preact/hooks";887import {888 parseCollaborativeListProtocolTarget,889 type CollaborativeListProtocolResult,890} from "@nzip/lofi/recipes/protocol-handler";891892export default function ProtocolItem() {893 const [result, setResult] = useState<CollaborativeListProtocolResult>();894 useEffect(() => {895 setResult(parseCollaborativeListProtocolTarget(location.search, {896 protocol: "web+lofi",897 parameter: "url",898 maxLength: 256,899 }));900 }, []);901 const base = import.meta.env.BASE_URL;902 if (!result) return <p role="status">Opening item…</p>;903 if (!result.ok) return (904 <section class="island">905 <h1>Open collaborative-list item</h1>906 <p role="alert">This item link is not valid.</p>907 <a data-protocol-fallback href={base}>Open the app normally</a>908 </section>909 );910 const { listId, itemId } = result.target;911 const fallback = new URL(base, location.origin);912 fallback.searchParams.set("list", listId);913 fallback.searchParams.set("item", itemId);914 return (915 <section class="island">916 <h1>Open collaborative-list item</h1>917 <p data-protocol-list>{listId}</p>918 <p data-protocol-item>{itemId}</p>919 <a data-protocol-fallback href={fallback.href}>Open with HTTPS</a>920 </section>921 );922}923`,924 );925}926927async function installRelatedAppDiscoveryRecipe(projectRoot: string) {928 await patchManifest(projectRoot, (manifest) => {929 manifest.prefer_related_applications = false;930 manifest.related_applications = [{931 platform: "play",932 id: "com.example.companion",933 url: "https://play.google.com/store/apps/details?id=com.example.companion",934 }];935 });936 await Deno.writeTextFile(937 join(projectRoot, "src", "pages", "companion.astro"),938 `---939import CompanionDiscovery from "../islands/CompanionDiscovery.tsx";940import Shell from "../layouts/Shell.astro";941import "../styles/global.css";942---943<Shell title="Companion app"><CompanionDiscovery client:load /></Shell>944`,945 );946 await Deno.writeTextFile(947 join(projectRoot, "src", "islands", "CompanionDiscovery.tsx"),948 `import { useEffect, useState } from "preact/hooks";949import { discoverRelatedApplications, type RelatedApplicationDiscovery } from "@nzip/lofi/recipes/related-app-discovery";950const allow = [{ platform: "play", id: "com.example.companion", url: "https://play.google.com/store/apps/details?id=com.example.companion" }] as const;951export default function CompanionDiscovery() {952 const [result, setResult] = useState<RelatedApplicationDiscovery>();953 useEffect(() => { void discoverRelatedApplications({ allow }).then(setResult); }, []);954 const installed = result?.status === "installed";955 return <section class="island"><h1>Companion app</h1><output data-related-status>{result?.status ?? "checking"}</output>{!installed && <p data-related-onboarding>Get the companion app</p>}{installed && <p data-related-installed>Companion already installed</p>}<p data-related-auth>Sign-in and permissions are unchanged.</p></section>;956}957`,958 );959}960961async function installScopeExtensionRecipe(projectRoot: string) {962 await patchManifest(projectRoot, (manifest) => {963 manifest.scope_extensions = [{ type: "origin", origin: "https://help.example.com" }];964 });965 await Deno.writeTextFile(966 join(projectRoot, "src", "pages", "scope-extension.astro"),967 `---968import {969 createScopeExtension,970 createWebAppOriginAssociation,971 verifyWebAppOriginAssociation,972} from "@nzip/lofi/recipes/scope-extension";973import Shell from "../layouts/Shell.astro";974import "../styles/global.css";975const declaration = createScopeExtension("https://help.example.com");976const expected = { manifestId: "https://app.example.com/notes", scope: "/notes/" };977const association = createWebAppOriginAssociation(expected);978const verified = verifyWebAppOriginAssociation(association, expected);979---980<Shell title="Product help">981 <section class="island">982 <h1>Product help origin</h1>983 <output data-scope-origin>{declaration.origin}</output>984 <output data-scope-association>{verified ? JSON.stringify(association) : "invalid"}</output>985 <a data-scope-fallback href="https://help.example.com/notes/">Open product help</a>986 </section>987</Shell>988`,989 );990}991992async function installWindowControlsOverlayRecipe(projectRoot: string) {993 await patchManifest(projectRoot, (manifest) => {994 manifest.display_override = ["window-controls-overlay"];995 });996 await Deno.writeTextFile(997 join(projectRoot, "src", "pages", "desktop-titlebar.astro"),998 `---999import DesktopTitlebar from "../islands/DesktopTitlebar.tsx";1000import Shell from "../layouts/Shell.astro";1001import "../styles/global.css";1002---1003<Shell title="Desktop workspace">1004 <DesktopTitlebar client:load />1005</Shell>1006<style is:global>1007 .desktop-titlebar { min-block-size: 3rem; display: flex; align-items: center; gap: .75rem; border-block-end: 1px solid CanvasText; }1008 @media (display-mode: window-controls-overlay) {1009 .desktop-titlebar { position: fixed; left: env(titlebar-area-x, 0px); top: env(titlebar-area-y, 0px); width: env(titlebar-area-width, 100%); height: env(titlebar-area-height, 3rem); min-block-size: 0; app-region: drag; }1010 .desktop-titlebar :is(button, output) { app-region: no-drag; }1011 }1012 @media (forced-colors: active) { .desktop-titlebar { border-color: CanvasText; } }1013</style>1014`,1015 );1016 await Deno.writeTextFile(1017 join(projectRoot, "src", "islands", "DesktopTitlebar.tsx"),1018 `import { useEffect, useState } from "preact/hooks";1019import { observeWindowControlsOverlay, type WindowControlsOverlayGeometry } from "@nzip/lofi/recipes/window-controls-overlay";1020export default function DesktopTitlebar() {1021 const [supported, setSupported] = useState<boolean>();1022 const [geometry, setGeometry] = useState<WindowControlsOverlayGeometry>();1023 useEffect(() => {1024 const observer = observeWindowControlsOverlay({ onGeometry: setGeometry });1025 setSupported(observer.supported);1026 return observer.dispose;1027 }, []);1028 return <header class="desktop-titlebar" data-desktop-titlebar>1029 <strong>Field Notes</strong>1030 <button type="button">Workspace menu</button>1031 <output data-overlay-support>{supported === undefined ? "checking" : supported ? "supported" : "unsupported"}</output>1032 {geometry && <output data-overlay-width>{geometry.titlebarArea.width}</output>}1033 </header>;1034}1035`,1036 );1037}10381039async function main() {1040 const { source, version } = parseArgs(Deno.args);1041 await Deno.remove(artifactsRoot, { recursive: true }).catch((error) => {1042 if (!(error instanceof Deno.errors.NotFound)) throw error;1043 });1044 const temporaryRoot = await Deno.makeTempDir({ prefix: "lofi-golden-" });1045 const port = allocatePort();1046 // `localhost` is a valid WebAuthn RP-ID; an IP address is not portable across1047 // authenticators even though Chromium treats it as a secure context.1048 const origin = `http://localhost:${port}/`;1049 let projectRoot: string | undefined;1050 let dev: ManagedProcess | undefined;1051 let preview: ManagedProcess | undefined;1052 let browser: Browser | undefined;1053 let context: BrowserContext | undefined;1054 let page: Page | undefined;1055 let syncServer: Awaited<ReturnType<typeof startLocalJazzServer>> | undefined;1056 let recoveryCredential: VirtualAuthenticatorCredential | undefined;1057 let recoveryPhrase = "";1058 let restoredIdentity = "";1059 const browserDiagnostics: string[] = [];1060 try {1061 projectRoot = await stage("create", () => generateProject(source, version, temporaryRoot));1062 await stage("generated boundary", () => assertGeneratedBoundary(projectRoot!, source, version));1063 await stage(1064 "doctor",1065 () =>1066 runCommand("doctor", Deno.execPath(), ["task", "doctor"], projectRoot!).then(() =>1067 undefined1068 ),1069 );1070 await stage(1071 "test",1072 () =>1073 runCommand("test", Deno.execPath(), ["task", "test"], projectRoot!).then(() => undefined),1074 );10751076 syncServer = await stage("managed sync", () => configureGoldenSync(projectRoot!));1077 const syncEnvironment = {1078 JAZZ_APP_ID: syncServer.appId,1079 JAZZ_SERVER_URL: syncServer.url,1080 };10811082 dev = new ManagedProcess(1083 "dev",1084 [1085 "task",1086 "dev",1087 "--",1088 "--host",1089 "127.0.0.1",1090 "--port",1091 String(port),1092 ],1093 projectRoot,1094 syncEnvironment,1095 );1096 await stage(1097 "development readiness",1098 async () => {1099 await dev!.waitForOutput(new RegExp(`http://127\\.0\\.0\\.1:${port}/`));1100 await waitForListening(dev!, port);1101 },1102 );1103 browser = await chromium.launch({ headless: true });1104 context = await browser.newContext();1105 await context.tracing.start({ screenshots: false, snapshots: false, sources: false });1106 page = await context.newPage();1107 const profileAAuthenticator = await withVirtualAuthenticator(page);1108 page.on(1109 "console",1110 (message) => browserDiagnostics.push(`console ${message.type()}: ${message.text()}`),1111 );1112 page.on(1113 "pageerror",1114 (error) => browserDiagnostics.push(`pageerror: ${error.name}: ${error.message}`),1115 );1116 page.on(1117 "requestfailed",1118 (request) =>1119 browserDiagnostics.push(1120 `requestfailed: ${request.url()} ${request.failure()?.errorText ?? "unknown"}`,1121 ),1122 );1123 await stage("development browser", async () => {1124 await page!.goto(origin, { waitUntil: "domcontentloaded" });1125 await waitForReady(page!, taskAppReady, undefined, {1126 description: "generated task app local store",1127 timeoutMs: 30_000,1128 });1129 await page!.fill("#new-task", privateValues[0]);1130 await page!.press("#new-task", "Enter");1131 await page!.locator(".task", { hasText: privateValues[0] }).waitFor({ state: "visible" });1132 await page!.reload({ waitUntil: "domcontentloaded" });1133 await waitForReady(page!, taskAppReady, undefined, {1134 description: "task app after full reload",1135 });1136 await page!.locator(".task", { hasText: privateValues[0] }).waitFor({ state: "visible" });1137 const worker = await page!.evaluate(() =>1138 (globalThis as typeof globalThis & { __LOFI_PWA_STATE__?: { worker?: string } })1139 .__LOFI_PWA_STATE__?.worker1140 );1141 assert(worker === "development-disabled", `development worker state was ${worker}`);1142 });1143 await stage("passkey backup", async () => {1144 // Backup is a two-step ceremony: the phrase is revealed first and sync1145 // is enabled only after the explicit saved-it confirmation (which1146 // reloads the document into the managed namespace).1147 await page!.getByRole("button", { name: "Back up & enable sync" }).click();1148 await page!.locator('[aria-label="Recovery phrase"] li').first().waitFor({1149 state: "visible",1150 timeout: 30_000,1151 });1152 await page!.getByRole("button", { name: "I saved my phrase — enable sync" }).click();1153 await page!.getByRole("heading", { name: "Backed up & syncing" }).waitFor({1154 state: "visible",1155 timeout: 30_000,1156 });1157 await page!.getByRole("button", { name: "Show recovery phrase" }).click();1158 await page!.locator(".account-words").waitFor({ state: "visible" });1159 recoveryPhrase = (await page!.locator(".account-words li").allTextContents()).join(" ");1160 assert(recoveryPhrase.split(" ").length === 24, "backup did not reveal a 24-word phrase");1161 privateValues.push(recoveryPhrase);1162 restoredIdentity = (await page!.locator("[data-sharing-identity]").textContent())?.trim() ??1163 "";1164 assert(restoredIdentity.startsWith("lofi1:"), "profile A did not expose a sharing identity");1165 const credentials = await profileAAuthenticator.credentials();1166 assert(1167 credentials.length === 1,1168 `expected one recoverable passkey, received ${credentials.length}`,1169 );1170 recoveryCredential = credentials[0];1171 await waitForReady(page!, taskAppReady, undefined, {1172 description: "task store after sync runtime recreation",1173 timeoutMs: 30_000,1174 });1175 await page!.fill("#new-task", privateValues[2]);1176 await page!.press("#new-task", "Enter");1177 await page!.locator(".task", { hasText: privateValues[2] }).waitFor({ state: "visible" });1178 });1179 await profileAAuthenticator.dispose();11801181 await stage("two-profile passkey restore", async () => {1182 const profileBBrowser = await chromium.launch({ headless: true });1183 const profileB = await profileBBrowser.newContext();1184 const profileBPage = await profileB.newPage();1185 const profileBAuthenticator = await withVirtualAuthenticator(profileBPage);1186 try {1187 await profileBPage.goto(origin, { waitUntil: "domcontentloaded" });1188 await waitForReady(profileBPage, taskAppReady, undefined, {1189 description: "fresh profile B local account",1190 timeoutMs: 30_000,1191 });1192 await profileBAuthenticator.addCredential(recoveryCredential!);1193 assert(1194 (await profileBAuthenticator.credentials()).length === 1,1195 "profile B virtual authenticator did not retain the recovery credential",1196 );1197 await selectVirtualCredential(profileBPage, recoveryCredential!.credentialId);1198 await profileBPage.getByRole("checkbox", { name: /I understand restore replaces/ }).check();1199 await profileBPage.getByRole("button", { name: "Use passkey" }).click();1200 try {1201 await profileBPage.getByRole("heading", { name: "Backed up & syncing" }).waitFor({1202 state: "visible",1203 timeout: 5_000,1204 });1205 } catch (cause) {1206 const accountText = await profileBPage.locator(".account").innerText().catch(() => "");1207 const elected = await profileBPage.evaluate(() =>1208 Object.entries(localStorage).some(([key, value]) =>1209 key.startsWith("lofi:sync-elected:") && value === "1"1210 )1211 );1212 throw new Error(1213 `profile B did not complete passkey restore (sync elected: ${elected}): ${accountText}`,1214 { cause },1215 );1216 }1217 const identityB = (await profileBPage.locator("[data-sharing-identity]").textContent())1218 ?.trim() ?? "";1219 assert(1220 identityB === restoredIdentity,1221 "passkey restore opened a different session.user_id",1222 );1223 await waitForReady(profileBPage, taskAppReady, undefined, {1224 description: "profile B restored task store",1225 timeoutMs: 30_000,1226 });1227 await profileBPage.locator(".task", { hasText: privateValues[2] }).waitFor({1228 state: "visible",1229 timeout: 30_000,1230 });1231 await profileBPage.locator(".task", { hasText: privateValues[0] }).waitFor({1232 state: "visible",1233 timeout: 30_000,1234 });1235 } finally {1236 await profileBAuthenticator.dispose().catch(() => undefined);1237 await profileB.close();1238 await profileBBrowser.close();1239 }1240 });12411242 await stage("phrase fallback after unavailable passkey", async () => {1243 const profileCBrowser = await chromium.launch({ headless: true });1244 const profileC = await profileCBrowser.newContext();1245 await profileC.addInitScript(() => {1246 Object.defineProperty(navigator, "credentials", { configurable: true, value: undefined });1247 });1248 const profileCPage = await profileC.newPage();1249 try {1250 await profileCPage.goto(origin, { waitUntil: "domcontentloaded" });1251 await waitForReady(profileCPage, taskAppReady, undefined, {1252 description: "fresh profile C local account",1253 timeoutMs: 30_000,1254 });1255 await profileCPage.getByRole("checkbox", { name: /I understand restore replaces/ }).check();1256 await profileCPage.getByRole("button", { name: "Use passkey" }).click();1257 await profileCPage.locator("#recovery-input").waitFor({1258 state: "visible",1259 timeout: 10_000,1260 });1261 await profileCPage.fill("#recovery-input", recoveryPhrase);1262 await profileCPage.getByRole("button", { name: "Restore account" }).click();1263 await profileCPage.getByRole("heading", { name: "Backed up & syncing" }).waitFor({1264 state: "visible",1265 timeout: 30_000,1266 });1267 const identityC = (await profileCPage.locator("[data-sharing-identity]").textContent())1268 ?.trim() ?? "";1269 assert(1270 identityC === restoredIdentity,1271 "phrase fallback opened a different session.user_id",1272 );1273 } finally {1274 await profileC.close();1275 await profileCBrowser.close();1276 }1277 });1278 await stage("return to local-only", async () => {1279 await page!.getByRole("button", { name: "Stop syncing" }).click();1280 await page!.getByRole("heading", { name: "Back up & sync" }).waitFor({ state: "visible" });1281 await waitForReady(page!, taskAppReady, undefined, {1282 description: "task store after returning local-only",1283 timeoutMs: 30_000,1284 });1285 await page!.locator(".task", { hasText: privateValues[2] }).waitFor({ state: "visible" });1286 });1287 await stage("development shutdown", () => dev!.stop());1288 dev = undefined;12891290 await stage("install web-share recipe", () => installWebShareRecipe(projectRoot!));1291 await stage("install launch-handler recipe", () => installLaunchHandlerRecipe(projectRoot!));1292 await stage("install file-handler recipe", () => installFileHandlerRecipe(projectRoot!));1293 await stage(1294 "install protocol-handler recipe",1295 () => installProtocolHandlerRecipe(projectRoot!),1296 );1297 await stage(1298 "install related-app-discovery recipe",1299 () => installRelatedAppDiscoveryRecipe(projectRoot!),1300 );1301 await stage(1302 "install scope-extension recipe",1303 () => installScopeExtensionRecipe(projectRoot!),1304 );1305 await stage(1306 "install window-controls-overlay recipe",1307 () => installWindowControlsOverlayRecipe(projectRoot!),1308 );13091310 await stage(1311 "build",1312 () =>1313 runCommand(1314 "build",1315 Deno.execPath(),1316 ["task", "build"],1317 projectRoot!,1318 syncEnvironment,1319 ).then(() => undefined),1320 );1321 await stage("production output", () => assertProductionOutput(projectRoot!));1322 preview = new ManagedProcess(1323 "preview",1324 ["task", "preview", "--port", String(port)],1325 projectRoot,1326 );1327 await stage(1328 "preview readiness",1329 async () => {1330 await preview!.waitForOutput(1331 new RegExp(`lofi preview: http://127\\.0\\.0\\.1:${port}/`),1332 );1333 await waitForListening(preview!, port);1334 },1335 );1336 await stage("manual install fallback browser", async () => {1337 const fallbackPage = await context!.newPage();1338 try {1339 await fallbackPage.addInitScript(() => {1340 const addEventListener = globalThis.addEventListener.bind(globalThis);1341 globalThis.addEventListener = ((1342 type: string,1343 listener: EventListenerOrEventListenerObject,1344 options?: boolean | AddEventListenerOptions,1345 ) => {1346 if (type !== "beforeinstallprompt") addEventListener(type, listener, options);1347 }) as typeof globalThis.addEventListener;1348 });1349 await fallbackPage.goto(origin, { waitUntil: "domcontentloaded" });1350 await fallbackPage.getByText("No in-page install button is available right now.")1351 .waitFor({ state: "visible", timeout: 30_000 });1352 const guidance = await fallbackPage.locator(".pwa-browser-guidance").textContent() ?? "";1353 assert(1354 guidance.includes("if offered"),1355 "manual browser guidance claimed an unavailable install capability",1356 );1357 } finally {1358 await fallbackPage.close();1359 }1360 });1361 await stage("production offline browser", async () => {1362 await page!.goto(origin, { waitUntil: "domcontentloaded" });1363 await waitForReady(1364 page!,1365 () => {1366 const global = globalThis as typeof globalThis & {1367 __LOFI_PWA_STATE__?: { worker?: string };1368 };1369 return global.__LOFI_PWA_STATE__?.worker === "ready";1370 },1371 undefined,1372 { description: "production service worker ready", timeoutMs: 30_000 },1373 );1374 await waitForReady(page!, taskAppReady, undefined, {1375 description: "production task app ready",1376 });1377 const shellCached = await page!.evaluate(async () =>1378 Boolean(await caches.match(new URL("./", location.href)))1379 );1380 assert(shellCached, "service-worker precache does not contain the app shell");1381 await context!.setOffline(true);1382 await page!.reload({ waitUntil: "domcontentloaded" });1383 await waitForReady(page!, taskAppReady, undefined, {1384 description: "offline cold page ready",1385 });1386 await page!.locator(".task", { hasText: privateValues[2] }).waitFor({ state: "visible" });1387 await page!.fill("#new-task", privateValues[1]);1388 await page!.press("#new-task", "Enter");1389 await page!.locator(".task", { hasText: privateValues[1] }).waitFor({ state: "visible" });1390 await page!.reload({ waitUntil: "domcontentloaded" });1391 await waitForReady(page!, taskAppReady, undefined, { description: "offline task persisted" });1392 await page!.locator(".task", { hasText: privateValues[1] }).waitFor({ state: "visible" });1393 await context!.setOffline(false);1394 await waitForReady(1395 page!,1396 () => {1397 const syncLabel = [...document.querySelectorAll("dt")].find((node) =>1398 node.textContent?.trim() === "Sync"1399 );1400 const taskStatus = document.querySelector('[role="status"]')?.textContent ?? "";1401 return /item\(s\)/.test(taskStatus) &&1402 syncLabel?.nextElementSibling?.textContent?.trim() === "available — not yet backed up";1403 },1404 undefined,1405 { description: "public local-only durability and sync state" },1406 );1407 const state = await page!.evaluate(() => {1408 const syncLabel = [...document.querySelectorAll("dt")].find((node) =>1409 node.textContent?.trim() === "Sync"1410 );1411 return {1412 worker: (globalThis as typeof globalThis & { __LOFI_PWA_STATE__?: { worker?: string } })1413 .__LOFI_PWA_STATE__?.worker,1414 status: document.querySelector('[role="status"]')?.textContent ?? "",1415 sync: syncLabel?.nextElementSibling?.textContent?.trim() ?? "",1416 };1417 });1418 assert(state.worker === "ready", `production service worker ended in ${state.worker}`);1419 assert(/item\(s\)/.test(state.status), "public retained-row state is missing");1420 assert(1421 state.sync === "available — not yet backed up",1422 `unexpected public sync state ${state.sync}`,1423 );1424 });1425 await stage("web-share recipe browser", async () => {1426 await context!.setOffline(true);1427 try {1428 await page!.goto(1429 `${origin}share/?title=One+task&text=Review+it&url=https%3A%2F%2Fexample.com%2Fnotes`,1430 { waitUntil: "domcontentloaded" },1431 );1432 await page!.getByRole("heading", { name: "Review shared content" }).waitFor({1433 state: "visible",1434 });1435 assert(1436 await page!.locator("[data-share-title]").textContent() === "One task",1437 "offline share title was not parsed",1438 );1439 assert(1440 await page!.locator("[data-share-url]").getAttribute("href") ===1441 "https://example.com/notes",1442 "offline share URL was not normalized",1443 );1444 assert(1445 await page!.getByText("Draft confirmed.").count() === 0,1446 "received share was confirmed before the user action",1447 );1448 await page!.getByRole("button", { name: "Add to app" }).click();1449 await page!.getByText("Draft confirmed.").waitFor({ state: "visible" });1450 await page!.getByRole("button", { name: "Share this draft" }).click();1451 await page!.locator("[data-share-outcome]").waitFor({ state: "visible" });1452 assert(1453 await page!.locator("[data-share-outcome]").textContent() === "fallback",1454 "unsupported outbound share did not use the normal web fallback",1455 );14561457 await page!.goto(`${origin}share/?url=javascript%3Aalert%281%29`, {1458 waitUntil: "domcontentloaded",1459 });1460 await page!.getByRole("alert").waitFor({ state: "visible" });1461 const invalidText = await page!.locator("body").innerText();1462 assert(!invalidText.includes("javascript:"), "invalid share value was echoed in the UI");1463 } finally {1464 await context!.setOffline(false);1465 }14661467 await page!.addInitScript(() => {1468 Object.defineProperty(navigator, "canShare", {1469 configurable: true,1470 value: () => true,1471 });1472 Object.defineProperty(navigator, "share", {1473 configurable: true,1474 value: () => Promise.reject(new DOMException("closed", "AbortError")),1475 });1476 });1477 await page!.goto(`${origin}share/?text=Cancelled+share`, {1478 waitUntil: "domcontentloaded",1479 });1480 await page!.getByRole("button", { name: "Share this draft" }).click();1481 await page!.locator("[data-share-outcome]").waitFor({ state: "visible" });1482 assert(1483 await page!.locator("[data-share-outcome]").textContent() === "cancelled",1484 "browser cancellation was reported as a failure",1485 );1486 await page!.goto(origin, { waitUntil: "domcontentloaded" });1487 await waitForReady(page!, taskAppReady, undefined, {1488 description: "task app after share recipe exercise",1489 });1490 });1491 await stage("launch-handler recipe browser", async () => {1492 const unsupportedPage = await context!.newPage();1493 try {1494 await unsupportedPage.addInitScript(() => {1495 Object.defineProperty(globalThis, "launchQueue", {1496 configurable: true,1497 value: undefined,1498 });1499 });1500 await unsupportedPage.goto(`${origin}launch/`, { waitUntil: "domcontentloaded" });1501 await unsupportedPage.locator("[data-launch-support]").waitFor({ state: "visible" });1502 assert(1503 await unsupportedPage.locator("[data-launch-support]").textContent() === "unsupported",1504 "missing launchQueue was not reported as unsupported",1505 );1506 } finally {1507 await unsupportedPage.close();1508 }15091510 await page!.addInitScript(() => {1511 let consumer: (parameters: { targetURL?: string }) => void = () => {};1512 Object.defineProperty(globalThis, "launchQueue", {1513 configurable: true,1514 value: {1515 setConsumer(next: typeof consumer) {1516 consumer = next;1517 },1518 },1519 });1520 Object.defineProperty(globalThis, "__LOFI_DELIVER_LAUNCH__", {1521 configurable: true,1522 value: (targetURL: string) => consumer({ targetURL }),1523 });1524 });1525 await context!.setOffline(true);1526 try {1527 await page!.goto(`${origin}launch/`, { waitUntil: "domcontentloaded" });1528 await waitForReady(1529 page!,1530 () => document.querySelector("[data-launch-support]")?.textContent === "supported",1531 undefined,1532 { description: "mock launchQueue consumed" },1533 );1534 await page!.evaluate((targetURL) => {1535 const deliver = (globalThis as typeof globalThis & {1536 __LOFI_DELIVER_LAUNCH__?: (targetURL: string) => void;1537 }).__LOFI_DELIVER_LAUNCH__;1538 deliver?.(targetURL);1539 }, `${origin}launch/?item=1#selected`);1540 await page!.locator("[data-launch-target]").waitFor({ state: "visible" });1541 assert(1542 await page!.locator("[data-launch-target]").textContent() ===1543 `${origin}launch/?item=1#selected`,1544 "valid offline launch target was not normalized",1545 );1546 await page!.evaluate(() => {1547 const deliver = (globalThis as typeof globalThis & {1548 __LOFI_DELIVER_LAUNCH__?: (targetURL: string) => void;1549 }).__LOFI_DELIVER_LAUNCH__;1550 deliver?.("https://outside.invalid/private");1551 });1552 await page!.getByRole("alert").waitFor({ state: "visible" });1553 const rejected = await page!.locator("body").innerText();1554 assert(!rejected.includes("outside.invalid"), "rejected launch target leaked into the UI");1555 } finally {1556 await context!.setOffline(false);1557 }1558 await page!.goto(origin, { waitUntil: "domcontentloaded" });1559 await waitForReady(page!, taskAppReady, undefined, {1560 description: "task app after launch-handler exercise",1561 });1562 });1563 await stage("file-handler recipe browser", async () => {1564 const unsupportedPage = await context!.newPage();1565 try {1566 await unsupportedPage.addInitScript(() => {1567 Object.defineProperty(globalThis, "launchQueue", {1568 configurable: true,1569 value: undefined,1570 });1571 });1572 await unsupportedPage.goto(`${origin}import/`, { waitUntil: "domcontentloaded" });1573 await unsupportedPage.locator("[data-file-support]").waitFor({ state: "visible" });1574 assert(1575 await unsupportedPage.locator("[data-file-support]").textContent() === "unsupported",1576 "missing launchQueue was not reported as unsupported for file import",1577 );1578 await unsupportedPage.locator('input[type="file"]').setInputFiles({1579 name: "picker.json",1580 mimeType: "application/json",1581 buffer: Buffer.from('{"title":"Picker import"}'),1582 });1583 await unsupportedPage.getByText("Picker import").waitFor({ state: "visible" });1584 } finally {1585 await unsupportedPage.close();1586 }15871588 await page!.addInitScript(() => {1589 let consumer: (parameters: { files?: unknown[] }) => void = () => {};1590 Object.defineProperty(globalThis, "launchQueue", {1591 configurable: true,1592 value: {1593 setConsumer(next: typeof consumer) {1594 consumer = next;1595 },1596 },1597 });1598 Object.defineProperty(globalThis, "__LOFI_DELIVER_FILE__", {1599 configurable: true,1600 value: (name: string, type: string, content: string) =>1601 consumer({1602 files: [{1603 kind: "file",1604 name,1605 getFile: () => Promise.resolve(new File([content], name, { type })),1606 }],1607 }),1608 });1609 });1610 await context!.setOffline(true);1611 try {1612 await page!.goto(`${origin}import/`, { waitUntil: "domcontentloaded" });1613 await waitForReady(1614 page!,1615 () => document.querySelector("[data-file-support]")?.textContent === "supported",1616 undefined,1617 { description: "mock launchQueue consumed by file import" },1618 );1619 await page!.evaluate(() => {1620 const deliver = (globalThis as typeof globalThis & {1621 __LOFI_DELIVER_FILE__?: (name: string, type: string, content: string) => void;1622 }).__LOFI_DELIVER_FILE__;1623 deliver?.("opened.json", "application/json", '{"title":"Opened import"}');1624 });1625 await page!.getByText("Opened import").waitFor({ state: "visible" });1626 assert(1627 await page!.getByText("Import confirmed.").count() === 0,1628 "received file was persisted before explicit confirmation",1629 );1630 await page!.getByRole("button", { name: "Import these notes" }).click();1631 await page!.getByText("Import confirmed.").waitFor({ state: "visible" });1632 await page!.evaluate(() => {1633 const deliver = (globalThis as typeof globalThis & {1634 __LOFI_DELIVER_FILE__?: (name: string, type: string, content: string) => void;1635 }).__LOFI_DELIVER_FILE__;1636 deliver?.("private.txt", "text/plain", "private-value");1637 });1638 await page!.getByRole("alert").waitFor({ state: "visible" });1639 const rejected = await page!.locator("body").innerText();1640 assert(!rejected.includes("private-value"), "rejected file content leaked into the UI");1641 assert(!rejected.includes("private.txt"), "rejected file name leaked into the UI");1642 } finally {1643 await context!.setOffline(false);1644 }1645 await page!.goto(origin, { waitUntil: "domcontentloaded" });1646 await waitForReady(page!, taskAppReady, undefined, {1647 description: "task app after file-handler exercise",1648 });1649 });1650 await stage("protocol-handler recipe browser", async () => {1651 await context!.setOffline(true);1652 try {1653 const payload = encodeURIComponent(1654 "web+lofi:collaborative-list/list_123/item/item_456",1655 );1656 await page!.goto(`${origin}open-item/?url=${payload}`, {1657 waitUntil: "domcontentloaded",1658 });1659 await page!.getByText("list_123").waitFor({ state: "visible" });1660 await page!.getByText("item_456").waitFor({ state: "visible" });1661 const fallback = await page!.locator("[data-protocol-fallback]").getAttribute("href");1662 assert(1663 fallback === `${origin}?list=list_123&item=item_456`,1664 "protocol recipe did not preserve the ordinary HTTPS fallback",1665 );16661667 const invalid = encodeURIComponent("web+lofi:https://outside.invalid/private-value");1668 await page!.goto(`${origin}open-item/?url=${invalid}`, {1669 waitUntil: "domcontentloaded",1670 });1671 await page!.getByRole("alert").waitFor({ state: "visible" });1672 const rejected = await page!.locator("body").innerText();1673 assert(!rejected.includes("outside.invalid"), "rejected protocol host leaked into the UI");1674 assert(!rejected.includes("private-value"), "rejected protocol value leaked into the UI");16751676 await page!.goto(`${origin}open-item/`, { waitUntil: "domcontentloaded" });1677 await page!.getByRole("alert").waitFor({ state: "visible" });1678 assert(1679 await page!.locator("[data-protocol-fallback]").getAttribute("href") === "/",1680 "direct handler visit lost its ordinary app fallback",1681 );1682 } finally {1683 await context!.setOffline(false);1684 }1685 await page!.goto(origin, { waitUntil: "domcontentloaded" });1686 await waitForReady(page!, taskAppReady, undefined, {1687 description: "task app after protocol-handler exercise",1688 });1689 });1690 await stage("related-app-discovery recipe browser", async () => {1691 const unsupportedPage = await context!.newPage();1692 try {1693 await unsupportedPage.addInitScript(() => {1694 Object.defineProperty(navigator, "getInstalledRelatedApps", {1695 configurable: true,1696 value: undefined,1697 });1698 });1699 await unsupportedPage.goto(`${origin}companion/`, { waitUntil: "domcontentloaded" });1700 await unsupportedPage.locator("[data-related-onboarding]").waitFor({ state: "visible" });1701 await waitForReady(1702 unsupportedPage,1703 () => document.querySelector("[data-related-status]")?.textContent === "unsupported",1704 undefined,1705 { description: "unsupported related-app discovery" },1706 );1707 } finally {1708 await unsupportedPage.close();1709 }1710 await page!.addInitScript(() => {1711 Object.defineProperty(navigator, "getInstalledRelatedApps", {1712 configurable: true,1713 value: () =>1714 Promise.resolve([{1715 platform: "play",1716 id: "com.example.companion",1717 url: "https://play.google.com/store/apps/details?id=com.example.companion",1718 version: "private-version",1719 }]),1720 });1721 });1722 await page!.goto(`${origin}companion/`, { waitUntil: "domcontentloaded" });1723 await page!.locator("[data-related-installed]").waitFor({ state: "visible" });1724 assert(1725 await page!.locator("[data-related-auth]").textContent() ===1726 "Sign-in and permissions are unchanged.",1727 "discovery presentation implied an auth or authorization change",1728 );1729 assert(1730 !(await page!.locator("body").innerText()).includes("private-version"),1731 "browser-supplied companion version leaked into presentation",1732 );1733 await page!.goto(origin, { waitUntil: "domcontentloaded" });1734 await waitForReady(page!, taskAppReady, undefined, {1735 description: "task app after related-app discovery exercise",1736 });1737 });1738 await stage("scope-extension recipe browser", async () => {1739 await page!.goto(`${origin}scope-extension/`, { waitUntil: "domcontentloaded" });1740 assert(1741 await page!.locator("[data-scope-origin]").textContent() === "https://help.example.com",1742 "scope extension origin was not normalized",1743 );1744 const association = await page!.locator("[data-scope-association]").textContent() ?? "";1745 assert(1746 association.includes('"https://app.example.com/notes":{"scope":"/notes/"}'),1747 "reciprocal association was not generated and verified",1748 );1749 assert(1750 await page!.locator("[data-scope-fallback]").getAttribute("href") ===1751 "https://help.example.com/notes/",1752 "unsupported scope extension lost ordinary external navigation",1753 );1754 await page!.goto(origin, { waitUntil: "domcontentloaded" });1755 await waitForReady(page!, taskAppReady, undefined, {1756 description: "task app after scope-extension exercise",1757 });1758 });1759 await stage("window-controls-overlay recipe browser", async () => {1760 await page!.goto(`${origin}desktop-titlebar/`, { waitUntil: "domcontentloaded" });1761 const titlebar = page!.locator("[data-desktop-titlebar]");1762 await titlebar.waitFor({ state: "visible" });1763 await page!.getByRole("button", { name: "Workspace menu" }).focus();1764 assert(1765 await page!.getByRole("button", { name: "Workspace menu" }).evaluate((element) =>1766 element === document.activeElement1767 ),1768 "titlebar controls were not keyboard reachable",1769 );1770 const box = await titlebar.boundingBox();1771 assert(1772 Boolean(box && box.width > 0 && box.height > 0),1773 "standalone titlebar fallback collapsed",1774 );1775 await context!.setOffline(true);1776 try {1777 await page!.goto(`${origin}desktop-titlebar/`, { waitUntil: "domcontentloaded" });1778 await page!.locator("[data-desktop-titlebar]").waitFor({ state: "visible" });1779 } finally {1780 await context!.setOffline(false);1781 }1782 await page!.goto(origin, { waitUntil: "domcontentloaded" });1783 await waitForReady(page!, taskAppReady, undefined, {1784 description: "task app after window-controls-overlay exercise",1785 });1786 });1787 await stage("waiting worker update browser", async () => {1788 const workerPath = join(projectRoot!, "dist", "sw.js");1789 const worker = await Deno.readTextFile(workerPath);1790 await Deno.writeTextFile(workerPath, `${worker}\n// golden update ${Date.now()}\n`);1791 await page!.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));1792 await waitForReady(1793 page!,1794 () => {1795 const global = globalThis as typeof globalThis & {1796 __LOFI_PWA_STATE__?: { update?: string };1797 };1798 return global.__LOFI_PWA_STATE__?.update === "ready";1799 },1800 undefined,1801 { description: "waiting service-worker update", timeoutMs: 30_000 },1802 );1803 await Promise.all([1804 page!.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 30_000 }),1805 page!.getByRole("button", { name: "Update app" }).click(),1806 ]);1807 await waitForReady(1808 page!,1809 () => {1810 const global = globalThis as typeof globalThis & {1811 __LOFI_PWA_STATE__?: { worker?: string; update?: string };1812 };1813 return global.__LOFI_PWA_STATE__?.worker === "ready" &&1814 global.__LOFI_PWA_STATE__?.update === "idle";1815 },1816 undefined,1817 { description: "applied service-worker update", timeoutMs: 30_000 },1818 );1819 });1820 await context.tracing.stop();1821 await stage("preview shutdown", () => preview!.stop());1822 preview = undefined;1823 console.log(1824 `golden result: ${source} @nzip/lofi ${version} passed on allocated origin ${origin}`,1825 );1826 console.log(1827 `golden timings: ${1828 [...timings].map(([name, milliseconds]) => `${name}=${milliseconds}ms`).join(", ")1829 }`,1830 );1831 } catch (error) {1832 await Promise.allSettled([dev?.stop(), preview?.stop()]);1833 dev = undefined;1834 preview = undefined;1835 await captureFailure(error, projectRoot, page, context, browserDiagnostics);1836 throw error;1837 } finally {1838 await Promise.allSettled([dev?.stop(), preview?.stop()]);1839 await context?.close().catch(() => undefined);1840 await browser?.close().catch(() => undefined);1841 await syncServer?.stop().catch(() => undefined);1842 await Deno.remove(temporaryRoot, { recursive: true }).catch(() => undefined);1843 }1844}18451846if (import.meta.main) {1847 await main().catch((error) => {1848 console.error(error instanceof Error ? error.message : String(error));1849 Deno.exit(1);1850 });1851}