fix(zone.js): vitest patching of describe/it curried calls

Separate direct from curried calls of `describe`/`it` modifiers
(direct: `skip`, `only`, etc.; curried: `runIf`, `each`, etc.)
and perform the required patching to them.

Fixes: #69748
This commit is contained in:
hawkgs
2026-07-13 14:30:34 +03:00
committed by Pawel Kozlowski
parent d14696e430
commit e23541b263
2 changed files with 105 additions and 54 deletions
+48 -54
View File
@@ -19,40 +19,31 @@ interface TestingZoneType extends ZoneType {
}
/**
* The list of method names for the describe/suite factories.
* The list of method names for the describe/suite and test/it factories
* that are called directly (i.e. same signature as describe/it).
*
* Example: `describe.skip('...', () => { ... });`
* Sourced from https://vitest.dev/api/#describe
*/
const DESCRIBE_FACTORY_NAMES = [
const DIRECT_MODIFIER_NAMES = [
'skip',
'skipIf',
'runIf',
'only',
'concurrent',
'sequential',
'shuffle',
'todo',
'each',
'for',
] as const;
/**
* The list of method names for the test/it factories.
* Example: `test.skip('...', () => { ... });`
* Sourced from https://vitest.dev/api/#test
* The list of method names for the describe/suite and test/it modifiers
* that are curried (i.e. called once with a condition/table to get back a chainable fn).
*
* Example: `describe.each([...])('...', () => { ... });`
* Sourced from https://vitest.dev/api/#describe
*/
const TEST_FACTORY_NAMES = [
'skip',
'skipIf',
'runIf',
'only',
'concurrent',
'sequential',
'shuffle',
'todo',
'each',
'for',
] as const;
const CURRIED_MODIFIER_NAMES = ['skipIf', 'runIf', 'each', 'for'] as const;
type TEST_MODIFIER_NAME = (typeof DIRECT_MODIFIER_NAMES | typeof CURRIED_MODIFIER_NAMES)[number];
export function patchVitest(Zone: ZoneType): void {
Zone.__load_patch('vitest', (context: any, Zone: TestingZoneType) => {
@@ -83,6 +74,10 @@ export function patchVitest(Zone: ZoneType): void {
* synchronous-only zone.
*/
function wrapDescribeInZone(describeBody: Function): Function {
// `describe` might be called without a body (e.g. `describe.todo`)
if (typeof describeBody !== 'function') {
return describeBody;
}
return function (this: unknown, ...args: unknown[]) {
return syncZone.run(describeBody, this, args);
};
@@ -111,54 +106,53 @@ export function patchVitest(Zone: ZoneType): void {
return wrappedFunc;
}
['suite', 'describe'].forEach((methodName) => {
let originalVitestFn: Function & Record<(typeof DESCRIBE_FACTORY_NAMES)[number], Function> =
context[methodName];
/** Patch functions with modifiers (i.e. `describe`/`it`). */
function patchFnWithModifiers(methodName: string, wrapFn: (fn: Function) => Function) {
const originalVitestFn: Function & Record<TEST_MODIFIER_NAME, Function> = context[methodName];
// Skip if already patched
if (context[Zone.__symbol__(methodName)]) {
return;
}
context[Zone.__symbol__(methodName)] = originalVitestFn;
// Patching the main function
context[methodName] = function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
args[1] = wrapDescribeInZone(args[1]);
args[1] = wrapFn(args[1]);
return originalVitestFn.apply(this, args);
};
for (const factoryName of DESCRIBE_FACTORY_NAMES) {
context[methodName][factoryName] = function (this: unknown, ...factoryArgs: unknown[]) {
const originalDescribeFn = originalVitestFn.apply(this, factoryArgs);
// Patching direct modifier calls
for (const modifierName of DIRECT_MODIFIER_NAMES) {
context[methodName][modifierName] = function (
this: unknown,
...args: [unknown, Function, ...unknown[]]
) {
args[1] = wrapFn(args[1]);
return originalVitestFn[modifierName].apply(this, args);
};
}
// Patching curried modifier calls
for (const modifierName of CURRIED_MODIFIER_NAMES) {
context[methodName][modifierName] = function (this: unknown, ...modifierArgs: unknown[]) {
// Since we are patching a curried function, we need
// to pass the original context first (`originalVitestFn`).
// Else, the chaining won't be possible (will get an error).
const originalFn = originalVitestFn[modifierName].apply(originalVitestFn, modifierArgs);
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
args[1] = wrapDescribeInZone(args[1]);
return originalDescribeFn.apply(this, args);
args[1] = wrapFn(args[1]);
return originalFn.apply(this, args);
};
};
}
});
}
['it', 'test'].forEach((methodName) => {
let originalVitestFn: Function & Record<(typeof TEST_FACTORY_NAMES)[number], Function> =
context[methodName];
// Skip if already patched
if (context[Zone.__symbol__(methodName)]) {
return;
}
['suite', 'describe'].forEach((methodName) =>
patchFnWithModifiers(methodName, wrapDescribeInZone),
);
context[Zone.__symbol__(methodName)] = originalVitestFn;
context[methodName] = function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
args[1] = wrapTestInZone(args[1]);
return originalVitestFn.apply(this, args);
};
for (const factoryName of TEST_FACTORY_NAMES) {
context[methodName][factoryName] = function (this: unknown, ...factoryArgs: unknown[]) {
return function (this: unknown, ...args: [unknown, Function, ...unknown[]]) {
args[1] = wrapTestInZone(args[1]);
return originalVitestFn.apply(this, factoryArgs).apply(this, args);
};
};
}
});
['it', 'test'].forEach((methodName) => patchFnWithModifiers(methodName, wrapTestInZone));
['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach((methodName) => {
const originalVitestFn: Function = context[methodName];
@@ -46,6 +46,63 @@ suite('patchVitest `suite`', () => {
}));
});
describe('patchVitest `describe`/`it` modifiers', () => {
describe('direct modifiers (e.g. skip, only, etc.)', () => {
let skippedDescribeBodyRan = false;
describe.skip('a skipped `describe`', () => {
it('should not run because the parent describe is skipped', () => {
skippedDescribeBodyRan = true;
});
});
let skippedTestRan = false;
it.skip('a skipped test', () => {
skippedTestRan = true;
});
it('should not have run the body of a skipped `describe`', () => {
expect(skippedDescribeBodyRan).toBe(false);
});
it('should not have run the body of a skipped test', () => {
expect(skippedTestRan).toBe(false);
});
// Validate that direct calls are patched.
describe.shuffle('a shuffled `describe`', () => {
it('should work with a patched `describe.shuffle`', fakeAsync(() => {
expect(createAsyncTesterFlag()).toBe(true);
}));
});
});
describe('curried modifiers (e.g. each, runIf, etc.)', () => {
describe.each([['suite A'], ['suite B']])('%s', (suiteName) => {
it(`should execute ${suiteName} tests inside a zone`, fakeAsync(() => {
expect(createAsyncTesterFlag()).toBe(true);
}));
});
it.each([
[1, 1, 2],
[1, 2, 3],
])(
'should add the numbers and run inside a zone',
fakeAsync((a, b, expected) => {
expect(a + b).toBe(expected);
expect(createAsyncTesterFlag()).toBe(true);
}),
);
it.runIf(true)(
'should run when `runIf` condition is true',
fakeAsync(() => {
expect(createAsyncTesterFlag()).toBe(true);
}),
);
});
});
function createAsyncTesterFlag() {
let flag = false;
setTimeout(() => {