clone

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

feat/141-demo-review-fixes
lofi / package / starter / tests / convergence_e2e_test.ts.txt
convergence_e2e_test.ts.txt 4.6K
1/**2 * A real, worked example of the `@nzip/lofi/testing` browser scenario: two3 * browser clients each add a task while offline, then reconnect and converge4 * on both.5 *6 * This is an opt-in browser gate, not part of `deno task test`:7 * 1. Configure managed sync (JAZZ_APP_ID + JAZZ_SERVER_URL in .env) with the8 * schema deployed to that store, then serve the app — for example9 * `deno task build` followed by `deno task preview`, or `deno task dev`.10 * 2. Point the test at that URL — it must be `localhost`, not `127.0.0.1`,11 * because the sync election enrolls a passkey and an IP address is not a12 * valid WebAuthn RP ID — and run it directly:13 * LOFI_E2E_BASE_URL=http://localhost:4321/ deno test -A tests/convergence_e2e_test.ts14 *15 * The primary client performs the real backup-and-sync election (with a16 * virtual authenticator) before its state is cloned: lofi is local-only by17 * design until the account opts in, so without the election there is nothing18 * to converge and the gate would fail for a reason that indicts no one.19 *20 * Without LOFI_E2E_BASE_URL (or without Chromium installed) it skips, so the21 * default suite stays fast and never launches a browser.22 */2324import type { BrowserScenarioPeer } from "@nzip/lofi/testing";2526function e2eBaseURL(): string | undefined {27 // The default suite runs without --allow-env; treat a denied read as "skip".28 try {29 return Deno.env.get("LOFI_E2E_BASE_URL") || undefined;30 } catch {31 return undefined;32 }33}3435// Runs inside the browser page, so it may only touch DOM APIs: the status line36// reports "N item(s) …" once the local store has hydrated.37const taskListReady = () => {38 const status = document.querySelector('[role="status"]');39 return status !== null && /item\(s\)/.test(status.textContent ?? "");40};4142const baseURL = e2eBaseURL();43if (baseURL === undefined) {44 Deno.test("two clients converge on concurrent offline task edits", () => {45 console.log(46 "skipping convergence example; set LOFI_E2E_BASE_URL to a synced deployment to run it",47 );48 });49} else {50 // Imported lazily so the default suite never loads Playwright.51 const { scenario, waitForReady, withVirtualAuthenticator } = await import("@nzip/lofi/testing");5253 const ready = (peer: BrowserScenarioPeer) =>54 waitForReady(peer.page, taskListReady, undefined, { description: `${peer.name} ready` });55 const addTask = async (peer: BrowserScenarioPeer, text: string) => {56 await peer.page.fill("#new-task", text);57 await peer.page.press("#new-task", "Enter");58 };59 const seeTask = (peer: BrowserScenarioPeer, text: string) =>60 peer.page.locator(".task", { hasText: text }).first().waitFor({61 state: "visible",62 timeout: 15_000,63 });6465 scenario.browser("two clients converge on concurrent offline task edits", {66 baseURL,67 // Shared identity clones the first client's state in memory after the68 // election, so both browser contexts act as the same synced account. The69 // election runs through the real account UI.70 identity: {71 mode: "shared",72 preparePrimary: async (client) => {73 const readyPage = () =>74 waitForReady(client.page, taskListReady, undefined, { description: "primary ready" });75 await readyPage();76 await withVirtualAuthenticator(client.page);77 await client.page.getByRole("button", { name: "Back up & enable sync" }).click();78 await client.page.locator('[aria-label="Recovery phrase"] li').first().waitFor({79 state: "visible",80 timeout: 30_000,81 });82 await client.page.getByRole("button", { name: "I saved my phrase — enable sync" }).click();83 await client.page.getByRole("heading", { name: "Backed up & syncing" }).waitFor({84 state: "visible",85 timeout: 30_000,86 });87 await readyPage();88 },89 },90 ready,91 // Failure artifacts stay value-free: counts and booleans only, no task text.92 snapshot: async (peer) => ({93 online: !peer.isOffline,94 items: await peer.page.locator(".task").count(),95 }),96 artifacts: { directory: "test-results" },97 }, async ({ alice, bob }) => {98 await alice.offline();99 await bob.offline();100 await addTask(alice, "first client task");101 await addTask(bob, "second client task");102 await seeTask(alice, "first client task");103 await seeTask(bob, "second client task");104 await alice.online();105 await bob.online();106107 // App-owned convergence: both tasks visible on both peers.108 for (const peer of [alice, bob]) {109 await seeTask(peer, "first client task");110 await seeTask(peer, "second client task");111 }112 });113}