From e23541b26385753ff2014d5f48cd1003f93cc27c Mon Sep 17 00:00:00 2001 From: hawkgs Date: Mon, 13 Jul 2026 14:30:34 +0300 Subject: [PATCH] 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 --- packages/zone.js/lib/vitest/vitest.ts | 102 +++++++++--------- .../test/vitest/vitest-patch-globals.spec.js | 57 ++++++++++ 2 files changed, 105 insertions(+), 54 deletions(-) diff --git a/packages/zone.js/lib/vitest/vitest.ts b/packages/zone.js/lib/vitest/vitest.ts index 99bae331d34..ca39387d75b 100644 --- a/packages/zone.js/lib/vitest/vitest.ts +++ b/packages/zone.js/lib/vitest/vitest.ts @@ -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 = 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]; diff --git a/packages/zone.js/test/vitest/vitest-patch-globals.spec.js b/packages/zone.js/test/vitest/vitest-patch-globals.spec.js index fcf95c93c92..e4acbcdbd05 100644 --- a/packages/zone.js/test/vitest/vitest-patch-globals.spec.js +++ b/packages/zone.js/test/vitest/vitest-patch-globals.spec.js @@ -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(() => {