@cloudflare/vitest-pool-workers
Version:
Workers Vitest integration for writing Vitest unit and integration tests that run inside the Workers runtime
679 lines (642 loc) • 24.9 kB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module "cloudflare:test" {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ProvidedEnv {}
/**
* 2nd argument passed to modules-format exported handlers. Contains bindings
* configured in top-level `miniflare` pool options. To configure the type
* of this value, use an ambient module type:
*
* ```ts
* declare module "cloudflare:test" {
* interface ProvidedEnv {
* NAMESPACE: KVNamespace;
* }
*
* // ...or if you have an existing `Env` type...
* interface ProvidedEnv extends Env {}
* }
* ```
*/
export const env: ProvidedEnv;
/**
* Service binding to the default export defined in the `main` worker. Note
* this `main` worker runs in the same isolate/context as tests, so any global
* mocks will apply to it too.
*/
export const SELF: Fetcher;
/**
* Declarative interface for mocking outbound `fetch()` requests. Deactivated
* by default and reset before running each test file. Only mocks `fetch()`
* requests for the current test runner worker. Auxiliary workers should mock
* `fetch()`es with the Miniflare `fetchMock`/`outboundService` options.
*/
export const fetchMock: MockAgent;
/**
* Runs `callback` inside the Durable Object pointed-to by `stub`'s context.
* Conceptually, this temporarily replaces your Durable Object's `fetch()`
* handler with `callback`, then sends a request to it, returning the result.
* This can be used to call/spy-on Durable Object instance methods or seed/get
* persisted data. Note this can only be used with `stub`s pointing to Durable
* Objects defined in the `main` worker.
*/
import type * as Rpc from "cloudflare:workers";
export function runInDurableObject<
O extends DurableObject | Rpc.DurableObject,
R,
>(
stub: DurableObjectStub<O>,
callback: (instance: O, state: DurableObjectState) => R | Promise<R>
): Promise<R>;
/**
* Immediately runs and removes the Durable Object pointed-to by `stub`'s
* alarm if one is scheduled. Returns `true` if an alarm ran, and `false`
* otherwise. Note this can only be used with `stub`s pointing to Durable
* Objects defined in the `main` worker.
*/
export function runDurableObjectAlarm(
stub: DurableObjectStub
): Promise<boolean /* ran */>;
/**
* Gets the IDs of all objects that have been created in the `namespace`.
* Respects `isolatedStorage` if enabled, i.e. objects created in a different
* test won't be returned.
*/
export function listDurableObjectIds(
namespace: DurableObjectNamespace
): Promise<DurableObjectId[]>;
/**
* Creates an instance of `ExecutionContext` for use as the 3rd argument to
* modules-format exported handlers.
*/
export function createExecutionContext(): ExecutionContext;
/**
* Waits for all `ExecutionContext#waitUntil()`ed `Promise`s to settle. Only
* accepts `ExecutionContext`s returned by `createExecutionContext()` or
* `EventContext`s return by `createPagesEventContext()`.
*/
export function waitOnExecutionContext(
ctx: ExecutionContext | EventContext<ProvidedEnv, string, any>
): Promise<void>;
/**
* Creates an instance of `ScheduledController` for use as the 1st argument to
* modules-format `scheduled()` exported handlers.
*/
export function createScheduledController(
options?: FetcherScheduledOptions
): ScheduledController;
/**
* Creates an instance of `MessageBatch` for use as the 1st argument to
* modules-format `queue()` exported handlers.
*/
export function createMessageBatch<Body = unknown>(
queueName: string,
messages: ServiceBindingQueueMessage<Body>[]
): MessageBatch<Body>;
/**
* Gets the ack/retry state of messages in the `MessageBatch`, and waits for
* all `ExecutionContext#waitUntil()`ed `Promise`s to settle. Only accepts
* instances of `MessageBatch` returned by `createMessageBatch()`, and
* instances of `ExecutionContext` returned by `createExecutionContext()`.
*/
export function getQueueResult(
batch: MessageBatch,
ctx: ExecutionContext
): Promise<FetcherQueueResult>;
export interface D1Migration {
name: string;
queries: string[];
}
/**
* Applies all un-applied `migrations` to database `db`, recording migrations
* state in the `migrationsTableName` table. `migrationsTableName` defaults to
* `d1_migrations`. Call the `readD1Migrations()` function from the
* `@cloudflare/vitest-pool-workers/config` package inside Node.js to get the
* `migrations` array.
*/
export function applyD1Migrations(
db: D1Database,
migrations: D1Migration[],
migrationsTableName?: string
): Promise<void>;
/**
* Creates an introspector for a specific Workflow instance, used to
* modify its behavior and await outcomes.
* This is the primary entry point for testing individual Workflow instances.
*
* @param workflow - The Workflow binding, e.g., `env.MY_WORKFLOW`.
* @param instanceId - The known ID of the Workflow instance to target.
* @returns A `WorkflowInstanceIntrospector` to control the instance's behavior.
*
* @remarks
* ### Dispose
*
* The introspector must be disposed after the test to remove mocks and release
* resources. This can be handled in two ways:
*
* 1. **Implicit dispose**: With the `await using` syntax.
* `await using instance = await introspectWorkflowInstance(...)`
*
* 2. **Explicit dispose**: Manually call `await instance.dispose()` at the end of the
* test.
*
* @example
* // Full test of a Workflow instance using implicit dispose
* it("should disable all sleeps and complete", async () => {
* // 1. CONFIGURATION
* // `await using` ensures .dispose() is automatically called at the end of the block.
* await using instance = await introspectWorkflowInstance(env.MY_WORKFLOW, "123456");
*
* await instance.modify(async (m) => {
* await m.disableSleeps();
* });
*
* // 2. EXECUTION
* await env.MY_WORKFLOW.create({ id: "123456" });
*
* // 3. ASSERTION
* await instance.waitForStatus("complete");
*
* // 4. DISPOSE is implicit and automatic here.
* });
*/
export function introspectWorkflowInstance(
workflow: Workflow,
instanceId: string
): Promise<WorkflowInstanceIntrospector>;
/**
* Provides methods to control a single Workflow instance.
*/
export interface WorkflowInstanceIntrospector {
/**
* Applies modifications to the Workflow instance's behavior.
* Takes a callback function to apply modifications.
*
* @param fn - An async callback that receives a `WorkflowInstanceModifier` object.
* @returns The `WorkflowInstanceIntrospector` instance for chaining.
*/
modify(
fn: (m: WorkflowInstanceModifier) => Promise<void>
): Promise<WorkflowInstanceIntrospector>;
/**
* Waits for a specific step to complete and return a result.
* If the step has already completed, this promise resolves immediately.
*
* @param step - An object specifying the step `name` and optional `index` (1-based).
* If multiple steps share the same name, `index` targets a specific one.
* Defaults to the first step found (`index: 1`).
* @returns A promise that resolves with the step's result,
* or rejects with an error if the step fails.
*/
waitForStepResult(step: { name: string; index?: number }): Promise<unknown>;
/**
* Waits for the Workflow instance to reach a specific InstanceStatus status
* (e.g., 'running', 'complete').
* If the instance is already in the target status, this promise resolves immediately.
* Throws an error if the Workflow instance reaches a finite state
* (e.g., complete, errored) that is different from the target status.
*
* @param status - The target `InstanceStatus` to wait for.
*/
waitForStatus(status: InstanceStatus["status"]): Promise<void>;
/**
* Disposes the Workflow instance introspector.
*
* This is crucial for ensuring test isolation by preventing state from
* leaking between tests. It should be called at the end of each test.
*/
dispose(): Promise<void>;
/**
* An alias for {@link dispose} to support automatic disposal with the `using` keyword.
*
* @see {@link dispose}
* @example
* ```ts
* it('my workflow test', async () => {
* await using instance = await introspectWorkflowInstance(env.WORKFLOW, "123456");
*
* // ... your test logic ...
*
* // .dispose() is automatically called here at the end of the scope
* });
* ```
*/
[Symbol.asyncDispose](): Promise<void>;
}
/**
* Provides methods to mock or alter the behavior of a Workflow instance's
* steps, events, and sleeps.
*/
interface WorkflowInstanceModifier {
/**
* Disables sleeps, causing `step.sleep()` and `step.sleepUntil()` to
* resolve immediately.
*
* @example Disable all sleeps:
* ```ts
* await instance.modify(m => {
* m.disableSleeps();
* });
* ```
*
* @example Disable a specific set of sleeps by their step names:
* ```ts
* await instance.modify(m => {
* m.disableSleeps([{ name: "sleep1" }, { name: "sleep5" }, { name: "sleep7" }]);
* });
* ```
*
* @param steps - Optional array of specific steps to disable sleeps for.
* If omitted, **all sleeps** in the Workflow will be disabled.
* A step is an object specifying the step `name` and optional `index` (1-based).
* If multiple steps share the same name, `index` targets a specific one.
* Defaults to the first step found (`index: 1`).
*/
disableSleeps(steps?: { name: string; index?: number }[]): Promise<void>;
/**
* Mocks the result of a `step.do()`, causing it to return a specified
* value instantly without executing the step's actual implementation.
*
* If called multiple times for the same step, an error will be thrown.
*
* @param step - An object specifying the step `name` and optional `index` (1-based).
* If multiple steps share the same name, `index` targets a specific one.
* Defaults to the first step found (`index: 1`).
* @param stepResult - The mock value to be returned by the step.
*
* @example Mock the result of the third step named "fetch-data":
* ```ts
* await instance.modify(m => {
* m.mockStepResult(
* { name: "fetch-data", index: 3 },
* { success: true, data: [1, 2, 3] }
* );
* });
* ```
*/
mockStepResult(
step: { name: string; index?: number },
stepResult: unknown
): Promise<void>;
/**
* Forces a `step.do()` to throw an error, simulating a failure without
* executing the step's actual implementation. Useful for testing retry logic
* and error handling.
*
* @example Mock a step that errors 3 times before succeeding:
* ```ts
* // This example assumes the "fetch-data" step is configured with at least 3 retries.
* await instance.modify(m => {
* m.mockStepError(
* { name: "fetch-data" },
* new Error("Failed!"),
* 3
* );
* m.mockStepResult(
* { name: "fetch-data" },
* { success: true, data: [1, 2, 3] }
* );
* });
* ```
*
* @param step - An object specifying the step `name` and optional `index` (1-based).
* If multiple steps share the same name, `index` targets a specific one.
* Defaults to the first step found (`index: 1`).
* @param error - The `Error` object to be thrown.
* @param times - Optional number of times to throw the error. If a step has
* retries configured, it will fail this many times before potentially
* succeeding on a subsequent attempt. If omitted, it will throw on **every attempt**.
*/
mockStepError(
step: { name: string; index?: number },
error: Error,
times?: number
): Promise<void>;
/**
* Forces a `step.do()` to fail by timing out immediately, without executing
* the step's actual implementation. Default step timeout is 10 minutes.
*
* @example Mock a step that times out 3 times before succeeding:
* ```ts
* // This example assumes the "fetch-data" step is configured with at least 3 retries.
* await instance.modify(m => {
* m.forceStepTimeout(
* { name: "fetch-data" },
* 3
* );
* m.mockStepResult(
* { name: "fetch-data" },
* { success: true, data: [1, 2, 3] }
* );
* });
* ```
*
* @param step - An object specifying the step `name` and optional `index` (1-based).
* If multiple steps share the same name, `index` targets a specific one.
* Defaults to the first step found (`index: 1`).
* @param times - Optional number of times the step will time out. Useful for
* testing retry logic. If omitted, it will time out on **every attempt**.
*/
forceStepTimeout(step: { name: string; index?: number }, times?: number);
/**
* Sends a mock event to the Workflow instance. This causes a `step.waitForEvent()`
* to resolve with the provided payload, as long as the step's timeout has not
* yet expired. Default event timeout is 24 hours.
*
* @example Mock a step event:
* ```ts
* await instance.modify(m => {
* m.mockEvent(
* { type: "user-approval", payload: { approved: true } },
* );
* ```
*
* @param event - The event to send, including its `type` and `payload`.
*/
mockEvent(event: { type: string; payload: unknown }): Promise<void>;
/**
* Forces a `step.waitForEvent()` to time out instantly, causing the step to fail.
* This simulates a scenario where an expected event never arrives.
* Default event timeout is 24 hours.
*
* @example Mock a step to time out:
* ```ts
* await instance.modify(m => {
* m.forceEventTimeout(
* { name: "user-approval" },
* );
* ```
*
* @param step - An object specifying the step `name` and optional `index` (1-based).
* If multiple steps share the same name, `index` targets a specific one.
* Defaults to the first step found (`index: 1`).
*/
forceEventTimeout(step: { name: string; index?: number }): Promise<void>;
}
/**
* Creates an **introspector** for a Workflow, where instance IDs are unknown
* beforehand. This allows for defining modifications that will apply to
* **all subsequently created instances**.
*
* This is the primary entry point for testing Workflow instances where the id
* is unknown before their creation.
*
* @param workflow - The Workflow binding, e.g., `env.MY_WORKFLOW`.
* @returns A `WorkflowIntrospector` to control the instances behavior.
*
* @remarks
* ### Dispose
*
* The introspector must be disposed after the test to remove mocks and release
* resources. This can be handled in two ways:
*
* 1. **Implicit dispose**: With the `await using` syntax.
* `await using introspector = await introspectWorkflow(...)`
*
* 2. **Explicit dispose**: Manually call `await introspector.dispose()` at the end of the
* test.
*
* @example
* ```ts
* // Full test of a Workflow instance using implicit dispose
* it("should disable all sleeps and complete", async () => {
* // 1. CONFIGURATION
* await using introspector = await introspectWorkflow(env.MY_WORKFLOW);
* await introspector.modifyAll(async (m) => {
* await m.disableSleeps();
* });
*
* // 2. EXECUTION
* await env.MY_WORKFLOW.create();
*
* // 3. ASSERTION
* const instances = introspector.get();
* for(const instance of instances) {
* await instance.waitForStatus("complete");
* }
*
* // 4. DISPOSE is implicit and automatic here.
* });
* ```
*/
export function introspectWorkflow(
workflow: Workflow
): Promise<WorkflowIntrospector>;
/**
* Provides methods to control all instances created by a Worflow.
*/
export interface WorkflowIntrospector {
/**
* Applies modifications to all Workflow instances created after calling
* `introspectWorkflow`. Takes a callback function to apply modifications.
*
* @param fn - An async callback that receives a `WorkflowInstanceModifier` object.
*/
modifyAll(
fn: (m: WorkflowInstanceModifier) => Promise<void>
): Promise<void>;
/**
* Returns all `WorkflowInstanceIntrospector`s from Workflow instances
* created after calling `introspectWorkflow`.
*/
get(): WorkflowInstanceIntrospector[];
/**
*
* Disposes the introspector and every `WorkflowInstanceIntrospector` from Workflow
* instances created after calling `introspectWorkflow`.
*
* This function is essential for test isolation, ensuring that results from one
* test do not leak into the next. It should be called at the end or after each test.
*
* **Note:** After dispose, `introspectWorkflow()` must be called again to begin
* a new introspection.
*
*/
dispose(): Promise<void>;
/**
* An alias for {@link dispose} to support automatic disposal with the `using` keyword.
* This is an alternative to calling `dispose()` in an `afterEach` hook.
*
* @see {@link dispose}
* @example
* it('my workflow test', async () => {
* await using workflowIntrospector = await introspectWorkflow(env.WORKFLOW);
*
* // ... your test logic ...
*
* // .dispose() is automatically called here at the end of the scope
* });
*/
[Symbol.asyncDispose](): Promise<void>;
}
// Only require `params` and `data` to be specified if they're non-empty
interface EventContextInitBase {
request: Request<unknown, IncomingRequestCfProperties>;
functionPath?: string;
next?(request: Request): Response | Promise<Response>;
}
type EventContextInitParams<Params extends string> = [Params] extends [never]
? { params?: Record<string, never> }
: { params: Record<Params, string | string[]> };
type EventContextInitData<Data> =
Data extends Record<string, never> ? { data?: Data } : { data: Data };
type EventContextInit<E extends EventContext<any, any, any>> =
E extends EventContext<any, infer Params, infer Data>
? EventContextInitBase &
EventContextInitParams<Params> &
EventContextInitData<Data>
: never;
/**
* Creates an instance of `EventContext` for use as the argument to Pages
* Functions.
*/
export function createPagesEventContext<
F extends PagesFunction<ProvidedEnv, string, any>,
>(init: EventContextInit<Parameters<F>[0]>): Parameters<F>[0];
// Taken from `undici` (https://github.com/nodejs/undici/tree/main/types) with
// no dependency on `@types/node` and with unusable functions removed
//
// MIT License
//
// Copyright (c) Matteo Collina and Undici contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
type IncomingHttpHeaders = Record<string, string | string[] | undefined>;
type Buffer = Uint8Array;
/** The scope associated with a mock dispatch. */
abstract class MockScope<TData extends object = object> {
/** Delay a reply by a set amount of time in ms. */
delay(waitInMs: number): MockScope<TData>;
/** Persist the defined mock data for the associated reply. It will return the defined mock data indefinitely. */
persist(): MockScope<TData>;
/** Define a reply for a set amount of matching requests. */
times(repeatTimes: number): MockScope<TData>;
}
/** The interceptor for a Mock. */
abstract class MockInterceptor {
/** Mock an undici request with the defined reply. */
reply<TData extends object = object>(
replyOptionsCallback: MockInterceptor.MockReplyOptionsCallback<TData>
): MockScope<TData>;
reply<TData extends object = object>(
statusCode: number,
data?:
| TData
| Buffer
| string
| MockInterceptor.MockResponseDataHandler<TData>,
responseOptions?: MockInterceptor.MockResponseOptions
): MockScope<TData>;
/** Mock an undici request by throwing the defined reply error. */
replyWithError<TError extends Error = Error>(error: TError): MockScope;
/** Set default reply headers on the interceptor for subsequent mocked replies. */
defaultReplyHeaders(headers: IncomingHttpHeaders): MockInterceptor;
/** Set default reply trailers on the interceptor for subsequent mocked replies. */
defaultReplyTrailers(trailers: Record<string, string>): MockInterceptor;
/** Set automatically calculated content-length header on subsequent mocked replies. */
replyContentLength(): MockInterceptor;
}
namespace MockInterceptor {
/** MockInterceptor options. */
export interface Options {
/** Path to intercept on. */
path: string | RegExp | ((path: string) => boolean);
/** Method to intercept on. Defaults to GET. */
method?: string | RegExp | ((method: string) => boolean);
/** Body to intercept on. */
body?: string | RegExp | ((body: string) => boolean);
/** Headers to intercept on. */
headers?: // eslint-disable-next-line unused-imports/no-unused-vars
| Record<string, string | RegExp | ((body: string) => boolean)>
| ((headers: Record<string, string>) => boolean);
/** Query params to intercept on */
query?: Record<string, unknown>;
}
export interface MockDispatch<
TData extends object = object,
TError extends Error = Error,
> extends Options {
times: number | null;
persist: boolean;
consumed: boolean;
data: MockDispatchData<TData, TError>;
}
export interface MockDispatchData<
TData extends object = object,
TError extends Error = Error,
> extends MockResponseOptions {
error: TError | null;
statusCode?: number;
data?: TData | string;
}
export interface MockResponseOptions {
headers?: IncomingHttpHeaders;
trailers?: Record<string, string>;
}
export interface MockResponseCallbackOptions {
path: string;
origin: string;
method: string;
body?: BodyInit;
headers: Headers | Record<string, string>;
maxRedirections: number;
}
export type MockResponseDataHandler<TData extends object = object> = (
opts: MockResponseCallbackOptions
) => TData | Buffer | string;
export type MockReplyOptionsCallback<TData extends object = object> = (
opts: MockResponseCallbackOptions
) => {
statusCode: number;
data?: TData | Buffer | string;
responseOptions?: MockResponseOptions;
};
}
interface Interceptable {
/** Intercepts any matching requests that use the same origin as this mock client. */
intercept(options: MockInterceptor.Options): MockInterceptor;
}
interface PendingInterceptor extends MockInterceptor.MockDispatch {
origin: string;
}
interface PendingInterceptorsFormatter {
format(pendingInterceptors: readonly PendingInterceptor[]): string;
}
/** A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. */
abstract class MockAgent {
/** Creates and retrieves mock Dispatcher instances which can then be used to intercept HTTP requests. If the number of connections on the mock agent is set to 1, a MockClient instance is returned. Otherwise a MockPool instance is returned. */
// eslint-disable-next-line no-shadow
get(origin: string | RegExp | ((origin: string) => boolean)): Interceptable;
/** Disables mocking in MockAgent. */
deactivate(): void;
/** Enables mocking in a MockAgent instance. When instantiated, a MockAgent is automatically activated. Therefore, this method is only effective after MockAgent.deactivate has been called. */
activate(): void;
/** Define host matchers so only matching requests that aren't intercepted by the mock dispatchers will be attempted. */
enableNetConnect(
// eslint-disable-next-line no-shadow
host?: string | RegExp | ((host: string) => boolean)
): void;
/** Causes all requests to throw when requests are not matched in a MockAgent intercept. */
disableNetConnect(): void;
pendingInterceptors(): PendingInterceptor[];
assertNoPendingInterceptors(options?: {
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
}): void;
}
}