Documentation menu

@gyral/testing

Gyral test helpers: pure update stepping, command helpers, fake drivers, virtual time, SSR page mounting, server rendering in Vitest browser tests.

npm install @gyral/testing

Writing code? Start from working code, not from this page. Testing. Adapt the closest example and check it, then look a name up here when you are about to call it.

Generated from the package's type declarations. Entry points: @gyral/testing, @gyral/testing/vitest.

@gyral/testing

Functions

Name Summary
callsIn The element method calls among the commands (call(selector, method, ...args)), in order:
collectErrors Starts collecting every GyralError Gyral reports, so deliberate failures don't fail the test run.
commandsFor The commands addressed to driver (matched by name, so fakes and real drivers agree).
fakeDriver A recording driver for DOM-level tests: el.drivers = { http: fakeDriver(http) }.
focusTargetsIn The focus requests among the commands (focus(selector, options)), in order:
hydrated Waits until the page's Gyral components have rendered (see settled()): releases lazy islands if asked, then await settled().
initial Runs init without a DOM.
inputsFor The inputs of the commands addressed to driver; given a store, the messages the commands send() to it, in order (ADR 0013).
mount Creates component (a class from define() or a registered tag name), sets props, attrs, drivers and stores, appends it, and waits for its first render.
mountSsr Mounts server output: a full document (head + body) or a body fragment.
outputsIn The outputs the commands send to the parent component (emit, outputs<O>()), in order.
parse Runs the parser of intent name with input (only the fields the parser reads are needed: { value: '3' }, { key: 'Enter' }, { detail }).
reject The message a command produces if its driver fails with error (already typed, i.e.
resetDocumentStores Tests only: forget the document default so each test starts clean.
resolve The message a command produces if its driver succeeds with output.
run Folds messages through update, collecting every command (including init's).
step Feeds one message through update without a DOM.
stepStore Feeds one message through a store's update without an instance.
subscriptionsFor The commands spec.subscriptions asks to keep running for state and props (ADR 0025), without a DOM: subscriptionsFor(Clock.spec, { live: true }).
viewOf Calls a component's pure view without mounting, a DOM shim or consumer casts.
virtualTime Replaces timers and Date with a virtual clock (ADR 0006, "Testing").

callsIn

function callsIn(commands: ReadonlyArray<Command<unknown>>): readonly CallInput[];

The element method calls among the commands (call(selector, method, ...args)), in order:

expect(callsIn(next.commands)).toEqual([{ selector: 'video', method: 'play', args: [] }]);

collectErrors

function collectErrors(): CollectedErrors;

Starts collecting every GyralError Gyral reports, so deliberate failures don't fail the test run. Errors that aren't Gyral's are left alone. Call stop() when the test ends.

commandsFor

function commandsFor<I, M>(commands: ReadonlyArray<Command<M>>, driver: Driver<I, unknown> | Named): ReadonlyArray<Command<M> & {
    readonly input: I;
}>;

The commands addressed to driver (matched by name, so fakes and real drivers agree).

fakeDriver

function fakeDriver<I = unknown, O = unknown, E = unknown>(of: string | Driver<I, O, E>, optionsOrRun?: FakeOptions<I, O, E> | FakeRun<I, O>): FakeDriver<I, O, E>;

A recording driver for DOM-level tests: el.drivers = { http: fakeDriver(http) }. Pass a real driver to copy its name and concurrency, or just a name. Failures pass through unchanged unless you give toError, so rejectNext(error) takes the typed error. A function as the second argument answers every call (impl):

el.drivers = { 'url-sync': fakeDriver<string | null, void>('url-sync', (q) => void urls.push(q)) };

Fakes go into el.drivers and provideDrivers as they are, with no cast.

focusTargetsIn

function focusTargetsIn(commands: ReadonlyArray<Command<unknown>>): readonly FocusTarget[];

The focus requests among the commands (focus(selector, options)), in order:

expect(focusTargetsIn(next.commands)).toEqual([{ selector: '#results' }]);

hydrated

function hydrated(page: MountedSsr | ParentNode, options?: HydratedOptions): Promise<void>;

Waits until the page's Gyral components have rendered (see settled()): releases lazy islands if asked, then await settled(). Islands still deferred are left alone. It throws when a server-rendered custom element never upgraded (import its module, or list it in allowUndefined). Given a MountedSsr, it also throws when console errors/warnings or uncaught errors were recorded since mounting.

initial

function initial<S, M extends Tagged, P>(spec: ComponentSpec<S, M, P>, props?: P): Stepped<S, M>;

Runs init without a DOM. props defaults to {}.

inputsFor

function inputsFor<S, SM extends Tagged>(commands: ReadonlyArray<Command<unknown>>, store: Store<S, SM>): readonly SM[];
function inputsFor<I, M>(commands: ReadonlyArray<Command<M>>, driver: Driver<I, unknown> | Named): readonly I[];

The inputs of the commands addressed to driver; given a store, the messages the commands send() to it, in order (ADR 0013).

mount

function mount<K extends keyof HTMLElementTagNameMap>(tag: K, options?: MountOptions<HTMLElementTagNameMap[K]>): Promise<Mounted<HTMLElementTagNameMap[K]>>;
function mount<E extends HTMLElement>(component: Constructor<E> | string, options?: MountOptions<E>): Promise<Mounted<E>>;

Creates component (a class from define() or a registered tag name), sets props, attrs, drivers and stores, appends it, and waits for its first render.

mountSsr

function mountSsr(html: string, options?: MountSsrOptions): MountedSsr;

Mounts server output: a full document (head + body) or a body fragment. Call it before importing component modules so elements upgrade and hydrate like on a real page load.

  • Only <head> styles are applied: <style> inside Declarative Shadow DOM belongs to its shadow root and must not leak into the page.
  • The store seed, named <meta>s and the managed head are restored, as the browser would see them.

outputsIn

function outputsIn<C extends WithOutputs>(commands: ReadonlyArray<Command<unknown>>, component: C): ReadonlyArray<OutputsOf<C>>;
function outputsIn<O extends Tagged = Tagged>(commands: ReadonlyArray<Command<unknown>>): readonly O[];

The outputs the commands send to the parent component (emit, outputs<O>()), in order. Pass the component class to type them by its output union (OutputsOf<typeof Item>):

expect(outputsIn(next.commands, Item)).toEqual([{ _tag: 'Picked', id: 1 }]);

The outputs, typed by the union you name: outputsIn<Out>(commands).

parse

function parse<S, M extends Tagged, P, N extends string, K extends N>(component: SpecOf<S, M, P, N>, name: K, input?: Partial<IntentInput>, options?: ParseOptions<NoInfer<S>, NoInfer<P>>): Parsed<M, K> | IntentRejected | undefined | Promise<Parsed<M, K> | IntentRejected | undefined>;

Runs the parser of intent name with input (only the fields the parser reads are needed: { value: '3' }, { key: 'Enter' }, { detail }). Returns what the parser returns: a message, an IntentRejected, undefined (ignored or declined), or a promise of one.

reject

function reject<M>(command: Command<M>, error: unknown): M | undefined;

The message a command produces if its driver fails with error (already typed, i.e. after the driver's toError). undefined when the command has no onFailure.

resetDocumentStores

function resetDocumentStores(): void;

Tests only: forget the document default so each test starts clean.

resolve

function resolve<M>(command: Command<M>, output: unknown): M | undefined;

The message a command produces if its driver succeeds with output.

run

function run<S, M extends Tagged, P>(spec: ComponentSpec<S, M, P>, messages: ReadonlyArray<NoInfer<StepMessage<M, P>>>, options?: NoInfer<RunOptions<S, P>>): Ran<S, M>;

Folds messages through update, collecting every command (including init's).

step

function step<S, M extends Tagged, P>(spec: ComponentSpec<S, M, P>, state: NoInfer<S>, msg: NoInfer<StepMessage<M, P>>, props?: NoInfer<P>, stores?: readonly AnyStoreInstance[]): Stepped<S, M>;

Feeds one message through update without a DOM. Framework messages (PropsChanged, IntentRejected, StoreChanged, Hydrated, Errored) without a reducer leave state unchanged, as in the element. stores are the instances ctx.read() sees.

stepStore

function stepStore<S, M extends Tagged>(store: Store<S, M>, state: NoInfer<S>, msg: NoInfer<M>): Stepped<S, M>;

Feeds one message through a store's update without an instance.

subscriptionsFor

function subscriptionsFor<S, M extends Tagged, P>(spec: ComponentSpec<S, M, P>, state: NoInfer<S>, props?: NoInfer<P>): ReadonlyArray<Command<M>>;

The commands spec.subscriptions asks to keep running for state and props (ADR 0025), without a DOM: subscriptionsFor(Clock.spec, { live: true }). A spec without the field gives none. Assert their inputs with inputsFor, as for any commands.

viewOf

function viewOf<S, M extends Tagged, P, N extends string>(component: ComponentSpec<S, M, P, N> | GyralElementClass<S, M, P, Tagged, N>, state: NoInfer<S>, options?: ViewOptions<NoInfer<P>>): ChildValue;

Calls a component's pure view without mounting, a DOM shim or consumer casts. Use renderToString(viewOf(Component, state, { props, stores }), { dev: false }) for stable HTML assertions. Hooks, native validity, focus and hydration need real-browser tests.

virtualTime

function virtualTime(): VirtualTime;

Replaces timers and Date with a virtual clock (ADR 0006, "Testing"). It patches the platform rather than Gyral's runtime, so it covers drivers, debounces and retry delays alike and does not depend on how the interpreter is implemented. Microtasks stay real, so promises and the runtime keep working. Animation frames are faked only where they exist, so model and driver tests can use it in a Node project.

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
CollectedErrors The failures collected so far; stop() removes the listeners.
FakeCall One recorded run of a fake driver.
FakeDriver
FakeOptions
FakeRun What a fake answers each call with (FakeOptions.impl).
FocusTarget What a focus() command asks for: the selector and its options.
HydratedOptions
Mounted
MountedSsr
MountOptions
MountSsrOptions
ParseOptions What parse passes the parser besides the input.
Ran
RunOptions
StepMessage Any message update accepts: the component's own, or a framework message.
Stepped A reducer result, normalised: the new state plus the commands it asked for.
ViewOptions
VirtualTime Controls time in a test.
WithOutputs Anything carrying a component's output union: a define() class.

CollectedErrors

interface CollectedErrors {
    readonly errors: readonly GyralError[];
    /** Stops collecting; later failures are reported as usual. */
    stop(): void;
}

The failures collected so far; stop() removes the listeners.

FakeCall

interface FakeCall<I, O> {
    readonly input: I;
    readonly signal: AbortSignal;
    /** Settles this call (only for fakes without `impl`). */
    readonly resolve: (output: O) => void;
    readonly reject: (error: unknown) => void;
    /** Streams a value into this call (DriverContext.emit); ignored once settled or aborted. */
    readonly emit: (output: O) => void;
    readonly settled: boolean;
}

One recorded run of a fake driver.

FakeDriver

interface FakeDriver<I, O, E> extends Driver<I, O, E> {
    readonly calls: ReadonlyArray<FakeCall<I, O>>;
    /** Inputs of every call so far. */
    readonly inputs: readonly I[];
    /** Settles the oldest unsettled, non-aborted call. Throws if there is none. */
    resolveNext(output: O): void;
    rejectNext(error: unknown): void;
    /** Streams a value into the newest call that is still running. Throws if there is none. */
    emitNext(output: O): void;
}

FakeOptions

interface FakeOptions<I, O, E> {
    /** Answer calls immediately. Without it, calls wait for `resolveNext`/`rejectNext`. */
    readonly impl?: FakeRun<I, O>;
    readonly concurrency?: Concurrency;
    readonly toError?: (cause: unknown) => E;
}

FakeRun

type FakeRun<I, O> = (input: I, ctx: DriverContext<O>) => O | Promise<O>;

What a fake answers each call with (FakeOptions.impl).

FocusTarget

interface FocusTarget extends FocusOptions {
    readonly selector: string;
}

What a focus() command asks for: the selector and its options.

HydratedOptions

interface HydratedOptions {
    /**
     * Custom element tags allowed to stay undefined (never upgraded). `gyral-stores` is always
     * allowed: in the browser it is a plain scoping element.
     */
    readonly allowUndefined?: readonly string[];
    /**
     * Release every lazy island first (`defer-hydration` + `data-gyral-hydrate`), as if each
     * one's trigger had fired. Default `false`: islands keep waiting.
     */
    readonly releaseIslands?: boolean;
}

Mounted

interface Mounted<E extends HTMLElement> {
    readonly el: E;
    /** The first match in the element's shadow root (or light DOM), or null. */
    $(selector: string): HTMLElement | null;
    /** Every match in the element's shadow root (or light DOM). */
    $$(selector: string): HTMLElement[];
    /** Sets a control's value as typing would, fires `input` and `change`, then waits. */
    type(selector: string, text: string): Promise<void>;
    /** Clicks the first match, then waits. */
    click(selector: string): Promise<void>;
    /** Waits until rendering, commands and messages have settled (`settled()`). */
    settled(): Promise<void>;
    /** Removes the element (and its overrides with it). */
    dispose(): void;
}

MountedSsr

interface MountedSsr {
    /** The element holding the server body (Declarative Shadow DOM already parsed). */
    readonly root: HTMLElement;
    /** Console errors/warnings and uncaught errors seen since mounting (see `hydrated`). */
    readonly problems: readonly string[];
    readonly unmount: () => void;
}

MountOptions

interface MountOptions<E extends HTMLElement> {
    /** Props, set as properties before the element connects. */
    readonly props?: PropsOf<E> & Readonly<Record<string, unknown>>;
    /** Attributes, set before the element connects. */
    readonly attrs?: Readonly<Record<string, string>>;
    /** Driver substitutions by name (`el.drivers`), set before the element connects. */
    readonly drivers?: DriverOverrides;
    /** Store instances by name (`el.stores`), set before the element connects. */
    readonly stores?: StoreOverrides;
    /** Query the element's light DOM instead of its shadow root (for `shadow: false`). */
    readonly light?: boolean;
    /** Where to append the element. Default `document.body`. */
    readonly parent?: ParentNode;
}

MountSsrOptions

interface MountSsrOptions {
    /** Restore the page-level store seed (`data-gyral-stores`). Default `true`. */
    readonly stores?: boolean;
    /** Copy `<meta name=…>` elements from the head (e.g. a CSRF token). Default `true`. */
    readonly metas?: boolean;
    /**
     * Copy the managed head (ADR 0019: elements marked `data-gyral-head`) right after the
     * document's `<title>`, as the server wrote it, so a test can check that the client's first
     * `setHead()` adopts it without writing. Default `true`.
     */
    readonly head?: boolean;
}

ParseOptions

interface ParseOptions<S, P> {
    /** Props as the component sees them. Default `{}`. */
    readonly props?: P;
    /** The state when the event fires. Default: the component's initial state for `props`. */
    readonly state?: S;
    /** Store instances `ctx.read` resolves (ADR 0013). */
    readonly stores?: readonly AnyStoreInstance[];
}

What parse passes the parser besides the input.

Ran

interface Ran<S, M> extends Stepped<S, M> {
    /** State after each message, in order. */
    readonly states: readonly S[];
}

RunOptions

interface RunOptions<S, P> {
    readonly props?: P;
    /** Store instances `ctx.read()` sees. */
    readonly stores?: readonly AnyStoreInstance[];
    /** Start from this state instead of `init(props)` (whose commands are then skipped). */
    readonly state?: S;
}

StepMessage

type StepMessage<M, P> = M | PropsChanged<P> | IntentRejected | StoreChanged | Hydrated | Errored;

Any message update accepts: the component's own, or a framework message.

Stepped

interface Stepped<S, M> {
    readonly state: S;
    readonly commands: ReadonlyArray<Command<M>>;
}

A reducer result, normalised: the new state plus the commands it asked for.

ViewOptions

interface ViewOptions<P> {
    readonly props?: P;
    readonly stores?: readonly AnyStoreInstance[];
}

VirtualTime

interface VirtualTime {
    /** Advances the clock, running due timers and the promise work between them. */
    advance(ms: number): Promise<void>;
    /** Runs every pending timer (and timers they schedule) until none remain. */
    runAll(): Promise<void>;
    /** Current virtual time in ms (`Date.now()` while installed). */
    now(): number;
    restore(): void;
}

Controls time in a test. Always restore() (for example in afterEach).

WithOutputs

interface WithOutputs {
    /** Type-only, as on `GyralElementClass`. */
    readonly outputs?: Tagged;
}

Anything carrying a component's output union: a define() class.

@gyral/testing/vitest

Functions

Name Summary
renderOnServer The browser command: browser: { commands: { renderOnServer } } in vitest.config.ts, then in a browser test:

renderOnServer

const renderOnServer: BrowserCommand<[ServerRenderRequest], string>

The browser command: browser: { commands: { renderOnServer } } in vitest.config.ts, then in a browser test:

import { commands } from 'vitest/browser';
const page = mountSsr(await commands.renderOnServer({ module: '../src/counter.ts',
  export: 'Counter', props: { start: 3 } }));
await import('../src/counter.ts'); // hydrates in place
await hydrated(page);

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
ServerRenderRequest What renderOnServer renders.

ServerRenderRequest

interface ServerRenderRequest {
    /**
     * The module to load on the server: relative to the test file (`'../src/counter.ts'`), an
     * absolute path, a path from the project root (`'/src/app.ts'`) or a package specifier.
     */
    readonly module: string;
    /**
     * The export to render (default `'default'`):
     * - a `define()` class: renders its element, with `props` set as properties;
     * - a function: called with `props`; it may return (or resolve to) a template result, an
     *   HTML string or a `Response` (a full page from `@gyral/ssr`'s `renderPage`, an app's
     *   `fetch` handler…);
     * - a template result: rendered as is.
     */
    readonly export?: string;
    /** Props for a component class, or the argument of a function export. */
    readonly props?: Readonly<Record<string, unknown>>;
    /**
     * Development markers and checks (`render`'s `dev` option). Default: the build of core the
     * server loads (development in Vitest).
     */
    readonly dev?: boolean;
}

What renderOnServer renders. Everything is sent from the browser, so it must be JSON.