1import type {2 BrowserTestClient,3 ConcurrentOfflineScenario,4 TwoClientFixture,5} from "@nzip/lofi/testing";67/**8 * A real, worked example of the `@nzip/lofi/testing` toolkit: two browser9 * clients each add a task while offline, then reconnect and converge on both.10 *11 * This is an opt-in browser gate, not part of `deno task test`:12 * 1. Configure managed sync (JAZZ_APP_ID + JAZZ_SERVER_URL in .env) so two13 * clients share one synced account, then serve the app — for example14 * `deno task build` followed by `deno task preview`, or `deno task dev`.15 * 2. Point the test at that URL and run it directly:16 * LOFI_E2E_BASE_URL=http://127.0.0.1:4321/ deno test -A tests/convergence_e2e_test.ts17 *18 * Without LOFI_E2E_BASE_URL (or without Chromium installed) it skips, so the19 * default suite stays fast and never launches a browser.20 */2122function e2eBaseURL(): string | undefined {23 // The default suite runs without --allow-env; treat a denied read as "skip".24 try {25 return Deno.env.get("LOFI_E2E_BASE_URL") || undefined;26 } catch {27 return undefined;28 }29}3031// Runs inside the browser page, so it may only touch DOM APIs: the status line32// reports "N item(s) …" once the local store has hydrated.33const taskListReady = () => {34 const status = document.querySelector('[role="status"]');35 return status !== null && /item\(s\)/.test(status.textContent ?? "");36};3738Deno.test("two clients converge on concurrent offline task edits", async () => {39 const baseURL = e2eBaseURL();40 if (!baseURL) {41 console.log(42 "skipping convergence example; set LOFI_E2E_BASE_URL to a synced deployment to run it",43 );44 return;45 }4647 // Imported lazily so the default suite never loads Playwright.48 const {49 BrowserUnavailableError,50 createTwoClientFixture,51 runConcurrentOfflineConvergence,52 waitForReady,53 } = await import("@nzip/lofi/testing");5455 const ready = (client: BrowserTestClient) =>56 waitForReady(client.page, taskListReady, undefined, { description: `${client.name} ready` });57 const addTask = async (client: BrowserTestClient, text: string) => {58 await client.page.fill("#new-task", text);59 await client.page.press("#new-task", "Enter");60 };61 const seeTask = (client: BrowserTestClient, text: string) =>62 client.page.locator(".task", { hasText: text }).first().waitFor({63 state: "visible",64 timeout: 15_000,65 });6667 let fixture: TwoClientFixture | undefined;68 try {69 fixture = await createTwoClientFixture({70 baseURL,71 // Shared identity clones the first client's device key in memory, so both72 // browser contexts act as the same synced account.73 identity: { mode: "shared", preparePrimary: ready },74 artifacts: { directory: "test-results" },75 });76 } catch (error) {77 if (error instanceof BrowserUnavailableError) {78 console.log(error.message);79 return;80 }81 throw error;82 }8384 try {85 const scenario: ConcurrentOfflineScenario<string> = {86 edits: ["first client task", "second client task"],87 ready,88 apply: addTask,89 locallyApplied: seeTask,90 converged: async ({ clients }) => {91 await Promise.all(clients.map((client) =>92 Promise.all([93 seeTask(client, "first client task"),94 seeTask(client, "second client task"),95 ])96 ));97 },98 // Failure artifacts stay value-free: counts and booleans only, no task text.99 snapshot: async (client) => ({100 online: !client.offline,101 items: await client.page.locator(".task").count(),102 }),103 failureLabel: "task-convergence",104 };105 await runConcurrentOfflineConvergence(fixture, scenario);106 } finally {107 await fixture.close();108 }109});