@samveys/testing

createTestApp, in-process HTTP test client, fake time, events fake, hook recording

Version
1.0.2Nov 5, 2026
Install
pnpm add @samveys/testing
Import
import { captureLogs, createTestApp, createTestClient, fakeEvents, fakeTime, recordHooks } from '@samveys/testing'

Function (6)

captureLogs Function

A logger that records each JSON line it writes, parsed, instead of printing it. Default level `debug`.

function captureLogs(level?: LogLevel | 'silent'): LogCapture;

Exported from packages/testing/src/logs.ts

createTestApp Function

Boots an app for a test: captured logs, recorded hooks and events, `container.swap()` that is undone on `close()`, and (with `@samveys/http`) an in-process HTTP client. Use `await using` or call `close()`. If startup throws, the app is shut down and the original error is rethrown.

function createTestApp(options?: TestAppOptions): Promise<TestApp>;

Exported from packages/testing/src/app.ts

createTestClient Function

An in-process test client over any `(Request) => Promise<Response>`, typically `kernel.fetch`. Responses are read once, so assertions are synchronous.

function createTestClient(fetchLike: (request: Request, options?: {

Exported from packages/testing/src/client.ts

fakeEvents Function

Records every `emitter.emit` call. Listeners still run unless `options.swallow` is true. Wraps `emit` on this emitter instance; `stop()` puts it back.

function fakeEvents(emitter: Emitter, options?: {

Exported from packages/testing/src/events.ts

fakeTime Function

Fakes timers and the clock on `node:test` `mock.timers` (or `options.mock.timers`, e.g. `t.mock.timers`). Starts at `2026-01-01T00:00:00.000Z` unless `now` is given. Use `using` or call `restore()`.

function fakeTime(options?: FakeTimeOptions): FakeTime;

Exported from packages/testing/src/time.ts

recordHooks Function

Records the arguments of every `on` emission (`registry.emit`) of any hook name, including hooks augmented by other packages. Wraps `emit` on this registry instance; `stop()` puts it back.

function recordHooks(registry: HookRegistry): HookRecorder & {

Exported from packages/testing/src/hooks.ts

Interface (11)

EventsFake Interface

No documentation comment.

interface EventsFake {
    /** Payloads emitted for `event`, in order. Listeners still run unless `swallow` was set. */
    emitted<K extends EventName>(event: K): readonly Events[K][];
    /** Throws an `AssertionError` naming the event when it was not emitted (or not with a matching payload). */
    assertEmitted<K extends EventName>(event: K, match?: (payload: Events[K]) => boolean): void;

Exported from packages/testing/src/contracts.ts

FakeTime Interface

No documentation comment.

interface FakeTime extends Disposable {
    /** Current fake time. */
    now(): Date;
    /** Advance by milliseconds or a duration string: `'500ms'`, `'30s'`, `'15m'`, `'2h'`, `'1d'`. Runs due timers. */
    advance(by: number | string): void;
    /** Jump to an absolute time without running timers in between. */
    set(to: Date | number | string): void;
    /** Run every pending timer. */
    runAll(): void;
    /** Restore the real timers and clock. Also runs on `using` disposal. */
    restore(): void;
}

Exported from packages/testing/src/contracts.ts

FakeTimeOptions Interface

No documentation comment.

interface FakeTimeOptions {
    /** Defaults to `2026-01-01T00:00:00.000Z`, so tests never depend on the real clock. */
    readonly now?: Date | number | string;
    /** Defaults to `setTimeout`, `setInterval`, `setImmediate`, `Date`. */
    readonly apis?: readonly ('setTimeout' | 'setInterval' | 'setImmediate' | 'Date' | 'scheduler.wait')[];
    /** Defaults to the global `mock` from `node:test`; pass `t.mock` to tie the fake to one test. */
    readonly mock?: {
        readonly timers: MockTimersLike;
    };
}

Exported from packages/testing/src/contracts.ts

HookRecorder Interface

Arguments of every `on` emission of `name` since recording started.

interface HookRecorder {
    calls<K extends HookName>(name: K): readonly HookArgs<K>[];
    count(name: HookName): number;
    clear(): void;
}

Exported from packages/testing/src/contracts.ts

LogCapture Interface

No documentation comment.

interface LogCapture {
    /** Every line written so far, parsed. */
    readonly records: readonly LogRecord[];
    /** The logger that writes into `records`. */
    readonly logger: Logger;
    /** Records at `level`, optionally only those whose `msg` contains `text`. */
    find(level: LogLevel, text?: string): readonly LogRecord[];
    clear(): void;
}

Exported from packages/testing/src/contracts.ts

MockTimersLike Interface

The `node:test` context's `mock.timers`, or the global `mock.timers`.

interface MockTimersLike {
    enable(options?: {
        readonly apis?: readonly string[];
        readonly now?: number | Date;
    }): void;
    tick(milliseconds?: number): void;
    setTime(milliseconds: number): void;
    runAll(): void;
    reset(): void;
}

Exported from packages/testing/src/contracts.ts

TestApp Interface

No documentation comment.

interface TestApp extends AsyncDisposable {
    readonly app: Application;
    readonly logs: LogCapture;
    /** Recording of every hook emission since the app was created. */
    readonly hooks: HookRecorder;
    /** Records `app.events` emissions. */
    readonly events: EventsFake;
    /** In-process client on `kernel.fetch`; created on first use. Needs `@samveys/http` installed. */
    readonly http: TestClient;
    /** The kernel behind `http`; created on first use. */
  …
Show the whole declaration (18 lines)
interface TestApp extends AsyncDisposable {
    readonly app: Application;
    readonly logs: LogCapture;
    /** Recording of every hook emission since the app was created. */
    readonly hooks: HookRecorder;
    /** Records `app.events` emissions. */
    readonly events: EventsFake;
    /** In-process client on `kernel.fetch`; created on first use. Needs `@samveys/http` installed. */
    readonly http: TestClient;
    /** The kernel behind `http`; created on first use. */
    kernel(): Promise<HttpKernel>;
    /** `container.swap` scoped to this test app; undone by `close()`. */
    swap: Application['container']['swap'];
    /** Fire a hook as the framework would (listeners run, results discarded). */
    emit<K extends HookName>(name: K, ...args: HookArgs<K>): Promise<void>;
    /** Shut the app down; returns the shutdown result. Idempotent. Also runs on `await using`. */
    close(): ReturnType<Application['shutdown']>;
}

Exported from packages/testing/src/contracts.ts

TestAppOptions Interface

No documentation comment.

interface TestAppOptions extends Omit<CreateAppOptions, 'loggerInstance'> {
    /** Plugins to install, exactly as in `samveys.config.ts`. */
    readonly plugins?: readonly PluginEntry[];
    /** Routes for the in-process HTTP kernel; added to the `http.router` binding before the kernel is built. */
    readonly routes?: (router: Router) => void;

Exported from packages/testing/src/contracts.ts

TestClient Interface

No documentation comment.

interface TestClient {
    get(path: string, options?: TestRequestOptions): Promise<TestResponse>;
    post(path: string, options?: TestRequestOptions): Promise<TestResponse>;
    put(path: string, options?: TestRequestOptions): Promise<TestResponse>;
    patch(path: string, options?: TestRequestOptions): Promise<TestResponse>;
    delete(path: string, options?: TestRequestOptions): Promise<TestResponse>;
    request(method: string, path: string, options?: TestRequestOptions): Promise<TestResponse>;
    /** Default headers for every later request from this client (e.g. an auth token). */
    withHeaders(headers: HeadersInput): TestClient;
}

Exported from packages/testing/src/contracts.ts

TestRequestOptions Interface

No documentation comment.

interface TestRequestOptions {
    readonly headers?: HeadersInput;
    readonly query?: Readonly<Record<string, string | number | boolean | readonly (string | number)[]>>;
    /** Sent as JSON with `content-type: application/json`. */
    readonly json?: unknown;
    /** Sent as `application/x-www-form-urlencoded`. */
    readonly form?: Readonly<Record<string, string>>;
    /** Raw body; set `content-type` yourself. */
    readonly body?: BodyInput;
    readonly signal?: AbortSignal;
  …
Show the whole declaration (16 lines)
interface TestRequestOptions {
    readonly headers?: HeadersInput;
    readonly query?: Readonly<Record<string, string | number | boolean | readonly (string | number)[]>>;
    /** Sent as JSON with `content-type: application/json`. */
    readonly json?: unknown;
    /** Sent as `application/x-www-form-urlencoded`. */
    readonly form?: Readonly<Record<string, string>>;
    /** Raw body; set `content-type` yourself. */
    readonly body?: BodyInput;
    readonly signal?: AbortSignal;
    /**
     * The client address the kernel sees (`ctx.clientIp`, trusted-proxy checks), as if the request came
     * from that socket peer. Default: none, like an in-process request.
     */
    readonly remoteAddress?: string;
}

Exported from packages/testing/src/contracts.ts

TestResponse Interface

A response with its body already read, so assertions are synchronous.

interface TestResponse {
    readonly status: number;
    readonly headers: Headers;
    readonly text: string;
    /** `JSON.parse(text)`; `undefined` when the body is empty. Throws when the body is not JSON. */
    json<T = unknown>(): T;
    /** The response as sent by the kernel (body already consumed). */
    readonly raw: Response;
    /** Throws an `AssertionError` with the status and body when `status` differs. */
    assertStatus(status: number): this;
  …
Show the whole declaration (16 lines)
interface TestResponse {
    readonly status: number;
    readonly headers: Headers;
    readonly text: string;
    /** `JSON.parse(text)`; `undefined` when the body is empty. Throws when the body is not JSON. */
    json<T = unknown>(): T;
    /** The response as sent by the kernel (body already consumed). */
    readonly raw: Response;
    /** Throws an `AssertionError` with the status and body when `status` differs. */
    assertStatus(status: number): this;
    /** Asserts `application/problem+json` with the given status and, when given, `SVS_E*` code. */
    assertProblem(status: number, code?: string): this;
    assertHeader(name: string, value?: string | RegExp): this;
    /** Deep-strict subset match of the JSON body. */
    assertJson(expected: unknown): this;
}

Exported from packages/testing/src/contracts.ts

Type (3)

BodyInput Type

The `body` field of `RequestInit`.

type BodyInput = NonNullable<RequestInit['body']>;

Exported from packages/testing/src/contracts.ts

HeadersInput Type

The `headers` argument of `new Headers()` (the DOM `HeadersInit` alias is not in `lib: esnext`).

type HeadersInput = ConstructorParameters<typeof Headers>[0];

Exported from packages/testing/src/contracts.ts

LogRecord Type

One parsed JSON log line.

type LogRecord = Readonly<Record<string, unknown>> & {
    readonly level: LogLevel;
    readonly msg: string;
};

Exported from packages/testing/src/contracts.ts