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.