Skip to content

Commit 442643b

Browse files
committed
feat(zone.js): support vitest patching in zone.js/testing
To support `fakeAsync` usage while using `vitest` as a test runner, Zone.js now provides patching when using the `zone.js/testing` package import. This patching is similar to that of the existing jasmine, mocha, and jest functionality.
1 parent 68d774f commit 442643b

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

packages/zone.js/lib/testing/zone-testing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {patchJasmine} from '../jasmine/jasmine';
1010
import {patchJest} from '../jest/jest';
1111
import {patchMocha} from '../mocha/mocha';
12+
import {patchVitest} from '../vitest/vitest';
1213
import {ZoneType} from '../zone-impl';
1314
import {patchAsyncTest} from '../zone-spec/async-test';
1415
import {patchFakeAsyncTest} from '../zone-spec/fake-async-test';
@@ -25,6 +26,7 @@ export function rollupTesting(Zone: ZoneType): void {
2526
patchJasmine(Zone);
2627
patchJest(Zone);
2728
patchMocha(Zone);
29+
patchVitest(Zone);
2830
patchAsyncTest(Zone);
2931
patchFakeAsyncTest(Zone);
3032
patchPromiseTesting(Zone);
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {ZoneType} from '../zone-impl';
10+
11+
/**
12+
* The ZoneType with additional testing related members added.
13+
* The additional members must be patched onto the type via the
14+
* `zone.js/testing` package entry point.
15+
*/
16+
interface TestingZoneType extends ZoneType {
17+
ProxyZoneSpec?: typeof import('../zone-spec/proxy').ProxyZoneSpec;
18+
SyncTestZoneSpec?: {new (namePrefix: string): ZoneSpec};
19+
}
20+
21+
/**
22+
* The list of method names for the describe/suite factories.
23+
* Example: `describe.skip('...', () => { ... });`
24+
* Sourced from https://vitest.dev/api/#describe
25+
*/
26+
const DESCRIBE_FACTORY_NAMES = [
27+
'skip',
28+
'skipIf',
29+
'runIf',
30+
'only',
31+
'concurrent',
32+
'sequential',
33+
'shuffle',
34+
'todo',
35+
'each',
36+
'for',
37+
] as const;
38+
39+
/**
40+
* The list of method names for the test/it factories.
41+
* Example: `test.skip('...', () => { ... });`
42+
* Sourced from https://vitest.dev/api/#test
43+
*/
44+
const TEST_FACTORY_NAMES = [
45+
'skip',
46+
'skipIf',
47+
'runIf',
48+
'only',
49+
'concurrent',
50+
'sequential',
51+
'shuffle',
52+
'todo',
53+
'each',
54+
'for',
55+
] as const;
56+
57+
export function patchVitest(Zone: ZoneType): void {
58+
Zone.__load_patch('vitest', (context: any, Zone: TestingZoneType) => {
59+
// Vitest global variable set by the Vitest runner during test execution
60+
const vitestGlobal = context['vitest'] as {['__zone_patch__']?: boolean} | undefined;
61+
62+
// Skip patching if vitest is not present or has already been patched
63+
if (typeof vitestGlobal === 'undefined' || vitestGlobal['__zone_patch__']) {
64+
return;
65+
}
66+
vitestGlobal['__zone_patch__'] = true;
67+
68+
// Ensure other testing related Zone.js patches have been applied
69+
if (!Zone.ProxyZoneSpec) {
70+
throw new Error('Missing ProxyZoneSpec');
71+
}
72+
if (!Zone.SyncTestZoneSpec) {
73+
throw new Error('Missing SyncTestZoneSpec');
74+
}
75+
76+
// Setup testing related Zone instances
77+
const rootZone = Zone.current;
78+
const syncZone = rootZone.fork(new Zone.SyncTestZoneSpec('vitest.describe'));
79+
const proxyZone = rootZone.fork(new Zone.ProxyZoneSpec());
80+
81+
function wrapDescribeFactoryInZone(originalVitestFn: Function) {
82+
return function (this: unknown, ...factoryArgs: unknown[]) {
83+
const originalDescribeFn = originalVitestFn.apply(this, factoryArgs);
84+
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
85+
args[1] = wrapDescribeInZone(args[1]);
86+
return originalDescribeFn.apply(this, args);
87+
};
88+
};
89+
}
90+
91+
function wrapTestFactoryInZone(originalVitestFn: Function) {
92+
return function (this: unknown, ...factoryArgs: unknown[]) {
93+
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
94+
args[1] = wrapTestInZone(args[1]);
95+
return originalVitestFn.apply(this, factoryArgs).apply(this, args);
96+
};
97+
};
98+
}
99+
100+
/**
101+
* Gets a function wrapping the body of a vitest `describe` block to execute in a
102+
* synchronous-only zone.
103+
*/
104+
function wrapDescribeInZone(describeBody: Function): Function {
105+
return function (this: unknown, ...args: unknown[]) {
106+
return syncZone.run(describeBody, this, args);
107+
};
108+
}
109+
110+
/**
111+
* Gets a function wrapping the body of a vitest `it/beforeEach/afterEach` block to
112+
* execute in a ProxyZone zone.
113+
* This will run in the `proxyZone`.
114+
*/
115+
function wrapTestInZone(testBody: Function): Function {
116+
if (typeof testBody !== 'function') {
117+
return testBody;
118+
}
119+
const wrappedFunc = function () {
120+
return proxyZone.run(testBody, null, arguments as any);
121+
};
122+
// Update the length of wrappedFunc to be the same as the length of the testBody
123+
// So vitest core can handle whether the test function has `done()` or not correctly
124+
Object.defineProperty(wrappedFunc, 'length', {
125+
configurable: true,
126+
writable: true,
127+
enumerable: false,
128+
});
129+
wrappedFunc.length = testBody.length;
130+
return wrappedFunc;
131+
}
132+
133+
['suite', 'describe'].forEach((methodName) => {
134+
let originalVitestFn: Function & Record<(typeof DESCRIBE_FACTORY_NAMES)[number], Function> =
135+
context[methodName];
136+
// Skip if already patched
137+
if (context[Zone.__symbol__(methodName)]) {
138+
return;
139+
}
140+
141+
context[Zone.__symbol__(methodName)] = originalVitestFn;
142+
context[methodName] = function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
143+
args[1] = wrapDescribeInZone(args[1]);
144+
return originalVitestFn.apply(this, args);
145+
};
146+
147+
for (const factoryName of DESCRIBE_FACTORY_NAMES) {
148+
context[methodName][factoryName] = wrapDescribeFactoryInZone(originalVitestFn[factoryName]);
149+
}
150+
});
151+
152+
['it', 'test'].forEach((methodName) => {
153+
let originalVitestFn: Function & Record<(typeof TEST_FACTORY_NAMES)[number], Function> =
154+
context[methodName];
155+
// Skip if already patched
156+
if (context[Zone.__symbol__(methodName)]) {
157+
return;
158+
}
159+
160+
context[Zone.__symbol__(methodName)] = originalVitestFn;
161+
context[methodName] = function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
162+
args[1] = wrapTestInZone(args[1]);
163+
return originalVitestFn.apply(this, args);
164+
};
165+
166+
for (const factoryName of TEST_FACTORY_NAMES) {
167+
context[methodName][factoryName] = wrapTestFactoryInZone(originalVitestFn[factoryName]);
168+
}
169+
});
170+
171+
['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach((methodName) => {
172+
const originalVitestFn: Function = context[methodName];
173+
if (context[Zone.__symbol__(methodName)]) {
174+
return;
175+
}
176+
177+
context[Zone.__symbol__(methodName)] = originalVitestFn;
178+
context[methodName] = function (this: unknown, ...args: [Function, ...unknown[]]) {
179+
args[0] = wrapTestInZone(args[0]);
180+
return originalVitestFn.apply(this, args);
181+
};
182+
});
183+
});
184+
}

0 commit comments

Comments
 (0)