Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 2197020

Browse files
committed
Add Vitest environment, closes #342
The Vitest environment behaves almost identically to the Jest environment. Due to Vitest custom environment API restrictions, isolated storage must be installed manually in each test file. Call the `setupMiniflareIsolatedStorage()` global function and use the returned `describe` function instead of the regular `describe`/`suite` functions imported from `vitest`.
1 parent c686827 commit 2197020

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2222
-180
lines changed

package-lock.json

Lines changed: 1143 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"devDependencies": {
3232
"@ava/typescript": "^2.0.0",
3333
"@microsoft/api-extractor": "^7.19.4",
34-
"@types/node": "^18.7.6",
34+
"@types/node": "^18.7.15",
3535
"@types/rimraf": "^3.0.2",
3636
"@types/which": "^2.0.1",
3737
"@typescript-eslint/eslint-plugin": "^5.9.1",

packages/jest-environment-miniflare/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# `@miniflare/jest`
1+
# `jest-environment-miniflare`
22

33
Jest testing module for [Miniflare](https://github.com/cloudflare/miniflare): a
44
fun, full-featured, fully-local simulator for Cloudflare Workers. See

packages/jest-environment-miniflare/package.json

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "jest-environment-miniflare",
33
"version": "2.7.1",
4-
"description": "Jest testing module for Miniflare: a fun, full-featured, fully-local simulator for Cloudflare Workers",
4+
"description": "Jest testing environment for Miniflare: a fun, full-featured, fully-local simulator for Cloudflare Workers",
55
"keywords": [
66
"cloudflare",
77
"workers",
@@ -35,17 +35,9 @@
3535
"extends": "../../package.json"
3636
},
3737
"dependencies": {
38-
"@miniflare/cache": "2.7.1",
39-
"@miniflare/core": "2.7.1",
40-
"@miniflare/durable-objects": "2.7.1",
41-
"@miniflare/html-rewriter": "2.7.1",
42-
"@miniflare/kv": "2.7.1",
4338
"@miniflare/runner-vm": "2.7.1",
4439
"@miniflare/shared": "2.7.1",
45-
"@miniflare/sites": "2.7.1",
46-
"@miniflare/storage-memory": "2.7.1",
47-
"@miniflare/web-sockets": "2.7.1",
48-
"miniflare": "2.7.1",
40+
"@miniflare/shared-test-environment": "2.7.1",
4941
"@jest/environment": ">=27",
5042
"@jest/fake-timers": ">=27",
5143
"@jest/types": ">=27",
@@ -63,6 +55,7 @@
6355
"jest": "^28.1.0",
6456
"jest-mock": "^28.1.0",
6557
"jest-util": "^28.1.0",
58+
"miniflare": "2.7.1",
6659
"react-dom": "^18.1.0"
6760
}
6861
}

packages/jest-environment-miniflare/src/index.ts

Lines changed: 17 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,22 @@ import type {
66
} from "@jest/environment";
77
import { LegacyFakeTimers, ModernFakeTimers } from "@jest/fake-timers";
88
import type { Circus, Config, Global } from "@jest/types";
9-
import { CachePlugin } from "@miniflare/cache";
10-
import {
11-
BindingsPlugin,
12-
CorePlugin,
13-
MiniflareCore,
14-
createFetchMock,
15-
} from "@miniflare/core";
16-
import {
17-
DurableObjectId,
18-
DurableObjectStorage,
19-
DurableObjectsPlugin,
20-
} from "@miniflare/durable-objects";
21-
import { HTMLRewriterPlugin } from "@miniflare/html-rewriter";
22-
import { KVPlugin } from "@miniflare/kv";
23-
import { R2Plugin } from "@miniflare/r2";
9+
import { MiniflareCore } from "@miniflare/core";
2410
import { VMScriptRunner, defineHasInstances } from "@miniflare/runner-vm";
25-
import { Context, NoOpLog } from "@miniflare/shared";
26-
import { SitesPlugin } from "@miniflare/sites";
27-
import { WebSocketPlugin } from "@miniflare/web-sockets";
11+
import {
12+
PLUGINS,
13+
StackedMemoryStorageFactory,
14+
createMiniflareEnvironment,
15+
} from "@miniflare/shared-test-environment";
2816
import { ModuleMocker } from "jest-mock";
2917
import { installCommonGlobals } from "jest-util";
30-
import { MockAgent } from "undici";
31-
import { StackedMemoryStorageFactory } from "./storage";
32-
33-
declare global {
34-
function getMiniflareBindings<Bindings = Context>(): Bindings;
35-
function getMiniflareDurableObjectStorage(
36-
id: DurableObjectId
37-
): Promise<DurableObjectStorage>;
38-
function getMiniflareFetchMock(): MockAgent;
39-
function flushMiniflareDurableObjectAlarms(
40-
ids: DurableObjectId[]
41-
): Promise<void>;
42-
}
43-
44-
// MiniflareCore will ensure CorePlugin is first and BindingsPlugin is last,
45-
// so help it out by doing it ourselves so it doesn't have to. BuildPlugin
46-
// is intentionally omitted as the worker should only be built once per test
47-
// run, as opposed to once per test suite. The user is responsible for this.
48-
const PLUGINS = {
49-
CorePlugin,
50-
KVPlugin,
51-
R2Plugin,
52-
DurableObjectsPlugin,
53-
CachePlugin,
54-
SitesPlugin,
55-
HTMLRewriterPlugin,
56-
WebSocketPlugin,
57-
BindingsPlugin,
58-
};
5918

6019
export type Timer = {
6120
id: number;
6221
ref: () => Timer;
6322
unref: () => Timer;
6423
};
6524

66-
const log = new NoOpLog();
67-
6825
// Adapted from jest-environment-node:
6926
// https://github.com/facebook/jest/blob/8f2cdad7694f4c217ac779d3f4e3a150b5a3d74d/packages/jest-environment-node/src/index.ts
7027
export default class MiniflareEnvironment implements JestEnvironment<Timer> {
@@ -81,7 +38,7 @@ export default class MiniflareEnvironment implements JestEnvironment<Timer> {
8138

8239
private readonly storageFactory = new StackedMemoryStorageFactory();
8340
private readonly scriptRunner: VMScriptRunner;
84-
private readonly mockAgent: MockAgent;
41+
private mf?: MiniflareCore<typeof PLUGINS>;
8542

8643
constructor(
8744
config:
@@ -102,8 +59,6 @@ export default class MiniflareEnvironment implements JestEnvironment<Timer> {
10259
defineHasInstances(this.context);
10360
this.scriptRunner = new VMScriptRunner(this.context);
10461

105-
this.mockAgent = createFetchMock();
106-
10762
const global = (this.global = vm.runInContext("this", this.context));
10863
global.global = global;
10964
global.self = global;
@@ -162,93 +117,30 @@ export default class MiniflareEnvironment implements JestEnvironment<Timer> {
162117

163118
async setup(): Promise<void> {
164119
const global = this.global as any;
165-
166-
const mf = new MiniflareCore(
167-
PLUGINS,
120+
const [mf, mfGlobalScope] = await createMiniflareEnvironment(
168121
{
169-
log,
170122
storageFactory: this.storageFactory,
171123
scriptRunner: this.scriptRunner,
172-
// Only run the script if we're using Durable Objects and need to have
173-
// access to the exported classes. This means we're only running the
174-
// script in modules mode, so we don't need to worry about
175-
// addEventListener being called twice (once when the script is run, and
176-
// again when the user imports the worker in Jest tests).
177-
scriptRunForModuleExports: true,
178124
},
125+
this.config.testEnvironmentOptions,
179126
{
180-
// Autoload configuration files from default locations by default,
181-
// like the CLI (but allow the user to disable this/customise locations)
182-
wranglerConfigPath: true,
183-
packagePath: true,
184-
envPathDefaultFallback: true,
185-
186-
// Apply user's custom Miniflare options
187-
...this.config.testEnvironmentOptions,
188-
189-
globals: {
190-
...(this.config.testEnvironmentOptions?.globals as any),
191-
192-
// Make sure fancy jest console and faked timers are included
193-
console: global.console,
194-
setTimeout: global.setTimeout,
195-
setInterval: global.setInterval,
196-
clearTimeout: global.clearTimeout,
197-
clearInterval: global.clearInterval,
198-
},
199-
200-
// These options cannot be overwritten:
201-
// - We get the global scope once, so watch mode wouldn't do anything,
202-
// apart from stopping Jest exiting
203-
watch: false,
204-
// - Persistence must be disabled for stacked storage to work
205-
kvPersist: false,
206-
cachePersist: false,
207-
durableObjectsPersist: false,
208-
// - Allow all global operations, tests will be outside of a request
209-
// context, but we definitely want to allow people to access their
210-
// namespaces, perform I/O, etc.
211-
globalAsyncIO: true,
212-
globalTimers: true,
213-
globalRandom: true,
214-
// - Use the actual `Date` class. We'll be operating outside a request
215-
// context, so we'd be returning the actual time anyway, and this
216-
// might mess with Jest's own mocking.
217-
actualTime: true,
218-
// - We always want getMiniflareFetchMock() to return this MockAgent
219-
fetchMock: this.mockAgent,
127+
// Make sure fancy jest console and faked timers are included
128+
console: global.console,
129+
setTimeout: global.setTimeout,
130+
setInterval: global.setInterval,
131+
clearTimeout: global.clearTimeout,
132+
clearInterval: global.clearInterval,
220133
}
221134
);
135+
this.mf = mf;
222136

223-
const mfGlobalScope = await mf.getGlobalScope();
224-
mfGlobalScope.global = global;
225-
mfGlobalScope.self = global;
226137
// Make sure Miniflare's global scope is assigned to Jest's global context,
227138
// even if we didn't run a script because we had no Durable Objects
228139
Object.assign(global, mfGlobalScope);
229-
230-
// Add a way of getting bindings in modules mode to allow seeding data.
231-
// These names are intentionally verbose so they don't collide with anything
232-
// else in scope.
233-
const bindings = await mf.getBindings();
234-
global.getMiniflareBindings = () => bindings;
235-
global.getMiniflareDurableObjectStorage = async (id: DurableObjectId) => {
236-
const plugin = (await mf.getPlugins()).DurableObjectsPlugin;
237-
const storage = mf.getPluginStorage("DurableObjectsPlugin");
238-
const state = await plugin.getObject(storage, id);
239-
return state.storage;
240-
};
241-
global.getMiniflareFetchMock = () => this.mockAgent;
242-
global.flushMiniflareDurableObjectAlarms = async (
243-
ids?: DurableObjectId[]
244-
): Promise<void> => {
245-
const plugin = (await mf.getPlugins()).DurableObjectsPlugin;
246-
const storage = mf.getPluginStorage("DurableObjectsPlugin");
247-
return plugin.flushAlarms(storage, ids);
248-
};
249140
}
250141

251142
async teardown(): Promise<void> {
143+
await this.mf?.dispose();
252144
this.fakeTimers?.dispose();
253145
this.fakeTimersModern?.dispose();
254146
this.context = null;

packages/jest-environment-miniflare/test/fixtures/kv.worker.spec.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@ describe("more KV tests", () => {
4646
});
4747
});
4848
});
49+
50+
test("KV test 7", async () => {
51+
await append("m");
52+
expect(await get()).toBe("abm");
53+
});
54+
test("KV test 8", async () => {
55+
await append("n");
56+
expect(await get()).toBe("abn");
57+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { MiniflareCore } from "@miniflare/core";
2+
import {
3+
DurableObjectId,
4+
DurableObjectStorage,
5+
} from "@miniflare/durable-objects";
6+
import { Context } from "@miniflare/shared";
7+
import { MockAgent } from "undici";
8+
import { PLUGINS } from "./plugins";
9+
10+
declare global {
11+
function getMiniflareBindings<Bindings = Context>(): Bindings;
12+
function getMiniflareDurableObjectStorage(
13+
id: DurableObjectId
14+
): Promise<DurableObjectStorage>;
15+
function getMiniflareFetchMock(): MockAgent;
16+
function flushMiniflareDurableObjectAlarms(
17+
ids: DurableObjectId[]
18+
): Promise<void>;
19+
}
20+
21+
export interface MiniflareEnvironmentUtilities {
22+
getMiniflareBindings<Bindings = Context>(): Bindings;
23+
getMiniflareDurableObjectStorage(
24+
id: DurableObjectId
25+
): Promise<DurableObjectStorage>;
26+
getMiniflareFetchMock(): MockAgent;
27+
flushMiniflareDurableObjectAlarms(ids: DurableObjectId[]): Promise<void>;
28+
}
29+
30+
export async function createMiniflareEnvironmentUtilities(
31+
mf: MiniflareCore<typeof PLUGINS>,
32+
fetchMock: MockAgent
33+
): Promise<MiniflareEnvironmentUtilities> {
34+
// Add a way of getting bindings in modules mode to allow seeding data.
35+
// These names are intentionally verbose, so they don't collide with anything
36+
// else in scope.
37+
const bindings = await mf.getBindings();
38+
return {
39+
getMiniflareBindings<Bindings>() {
40+
return bindings as Bindings;
41+
},
42+
async getMiniflareDurableObjectStorage(id: DurableObjectId) {
43+
const plugin = (await mf.getPlugins()).DurableObjectsPlugin;
44+
const storage = mf.getPluginStorage("DurableObjectsPlugin");
45+
const state = await plugin.getObject(storage, id);
46+
return state.storage;
47+
},
48+
getMiniflareFetchMock() {
49+
return fetchMock;
50+
},
51+
async flushMiniflareDurableObjectAlarms(ids?: DurableObjectId[]) {
52+
const plugin = (await mf.getPlugins()).DurableObjectsPlugin;
53+
const storage = mf.getPluginStorage("DurableObjectsPlugin");
54+
return plugin.flushAlarms(storage, ids);
55+
},
56+
};
57+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {
2+
MiniflareCore,
3+
MiniflareCoreContext,
4+
MiniflareCoreOptions,
5+
createFetchMock,
6+
} from "@miniflare/core";
7+
import { Context, NoOpLog } from "@miniflare/shared";
8+
import { createMiniflareEnvironmentUtilities } from "./globals";
9+
import { PLUGINS } from "./plugins";
10+
11+
export * from "./plugins";
12+
export * from "./storage";
13+
export type { MiniflareEnvironmentUtilities } from "./globals";
14+
15+
const log = new NoOpLog();
16+
17+
export async function createMiniflareEnvironment(
18+
ctx: Pick<MiniflareCoreContext, "storageFactory" | "scriptRunner">,
19+
options: MiniflareCoreOptions<typeof PLUGINS>,
20+
globalOverrides?: Context
21+
): Promise<[mf: MiniflareCore<typeof PLUGINS>, globals: Context]> {
22+
const fetchMock = createFetchMock();
23+
const mf = new MiniflareCore(
24+
PLUGINS,
25+
{
26+
log,
27+
...ctx,
28+
// Only run the script if we're using Durable Objects and need to have
29+
// access to the exported classes. This means we're only running the
30+
// script in modules mode, so we don't need to worry about
31+
// addEventListener being called twice (once when the script is run, and
32+
// again when the user imports the worker in Jest tests).
33+
scriptRunForModuleExports: true,
34+
},
35+
{
36+
// Autoload configuration files from default locations by default,
37+
// like the CLI (but allow the user to disable this/customise locations)
38+
wranglerConfigPath: true,
39+
packagePath: true,
40+
envPathDefaultFallback: true,
41+
42+
// Apply user's custom Miniflare options
43+
...options,
44+
45+
globals: {
46+
...(options?.globals as any),
47+
...globalOverrides,
48+
},
49+
50+
// These options cannot be overwritten:
51+
// - We get the global scope once, so watch mode wouldn't do anything,
52+
// apart from stopping Jest exiting
53+
watch: false,
54+
// - Persistence must be disabled for stacked storage to work
55+
kvPersist: false,
56+
cachePersist: false,
57+
durableObjectsPersist: false,
58+
// - Allow all global operations, tests will be outside of a request
59+
// context, but we definitely want to allow people to access their
60+
// namespaces, perform I/O, etc.
61+
globalAsyncIO: true,
62+
globalTimers: true,
63+
globalRandom: true,
64+
// - Use the actual `Date` class. We'll be operating outside a request
65+
// context, so we'd be returning the actual time anyway, and this
66+
// might mess with Jest's own mocking.
67+
actualTime: true,
68+
// - We always want getMiniflareFetchMock() to return this MockAgent
69+
fetchMock,
70+
}
71+
);
72+
73+
const mfGlobalScope = await mf.getGlobalScope();
74+
mfGlobalScope.global = global;
75+
mfGlobalScope.self = global;
76+
77+
// Attach Miniflare utility methods to global
78+
const mfUtilities = await createMiniflareEnvironmentUtilities(mf, fetchMock);
79+
Object.assign(mfGlobalScope, mfUtilities);
80+
81+
return [mf, mfGlobalScope];
82+
}

0 commit comments

Comments
 (0)