Skip to content

Commit 922d653

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 922d653

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-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: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
/**
82+
* Gets a function wrapping the body of a vitest `describe` block to execute in a
83+
* synchronous-only zone.
84+
*/
85+
function wrapDescribeInZone(describeBody: Function): Function {
86+
return function (this: unknown, ...args: unknown[]) {
87+
return syncZone.run(describeBody, this, args);
88+
};
89+
}
90+
91+
/**
92+
* Gets a function wrapping the body of a vitest `it/beforeEach/afterEach` block to
93+
* execute in a ProxyZone zone.
94+
* This will run in the `proxyZone`.
95+
*/
96+
function wrapTestInZone(testBody: Function): Function {
97+
if (typeof testBody !== 'function') {
98+
return testBody;
99+
}
100+
const wrappedFunc = function () {
101+
return proxyZone.run(testBody, null, arguments as any);
102+
};
103+
// Update the length of wrappedFunc to be the same as the length of the testBody
104+
// So vitest core can handle whether the test function has `done()` or not correctly
105+
Object.defineProperty(wrappedFunc, 'length', {
106+
configurable: true,
107+
writable: true,
108+
enumerable: false,
109+
});
110+
wrappedFunc.length = testBody.length;
111+
return wrappedFunc;
112+
}
113+
114+
['suite', 'describe'].forEach((methodName) => {
115+
let originalVitestFn: Function & Record<(typeof DESCRIBE_FACTORY_NAMES)[number], Function> =
116+
context[methodName];
117+
// Skip if already patched
118+
if (context[Zone.__symbol__(methodName)]) {
119+
return;
120+
}
121+
122+
context[Zone.__symbol__(methodName)] = originalVitestFn;
123+
context[methodName] = function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
124+
args[1] = wrapDescribeInZone(args[1]);
125+
return originalVitestFn.apply(this, args);
126+
};
127+
128+
for (const factoryName of DESCRIBE_FACTORY_NAMES) {
129+
context[methodName][factoryName] = function (this: unknown, ...factoryArgs: unknown[]) {
130+
const originalDescribeFn = originalVitestFn.apply(this, factoryArgs);
131+
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
132+
args[1] = wrapDescribeInZone(args[1]);
133+
return originalDescribeFn.apply(this, args);
134+
};
135+
};
136+
}
137+
});
138+
139+
['it', 'test'].forEach((methodName) => {
140+
let originalVitestFn: Function & Record<(typeof TEST_FACTORY_NAMES)[number], Function> =
141+
context[methodName];
142+
// Skip if already patched
143+
if (context[Zone.__symbol__(methodName)]) {
144+
return;
145+
}
146+
147+
context[Zone.__symbol__(methodName)] = originalVitestFn;
148+
context[methodName] = function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
149+
args[1] = wrapTestInZone(args[1]);
150+
return originalVitestFn.apply(this, args);
151+
};
152+
153+
for (const factoryName of TEST_FACTORY_NAMES) {
154+
context[methodName][factoryName] = function (this: unknown, ...factoryArgs: unknown[]) {
155+
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
156+
args[1] = wrapTestInZone(args[1]);
157+
return originalVitestFn.apply(this, factoryArgs).apply(this, args);
158+
};
159+
};
160+
}
161+
});
162+
163+
['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach((methodName) => {
164+
const originalVitestFn: Function = context[methodName];
165+
if (context[Zone.__symbol__(methodName)]) {
166+
return;
167+
}
168+
169+
context[Zone.__symbol__(methodName)] = originalVitestFn;
170+
context[methodName] = function (this: unknown, ...args: [Function, ...unknown[]]) {
171+
args[0] = wrapTestInZone(args[0]);
172+
return originalVitestFn.apply(this, args);
173+
};
174+
});
175+
});
176+
}

0 commit comments

Comments
 (0)