Skip to content

Commit 9b8bd90

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 9b8bd90

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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+
export function patchVitest(Zone: ZoneType): void {
22+
Zone.__load_patch('vitest', (context: any, Zone: TestingZoneType) => {
23+
// Vitest global variable set by the Vitest runner during test execution
24+
const vitestGlobal = context['vitest'] as {['__zone_patch__']?: boolean} | undefined;
25+
26+
// Skip patching if vitest is not present or has already been patched
27+
if (typeof vitestGlobal === 'undefined' || vitestGlobal['__zone_patch__']) {
28+
return;
29+
}
30+
vitestGlobal['__zone_patch__'] = true;
31+
32+
// Ensure other testing related Zone.js patches have been applied
33+
if (!Zone.ProxyZoneSpec) {
34+
throw new Error('Missing ProxyZoneSpec');
35+
}
36+
if (!Zone.SyncTestZoneSpec) {
37+
throw new Error('Missing SyncTestZoneSpec');
38+
}
39+
40+
// Setup testing related Zone instances
41+
const rootZone = Zone.current;
42+
const syncZone = rootZone.fork(new Zone.SyncTestZoneSpec('vitest.describe'));
43+
const proxyZone = rootZone.fork(new Zone.ProxyZoneSpec());
44+
45+
function wrapDescribeFactoryInZone(originalVitestFn: Function) {
46+
return function (this: unknown, ...factoryArgs: unknown[]) {
47+
const originalDescribeFn = originalVitestFn.apply(this, factoryArgs);
48+
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
49+
args[1] = wrapDescribeInZone(args[1]);
50+
return originalDescribeFn.apply(this, args);
51+
};
52+
};
53+
}
54+
55+
function wrapTestFactoryInZone(originalVitestFn: Function) {
56+
return function (this: unknown, ...factoryArgs: unknown[]) {
57+
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
58+
args[1] = wrapTestInZone(args[1]);
59+
return originalVitestFn.apply(this, factoryArgs).apply(this, args);
60+
};
61+
};
62+
}
63+
64+
/**
65+
* Gets a function wrapping the body of a vitest `describe` block to execute in a
66+
* synchronous-only zone.
67+
*/
68+
function wrapDescribeInZone(describeBody: Function): Function {
69+
return function (this: unknown, ...args: unknown[]) {
70+
return syncZone.run(describeBody, this, args);
71+
};
72+
}
73+
74+
/**
75+
* Gets a function wrapping the body of a vitest `it/beforeEach/afterEach` block to
76+
* execute in a ProxyZone zone.
77+
* This will run in the `proxyZone`.
78+
*/
79+
function wrapTestInZone(testBody: Function): Function {
80+
if (typeof testBody !== 'function') {
81+
return testBody;
82+
}
83+
const wrappedFunc = function () {
84+
return proxyZone.run(testBody, null, arguments as any);
85+
};
86+
// Update the length of wrappedFunc to be the same as the length of the testBody
87+
// So vitest core can handle whether the test function has `done()` or not correctly
88+
Object.defineProperty(wrappedFunc, 'length', {
89+
configurable: true,
90+
writable: true,
91+
enumerable: false,
92+
});
93+
wrappedFunc.length = testBody.length;
94+
return wrappedFunc;
95+
}
96+
97+
['suite', 'describe'].forEach((methodName) => {
98+
let originalVitestFn: Function = context[methodName];
99+
if (context[Zone.__symbol__(methodName)]) {
100+
return;
101+
}
102+
103+
context[Zone.__symbol__(methodName)] = originalVitestFn;
104+
context[methodName] = function (this: unknown, ...args: any[]) {
105+
args[1] = wrapDescribeInZone(args[1]);
106+
return originalVitestFn.apply(this, args);
107+
};
108+
109+
[
110+
'skip',
111+
'skipIf',
112+
'runIf',
113+
'only',
114+
'concurrent',
115+
'sequential',
116+
'shuffle',
117+
'todo',
118+
'each',
119+
'for',
120+
].forEach((subMethodName) => {
121+
context[methodName][subMethodName] = wrapDescribeFactoryInZone(
122+
(originalVitestFn as any)[subMethodName],
123+
);
124+
});
125+
});
126+
127+
['it', 'test'].forEach((methodName) => {
128+
let originalVitestFn: Function = context[methodName];
129+
if (context[Zone.__symbol__(methodName)]) {
130+
return;
131+
}
132+
133+
context[Zone.__symbol__(methodName)] = originalVitestFn;
134+
context[methodName] = function (this: unknown, ...args: any[]) {
135+
args[1] = wrapTestInZone(args[1]);
136+
return originalVitestFn.apply(this, args);
137+
};
138+
139+
[
140+
'skip',
141+
'skipIf',
142+
'runIf',
143+
'only',
144+
'concurrent',
145+
'sequential',
146+
'todo',
147+
'fails',
148+
'each',
149+
'for',
150+
].forEach((subMethodName) => {
151+
context[methodName][subMethodName] = wrapTestFactoryInZone(
152+
(originalVitestFn as any)[subMethodName],
153+
);
154+
});
155+
});
156+
157+
['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach((methodName) => {
158+
const originalVitestFn: Function = context[methodName];
159+
if (context[Zone.__symbol__(methodName)]) {
160+
return;
161+
}
162+
163+
context[Zone.__symbol__(methodName)] = originalVitestFn;
164+
context[methodName] = function (this: unknown, ...args: [Function, ...unknown[]]) {
165+
args[0] = wrapTestInZone(args[0]);
166+
return originalVitestFn.apply(this, args);
167+
};
168+
});
169+
});
170+
}

0 commit comments

Comments
 (0)