clone

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

feat/141-effect-library
lofi / package / testing / readiness.ts
readiness.ts 3.9K
1import type { Page } from "playwright";23/** Options controlling {@link waitForReady}'s description, timeout, and polling. */4export interface ReadinessOptions {5 /** A useful name for the condition, included when Playwright times out. */6 description?: string;7 /** Defaults to 10 seconds. */8 timeoutMs?: number;9 /**10 * `"raf"` re-checks on every animation frame; a number re-checks at that11 * fixed interval in milliseconds.12 * @default "raf"13 */14 polling?: "raf" | number;15}1617/** Thrown when the readiness predicate does not become true before the timeout. */18export class ReadinessError extends Error {19 /** Always `"ReadinessError"`. */20 override readonly name = "ReadinessError";2122 /**23 * Builds the error, embedding the condition that failed to become ready.24 * @param description the readiness condition that timed out, named in the message25 * @param options standard error options, e.g. a `cause` from the underlying timeout26 */27 constructor(description: string, options?: ErrorOptions) {28 super(`Page did not become ready: ${description}`, options);29 }30}3132function isReadinessOptions(value: unknown): value is ReadinessOptions | undefined {33 if (value === undefined) return true;34 if (typeof value !== "object" || value === null || Array.isArray(value)) return false;35 return Object.keys(value).every(36 (key) => key === "description" || key === "timeoutMs" || key === "polling",37 );38}3940/**41 * Wait for an app-owned browser predicate that takes no argument. The predicate42 * runs in the page and Playwright owns the timeout/polling, so tests do not43 * need arbitrary sleeps.44 *45 * @example46 * ```ts47 * import { waitForReady } from "@nzip/lofi/testing";48 *49 * await waitForReady(client.page, () => document.querySelector(".task-list") !== null, {50 * description: "task list rendered",51 * });52 * ```53 *54 * @param page The Playwright page to poll.55 * @param predicate An app-owned readiness check evaluated inside the page.56 * @param options Optional description, timeout, and polling configuration.57 * @returns Resolves when the predicate becomes true; rejects with {@link ReadinessError} on timeout.58 */59export function waitForReady(60 page: Page,61 predicate: () => boolean | Promise<boolean>,62 options?: ReadinessOptions,63): Promise<void>;64/**65 * Wait for an app-owned browser predicate, passing it a serialized argument.66 *67 * @param page The Playwright page to poll.68 * @param predicate An app-owned readiness check evaluated inside the page.69 * @param argument A serializable value passed to the predicate in the page.70 * @param options Optional description, timeout, and polling configuration.71 * @returns Resolves when the predicate becomes true; rejects with {@link ReadinessError} on timeout.72 */73export function waitForReady<Argument>(74 page: Page,75 predicate: (argument: Argument) => boolean | Promise<boolean>,76 argument: Argument,77 options?: ReadinessOptions,78): Promise<void>;79export async function waitForReady<Argument>(80 page: Page,81 predicate: (argument: Argument) => boolean | Promise<boolean>,82 argumentOrOptions?: Argument | ReadinessOptions,83 maybeOptions?: ReadinessOptions,84): Promise<void> {85 // A three-argument call carries options in third position only for the86 // zero-parameter overload; a predicate argument otherwise rides there.87 const optionsInThird = maybeOptions === undefined && predicate.length === 0 &&88 isReadinessOptions(argumentOrOptions);89 const options = (optionsInThird ? argumentOrOptions as ReadinessOptions : maybeOptions) ?? {};90 const argument = optionsInThird ? undefined : argumentOrOptions;91 const description = options.description ?? "application readiness predicate";92 try {93 // Playwright internally maps serializable values to its Unboxed<T> type.94 await page.waitForFunction(predicate as never, argument as never, {95 polling: options.polling ?? "raf",96 timeout: options.timeoutMs ?? 10_000,97 });98 } catch (error) {99 throw new ReadinessError(description, { cause: error });100 }101}