@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/testingWriting 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.