/** * Standing in for the Rust side. * * The frontend is an ordinary web page; the only thing it needs from Tauri is * the IPC bridge. Installing a fake one before the page loads lets the real * components run in a real browser with no native build, no window, and no * system permissions. * * This reaches for `window.__TAURI_INTERNALS__`, which is what * `@tauri-apps/api`'s own `mockIPC` sets. It is deliberately the only place in * the tests that knows that, so a Tauri upgrade that moves it breaks here and * nowhere else. */ import type { Page } from "@playwright/test"; import type { GraphRow } from "../../src/lib/bindings"; export type MockOptions = { /** Every row the fake backend is willing to hand over. */ rows: GraphRow[]; /** Make `open_repository` reject, to exercise the failure path. */ failToOpen?: string; }; /** * Install the fake bridge. Must be called before navigating. * * The fake honours the budget the way the real backend does: it sends what it * was asked for and no more, so a test that never scrolls really does see only * the first batch. */ export async function mockTauri(page: Page, options: MockOptions): Promise { await page.addInitScript((serialised: MockOptions) => { const all = serialised.rows; let sent = 0; let channel: { onmessage: (event: unknown) => void } | undefined; const deliver = (count: number) => { const batch = all.slice(sent, sent + count); if (batch.length > 0) { channel?.onmessage({ event: "rows", data: { start: sent, rows: batch } }); sent += batch.length; } if (sent >= all.length) { channel?.onmessage({ event: "exhausted", data: { total: sent, degraded: false } }); } }; // Recorded so tests can assert on what the frontend actually asked for. const calls: { command: string; args: Record }[] = []; (window as unknown as { __calls: typeof calls }).__calls = calls; (window as unknown as { __TAURI_INTERNALS__: unknown }).__TAURI_INTERNALS__ = { transformCallback: () => Math.random(), invoke: async (command: string, args: Record) => { calls.push({ command, args: { ...args, channel: undefined } }); switch (command) { case "open_repository": { if (serialised.failToOpen) { throw { kind: "git", message: serialised.failToOpen }; } return { id: "/demo/.git", summary: { gitDir: "/demo/.git", workdir: "/demo", isBare: false, head: { state: "branch", name: "refs/heads/main", shorthand: "main", id: "abc1234def5678", }, }, }; } case "stream_graph": { channel = args.channel as { onmessage: (event: unknown) => void }; sent = 0; deliver(args.budget as number); return null; } case "request_more_rows": { deliver(args.rows as number); return null; } default: return null; } }, }; }, options); } /** * Open a repository through the UI, the way a person would. * * Addressed by role rather than by label: the sidebar landmark is also named * "Repository", so a label lookup alone matches two different things. */ export async function openRepository(page: Page, path = "/demo"): Promise { await page.getByRole("textbox", { name: "Repository" }).fill(path); await page.getByRole("button", { name: "Open" }).click(); } /** Every command the frontend has invoked, in order. */ export function invocations(page: Page): Promise<{ command: string }[]> { return page.evaluate(() => (window as unknown as { __calls: { command: string }[] }).__calls); } /** * The commit rows. * * Scoped to `main`, because the ref sidebar is a list too — an unscoped * `listitem` query quietly counts four extra rows that are not commits. */ export function commitRows(page: Page) { return page.getByRole("main").getByRole("listitem"); }