Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract out test environment global types #494

Merged
merged 1 commit into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/jest-environment-miniflare/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@miniflare/shared-test-environment/globals";
3 changes: 2 additions & 1 deletion packages/jest-environment-miniflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"files": [
"dist/src"
"dist/src",
"globals.d.ts"
],
"engines": {
"node": ">=16.13"
Expand Down
232 changes: 232 additions & 0 deletions packages/shared-test-environment/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import type {
BodyInit,
DurableObject,
DurableObjectId,
DurableObjectState,
DurableObjectStorage,
ExecutionContext,
FetchEvent,
Headers,
ScheduledEvent,
} from "@cloudflare/workers-types/experimental";

declare global {
/**
* Get object containing all bindings (e.g. KV namespaces, R2 buckets).
* This is the `env` parameter passed to module workers.
*/
function getMiniflareBindings<Bindings = Record<string, any>>(): Bindings;
/**
* Get the underlying Durable Object storage for the specified ID.
* This is the `storage` property on the `state` parameter passed to the
* object constructor.
*/
function getMiniflareDurableObjectStorage(
id: DurableObjectId
): Promise<DurableObjectStorage>;
/**
* Get the Durable Object state for the specified ID.
* This is the `state` parameter passed to the object constructor.
*/
function getMiniflareDurableObjectState(
id: DurableObjectId
): Promise<DurableObjectState>;
/**
* Gets the singleton Durable Object instance for the specified ID.
* This is the same instance requests will be sent to via stubs obtained from
* Durable Object namespace bindings.
*/
function getMiniflareDurableObjectInstance<T extends DurableObject>(
id: DurableObjectId
): Promise<T>;
/**
* Waits for the Durable Object's associated input gate to open, closes the
* input gate, runs the closure under a new output gate, opens the input gate,
* then waits for the output gate to open. If you're calling `fetch` directly
* on a Durable Object instance, make sure to wrap the call with this to
* prevent race conditions.
*/
function runWithMiniflareDurableObjectGates<T>(
state: DurableObjectState,
closure: () => T | Promise<T>
): Promise<T>;
/**
* Gets the preconfigured `MockAgent` attached to Miniflare's `fetch`
* function. Use this to mock responses to `fetch` requests.
*/
function getMiniflareFetchMock(): MockAgent;
/**
* Waits for all `waitUntil`ed `Promise`s on the specified event or context
* to resolve, returning a `Promise` that resolves to an array of the values
* they resolve with.
*/
function getMiniflareWaitUntil<WaitUntil extends any[] = unknown[]>(
event: FetchEvent | ScheduledEvent | ExecutionContext
): Promise<WaitUntil>;
/**
* Immediately invokes scheduled Durable Object alarms. If an array of IDs is
* specified, only those Durable Objects will have their scheduled alarms
* invoked, otherwise all scheduled alarms will be invoked.
*/
function flushMiniflareDurableObjectAlarms(
ids?: DurableObjectId[]
): Promise<void>;
/**
* Gets an array containing IDs for all Durable Object instances Miniflare
* has constructed for the specified namespace.
*/
function getMiniflareDurableObjectIds(
namespace: string
): Promise<DurableObjectId[]>;
}

// 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>;

/** The scope associated with a mock dispatch. */
declare 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. */
declare 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;
}
declare 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?:
| Record<string, string | RegExp | ((body: string) => boolean)>
| ((headers: Record<string, string>) => boolean);
/** Query params to intercept on */
query?: Record<string, any>;
}
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. */
declare 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. */
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(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;
}
4 changes: 3 additions & 1 deletion packages/shared-test-environment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"files": [
"dist/src"
"dist/src",
"globals.d.ts"
],
"engines": {
"node": ">=16.13"
Expand All @@ -35,6 +36,7 @@
"extends": "../../package.json"
},
"dependencies": {
"@cloudflare/workers-types": "^4.20221111.1",
"@miniflare/cache": "2.11.0",
"@miniflare/core": "2.11.0",
"@miniflare/d1": "2.11.0",
Expand Down
27 changes: 0 additions & 27 deletions packages/shared-test-environment/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,6 @@ export class ExecutionContext {
}
}

declare global {
function getMiniflareBindings<Bindings = Context>(): Bindings;
function getMiniflareDurableObjectStorage(
id: DurableObjectId
): Promise<DurableObjectStorage>;
function getMiniflareDurableObjectState(
id: DurableObjectId
): Promise<DurableObjectState>;
function getMiniflareDurableObjectInstance(
id: DurableObjectId
): Promise<DurableObject>;
function runWithMiniflareDurableObjectGates<T>(
state: DurableObjectState,
closure: () => Awaitable<T>
): Promise<T>;
function getMiniflareFetchMock(): MockAgent;
function getMiniflareWaitUntil<WaitUntil extends any[] = unknown[]>(
event: FetchEvent | ScheduledEvent | ExecutionContext
): Promise<WaitUntil>;
function flushMiniflareDurableObjectAlarms(
ids: DurableObjectId[]
): Promise<void>;
function getMiniflareDurableObjectIds(
namespace: string
): Promise<DurableObjectId[]>;
}

export interface MiniflareEnvironmentUtilities {
getMiniflareBindings<Bindings = Context>(): Bindings;
getMiniflareDurableObjectStorage(
Expand Down
11 changes: 11 additions & 0 deletions packages/vitest-environment-miniflare/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "@miniflare/shared-test-environment/globals";
import { describe } from "vitest";

declare global {
/**
* Automatically undo changes made in tests. Make sure to use the returned
* `describe` function instead of the one from the `vitest` module.
* See https://miniflare.dev/testing/vitest#isolated-storage for more details.
*/
function setupMiniflareIsolatedStorage(): typeof describe;
}
3 changes: 2 additions & 1 deletion packages/vitest-environment-miniflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"module": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"files": [
"dist/src"
"dist/src",
"globals.d.ts"
],
"engines": {
"node": ">=16.13"
Expand Down
4 changes: 0 additions & 4 deletions packages/vitest-environment-miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import {
} from "vitest";
import { createChainable } from "./chain";

declare global {
function setupMiniflareIsolatedStorage(): typeof describe;
}

const scriptRunner = new VMScriptRunner();
const queueBroker = new QueueBroker();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from "vitest";
setupMiniflareIsolatedStorage();

test("fetch mock", async () => {
const fetchMock = getMiniflareFetchMock();

fetchMock.disableNetConnect();
fetchMock
.get("https://example.com")
.intercept({ path: "/" })
.reply(200, "body");

const response = await fetch("https://example.com");
expect(await response.text()).toBe("body");
});