Skip to main content

Testing

import * as mod from "jsr:@nzip/lofi/testing";

The @nzip/lofi/testing public surface: Playwright-backed helpers for testing local-first behavior, including two-client fixtures, concurrent offline convergence, app-owned readiness waits, value-free failure artifacts, and a CDP virtual authenticator for headless WebAuthn flows.

BrowserTestClient

class

class BrowserTestClient {
constructor(name: ClientName, context: BrowserContext, page: Page, baseURL: string, createContext: (state?: MemoryStorageState) => Promise<BrowserContext>, redact: (value: string) => string, traceOnFailure: boolean);
get context(): BrowserContext;
get page(): Page;
get offline(): boolean;
get diagnostics(): readonly BrowserDiagnostic[];
startRecording(): Promise<void>;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
reloadPage(): Promise<Page>;
restartPage(): Promise<Page>;
restartClient(options: { preserveIdentity?: boolean }): Promise<Page>;
captureTrace(path: string): Promise<boolean>;
close(): Promise<void>;
}

One browser client (context + page) whose identity and IndexedDB stay in memory. Records redacted diagnostics and can go offline/online, restart, and capture a sanitized trace.

BrowserUnavailableError

class

class BrowserUnavailableError extends Error {
constructor(options?: ErrorOptions);
readonly name: string;
}

Thrown when Playwright's Chromium browser is not installed, with install guidance.

ConvergenceScenarioError

class

class ConvergenceScenarioError extends Error {
constructor(stage: string, options?: ErrorOptions);
readonly name: string;
}

Thrown when a convergence scenario fails, naming the stage that failed via stage.

createTwoClientFixture

function

async function createTwoClientFixture(options: TwoClientFixtureOptions): Promise<TwoClientFixture>

Launch (or reuse) a browser and build a TwoClientFixture: opens both clients at baseURL, applies the chosen identity mode with state kept in memory, and starts recording. Cleans up on any setup failure.

| Parameter | Description |

| --- | --- |

| options | The base URL, identity mode, and optional browser, context, and artifact settings. |

Returns: A ready fixture whose two clients are open at baseURL with identity applied.

Example

const fixture = await createTwoClientFixture({
baseURL,
identity: { mode: "shared", preparePrimary: (client) => ready(client) },
artifacts: { directory: "test-results" },
});
try {
await fixture.goOffline();
// ...apply edits on fixture.first and fixture.second, then converge...
} finally {
await fixture.close();
}

ReadinessError

class

class ReadinessError extends Error {
constructor(description: string, options?: ErrorOptions);
readonly name: string;
}

Thrown when the readiness predicate does not become true before the timeout.

runConcurrentOfflineConvergence

function

async function runConcurrentOfflineConvergence<Edit, Client extends OfflineTestClient>(fixture: OfflineTestFixture<Client>, scenario: ConcurrentOfflineScenario<Edit, Client>): Promise<void>

Coordinates the transport lifecycle while the app owns edits, assertions and conflict semantics. Both offline edits are started in the same microtask.

TwoClientFixture

class

class TwoClientFixture {
constructor(browser: Browser, ownsBrowser: boolean, first: BrowserTestClient, second: BrowserTestClient, artifacts: FailureArtifactOptions | undefined);
readonly clients: readonly [BrowserTestClient, BrowserTestClient];
get first(): BrowserTestClient;
get second(): BrowserTestClient;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
captureFailure(label: string, snapshot?: (client: BrowserTestClient) => Promise<ValueFreeState>): Promise<FailureArtifacts | undefined>;
close(): Promise<void>;
}

Coordinates a pair of BrowserTestClients sharing one browser, with helpers to take both clients offline/online and to capture redacted, value-free failure artifacts.

waitForReady

function

async function waitForReady<Argument>(page: Page, predicate: (argument: Argument) => boolean | Promise<boolean>, argument: Argument, options: ReadinessOptions): Promise<void>

Wait for an app-owned browser predicate. The predicate runs in the page and Playwright owns the timeout/polling, so tests do not need arbitrary sleeps.

| Parameter | Description |

| --- | --- |

| page | The Playwright page to poll. |

| predicate | An app-owned readiness check evaluated inside the page. |

| argument | A serializable value passed to the predicate in the page. |

| options | Optional description, timeout, and polling configuration. |

Returns: Resolves when the predicate becomes true; rejects with ReadinessError on timeout.

Example

import { waitForReady } from "@nzip/lofi/testing";

await waitForReady(client.page, () => document.querySelector(".task-list") !== null, undefined, {
description: "task list rendered",
});

withVirtualAuthenticator

function

async function withVirtualAuthenticator(page: Page, options: VirtualAuthenticatorOptions): Promise<VirtualAuthenticatorHandle>

Install a CDP virtual authenticator on page and return a handle that removes it on VirtualAuthenticatorHandle.dispose. Defaults model an internal, resident, user-verifying platform authenticator that auto-satisfies presence.

BrowserDiagnostic

interface

interface BrowserDiagnostic {
readonly client: ClientName;
readonly kind: "console" | "page-error" | "request-failed";
readonly level?: string;
readonly message: string;
readonly url?: string;
}

A redacted console, page-error, or failed-request record captured from a client.

ClientName

type

type ClientName = "first" | "second";

Identifies which of the fixture's two clients a value belongs to.

ConcurrentOfflineScenario

interface

interface ConcurrentOfflineScenario<Edit, Client extends OfflineTestClient = BrowserTestClient> {
readonly edits: readonly [Edit, Edit];
readonly timeoutMs?: number;
readonly ready: (client: Client, signal: AbortSignal) => Promise<void>;
readonly apply: (client: Client, edit: Edit, signal: AbortSignal) => Promise<void>;
readonly locallyApplied: (client: Client, edit: Edit, signal: AbortSignal) => Promise<void>;
readonly whilePending?: (fixture: OfflineTestFixture<Client>, signal: AbortSignal) => Promise<void>;
readonly converged: (fixture: OfflineTestFixture<Client>, signal: AbortSignal) => Promise<void>;
readonly snapshot?: (client: Client) => Promise<ValueFreeState>;
readonly failureLabel?: string;
}

App-owned definition of a concurrent-offline convergence test: the two edits plus the hooks that assert readiness, apply and locally verify each edit, and confirm convergence after reconnection.

FailureArtifactOptions

interface

interface FailureArtifactOptions {
readonly directory: string;
readonly secretValues?: readonly string[];
readonly mask?: (client: BrowserTestClient) => readonly Locator[];
}

Configures where and how redacted failure artifacts are written on capture.

FailureArtifacts

interface

interface FailureArtifacts {
readonly directory: string;
readonly files: readonly string[];
}

The directory and file paths produced by a failure capture.

IdentityOptions

type

type IdentityOptions = { readonly mode: "shared"; readonly preparePrimary: (client: BrowserTestClient) => Promise<void> } | { readonly mode: "isolated"; readonly prepare?: (client: BrowserTestClient) => Promise<void> };

How the two clients obtain identity: shared clones the primary's prepared state (in memory) into the second client, isolated prepares each client on its own.

OfflineTestClient

interface

interface OfflineTestClient {
readonly offline: boolean;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
}

Minimal client contract the convergence runner needs: offline state and toggles.

OfflineTestFixture

interface

interface OfflineTestFixture<Client extends OfflineTestClient> {
readonly clients: readonly [Client, Client];
readonly first: Client;
readonly second: Client;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
captureFailure(label: string, snapshot?: (client: Client) => Promise<ValueFreeState>): Promise<unknown>;
}

Minimal two-client fixture contract required to drive an offline scenario.

ReadinessOptions

interface

interface ReadinessOptions {
description?: string;
timeoutMs?: number;
polling?: "raf" | number;
}

Options controlling waitForReady's description, timeout, and polling.

SafeContextOptions

type

type SafeContextOptions = Omit<BrowserContextOptions, "storageState"> & { storageState?: never };

Playwright context options with storageState forbidden, so identity never leaves memory via an on-disk state file.

TwoClientFixtureOptions

interface

interface TwoClientFixtureOptions {
readonly baseURL: string;
readonly identity: IdentityOptions;
readonly browser?: Browser;
readonly context?: SafeContextOptions;
readonly artifacts?: FailureArtifactOptions;
readonly traceOnFailure?: boolean;
}

Options for constructing a TwoClientFixture via createTwoClientFixture.

ValueFreeState

type

type ValueFreeState =
| null
| boolean
| number
| { [key: string]: ValueFreeState }
| readonly ValueFreeState[];

A JSON-like value restricted to booleans, finite numbers, arrays, and plain objects. Strings are excluded so state snapshots record shape and counts but never user values or credentials.

VirtualAuthenticatorCredential

interface

interface VirtualAuthenticatorCredential {
readonly credentialId: string;
readonly isResidentCredential: boolean;
readonly rpId?: string;
readonly privateKey: string;
readonly userHandle?: string;
readonly signCount: number;
readonly largeBlob?: string;
}

Serializable CDP credential used to copy one virtual passkey between browser profiles.

VirtualAuthenticatorHandle

interface

interface VirtualAuthenticatorHandle {
readonly authenticatorId: string;
credentials(): Promise<VirtualAuthenticatorCredential[]>;
addCredential(credential: VirtualAuthenticatorCredential): Promise<void>;
clearCredentials(): Promise<void>;
dispose(): Promise<void>;
}

A handle to an installed virtual authenticator; call dispose to remove it.

VirtualAuthenticatorOptions

interface

interface VirtualAuthenticatorOptions {
readonly protocol?: "ctap2" | "u2f";
readonly transport?: "usb" | "nfc" | "ble" | "cable" | "internal";
readonly hasResidentKey?: boolean;
readonly hasUserVerification?: boolean;
readonly isUserVerified?: boolean;
readonly automaticPresenceSimulation?: boolean;
}

Options for withVirtualAuthenticator.