UNPKG

zone.js

Version:
1,029 lines (1,025 loc) 97.8 kB
'use strict'; /** * @license Angular v<unknown> * (c) 2010-2024 Google LLC. https://angular.io/ * License: MIT */ function patchJasmine(Zone) { Zone.__load_patch('jasmine', (global, Zone, api) => { const __extends = function (d, b) { for (const p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __()); }; // Patch jasmine's describe/it/beforeEach/afterEach functions so test code always runs // in a testZone (ProxyZone). (See: angular/zone.js#91 & angular/angular#10503) if (!Zone) throw new Error('Missing: zone.js'); if (typeof jest !== 'undefined') { // return if jasmine is a light implementation inside jest // in this case, we are running inside jest not jasmine return; } if (typeof jasmine == 'undefined' || jasmine['__zone_patch__']) { return; } jasmine['__zone_patch__'] = true; const SyncTestZoneSpec = Zone['SyncTestZoneSpec']; const ProxyZoneSpec = Zone['ProxyZoneSpec']; if (!SyncTestZoneSpec) throw new Error('Missing: SyncTestZoneSpec'); if (!ProxyZoneSpec) throw new Error('Missing: ProxyZoneSpec'); const ambientZone = Zone.current; const symbol = Zone.__symbol__; // whether patch jasmine clock when in fakeAsync const disablePatchingJasmineClock = global[symbol('fakeAsyncDisablePatchingClock')] === true; // the original variable name fakeAsyncPatchLock is not accurate, so the name will be // fakeAsyncAutoFakeAsyncWhenClockPatched and if this enablePatchingJasmineClock is false, we // also automatically disable the auto jump into fakeAsync feature const enableAutoFakeAsyncWhenClockPatched = !disablePatchingJasmineClock && (global[symbol('fakeAsyncPatchLock')] === true || global[symbol('fakeAsyncAutoFakeAsyncWhenClockPatched')] === true); const ignoreUnhandledRejection = global[symbol('ignoreUnhandledRejection')] === true; if (!ignoreUnhandledRejection) { const globalErrors = jasmine.GlobalErrors; if (globalErrors && !jasmine[symbol('GlobalErrors')]) { jasmine[symbol('GlobalErrors')] = globalErrors; jasmine.GlobalErrors = function () { const instance = new globalErrors(); const originalInstall = instance.install; if (originalInstall && !instance[symbol('install')]) { instance[symbol('install')] = originalInstall; instance.install = function () { const isNode = typeof process !== 'undefined' && !!process.on; // Note: Jasmine checks internally if `process` and `process.on` is defined. // Otherwise, it installs the browser rejection handler through the // `global.addEventListener`. This code may be run in the browser environment where // `process` is not defined, and this will lead to a runtime exception since Webpack 5 // removed automatic Node.js polyfills. Note, that events are named differently, it's // `unhandledRejection` in Node.js and `unhandledrejection` in the browser. const originalHandlers = isNode ? process.listeners('unhandledRejection') : global.eventListeners('unhandledrejection'); const result = originalInstall.apply(this, arguments); isNode ? process.removeAllListeners('unhandledRejection') : global.removeAllListeners('unhandledrejection'); if (originalHandlers) { originalHandlers.forEach((handler) => { if (isNode) { process.on('unhandledRejection', handler); } else { global.addEventListener('unhandledrejection', handler); } }); } return result; }; } return instance; }; } } // Monkey patch all of the jasmine DSL so that each function runs in appropriate zone. const jasmineEnv = jasmine.getEnv(); ['describe', 'xdescribe', 'fdescribe'].forEach((methodName) => { let originalJasmineFn = jasmineEnv[methodName]; jasmineEnv[methodName] = function (description, specDefinitions) { return originalJasmineFn.call(this, description, wrapDescribeInZone(description, specDefinitions)); }; }); ['it', 'xit', 'fit'].forEach((methodName) => { let originalJasmineFn = jasmineEnv[methodName]; jasmineEnv[symbol(methodName)] = originalJasmineFn; jasmineEnv[methodName] = function (description, specDefinitions, timeout) { arguments[1] = wrapTestInZone(specDefinitions); return originalJasmineFn.apply(this, arguments); }; }); ['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach((methodName) => { let originalJasmineFn = jasmineEnv[methodName]; jasmineEnv[symbol(methodName)] = originalJasmineFn; jasmineEnv[methodName] = function (specDefinitions, timeout) { arguments[0] = wrapTestInZone(specDefinitions); return originalJasmineFn.apply(this, arguments); }; }); if (!disablePatchingJasmineClock) { // need to patch jasmine.clock().mockDate and jasmine.clock().tick() so // they can work properly in FakeAsyncTest const originalClockFn = (jasmine[symbol('clock')] = jasmine['clock']); jasmine['clock'] = function () { const clock = originalClockFn.apply(this, arguments); if (!clock[symbol('patched')]) { clock[symbol('patched')] = symbol('patched'); const originalTick = (clock[symbol('tick')] = clock.tick); clock.tick = function () { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { return fakeAsyncZoneSpec.tick.apply(fakeAsyncZoneSpec, arguments); } return originalTick.apply(this, arguments); }; const originalMockDate = (clock[symbol('mockDate')] = clock.mockDate); clock.mockDate = function () { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { const dateTime = arguments.length > 0 ? arguments[0] : new Date(); return fakeAsyncZoneSpec.setFakeBaseSystemTime.apply(fakeAsyncZoneSpec, dateTime && typeof dateTime.getTime === 'function' ? [dateTime.getTime()] : arguments); } return originalMockDate.apply(this, arguments); }; // for auto go into fakeAsync feature, we need the flag to enable it if (enableAutoFakeAsyncWhenClockPatched) { ['install', 'uninstall'].forEach((methodName) => { const originalClockFn = (clock[symbol(methodName)] = clock[methodName]); clock[methodName] = function () { const FakeAsyncTestZoneSpec = Zone['FakeAsyncTestZoneSpec']; if (FakeAsyncTestZoneSpec) { jasmine[symbol('clockInstalled')] = 'install' === methodName; return; } return originalClockFn.apply(this, arguments); }; }); } } return clock; }; } // monkey patch createSpyObj to make properties enumerable to true if (!jasmine[Zone.__symbol__('createSpyObj')]) { const originalCreateSpyObj = jasmine.createSpyObj; jasmine[Zone.__symbol__('createSpyObj')] = originalCreateSpyObj; jasmine.createSpyObj = function () { const args = Array.prototype.slice.call(arguments); const propertyNames = args.length >= 3 ? args[2] : null; let spyObj; if (propertyNames) { const defineProperty = Object.defineProperty; Object.defineProperty = function (obj, p, attributes) { return defineProperty.call(this, obj, p, { ...attributes, configurable: true, enumerable: true, }); }; try { spyObj = originalCreateSpyObj.apply(this, args); } finally { Object.defineProperty = defineProperty; } } else { spyObj = originalCreateSpyObj.apply(this, args); } return spyObj; }; } /** * Gets a function wrapping the body of a Jasmine `describe` block to execute in a * synchronous-only zone. */ function wrapDescribeInZone(description, describeBody) { return function () { // Create a synchronous-only zone in which to run `describe` blocks in order to raise an // error if any asynchronous operations are attempted inside of a `describe`. const syncZone = ambientZone.fork(new SyncTestZoneSpec(`jasmine.describe#${description}`)); return syncZone.run(describeBody, this, arguments); }; } function runInTestZone(testBody, applyThis, queueRunner, done) { const isClockInstalled = !!jasmine[symbol('clockInstalled')]; queueRunner.testProxyZoneSpec; const testProxyZone = queueRunner.testProxyZone; if (isClockInstalled && enableAutoFakeAsyncWhenClockPatched) { // auto run a fakeAsync const fakeAsyncModule = Zone[Zone.__symbol__('fakeAsyncTest')]; if (fakeAsyncModule && typeof fakeAsyncModule.fakeAsync === 'function') { testBody = fakeAsyncModule.fakeAsync(testBody); } } if (done) { return testProxyZone.run(testBody, applyThis, [done]); } else { return testProxyZone.run(testBody, applyThis); } } /** * Gets a function wrapping the body of a Jasmine `it/beforeEach/afterEach` block to * execute in a ProxyZone zone. * This will run in `testProxyZone`. The `testProxyZone` will be reset by the `ZoneQueueRunner` */ function wrapTestInZone(testBody) { // The `done` callback is only passed through if the function expects at least one argument. // Note we have to make a function with correct number of arguments, otherwise jasmine will // think that all functions are sync or async. return (testBody && (testBody.length ? function (done) { return runInTestZone(testBody, this, this.queueRunner, done); } : function () { return runInTestZone(testBody, this, this.queueRunner); })); } const QueueRunner = jasmine.QueueRunner; jasmine.QueueRunner = (function (_super) { __extends(ZoneQueueRunner, _super); function ZoneQueueRunner(attrs) { if (attrs.onComplete) { attrs.onComplete = ((fn) => () => { // All functions are done, clear the test zone. this.testProxyZone = null; this.testProxyZoneSpec = null; ambientZone.scheduleMicroTask('jasmine.onComplete', fn); })(attrs.onComplete); } const nativeSetTimeout = global[Zone.__symbol__('setTimeout')]; const nativeClearTimeout = global[Zone.__symbol__('clearTimeout')]; if (nativeSetTimeout) { // should run setTimeout inside jasmine outside of zone attrs.timeout = { setTimeout: nativeSetTimeout ? nativeSetTimeout : global.setTimeout, clearTimeout: nativeClearTimeout ? nativeClearTimeout : global.clearTimeout, }; } // create a userContext to hold the queueRunner itself // so we can access the testProxy in it/xit/beforeEach ... if (jasmine.UserContext) { if (!attrs.userContext) { attrs.userContext = new jasmine.UserContext(); } attrs.userContext.queueRunner = this; } else { if (!attrs.userContext) { attrs.userContext = {}; } attrs.userContext.queueRunner = this; } // patch attrs.onException const onException = attrs.onException; attrs.onException = function (error) { if (error && error.message === 'Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.') { // jasmine timeout, we can make the error message more // reasonable to tell what tasks are pending const proxyZoneSpec = this && this.testProxyZoneSpec; if (proxyZoneSpec) { const pendingTasksInfo = proxyZoneSpec.getAndClearPendingTasksInfo(); try { // try catch here in case error.message is not writable error.message += pendingTasksInfo; } catch (err) { } } } if (onException) { onException.call(this, error); } }; _super.call(this, attrs); } ZoneQueueRunner.prototype.execute = function () { let zone = Zone.current; let isChildOfAmbientZone = false; while (zone) { if (zone === ambientZone) { isChildOfAmbientZone = true; break; } zone = zone.parent; } if (!isChildOfAmbientZone) throw new Error('Unexpected Zone: ' + Zone.current.name); // This is the zone which will be used for running individual tests. // It will be a proxy zone, so that the tests function can retroactively install // different zones. // Example: // - In beforeEach() do childZone = Zone.current.fork(...); // - In it() try to do fakeAsync(). The issue is that because the beforeEach forked the // zone outside of fakeAsync it will be able to escape the fakeAsync rules. // - Because ProxyZone is parent fo `childZone` fakeAsync can retroactively add // fakeAsync behavior to the childZone. this.testProxyZoneSpec = new ProxyZoneSpec(); this.testProxyZone = ambientZone.fork(this.testProxyZoneSpec); if (!Zone.currentTask) { // if we are not running in a task then if someone would register a // element.addEventListener and then calling element.click() the // addEventListener callback would think that it is the top most task and would // drain the microtask queue on element.click() which would be incorrect. // For this reason we always force a task when running jasmine tests. Zone.current.scheduleMicroTask('jasmine.execute().forceTask', () => QueueRunner.prototype.execute.call(this)); } else { _super.prototype.execute.call(this); } }; return ZoneQueueRunner; })(QueueRunner); }); } function patchJest(Zone) { Zone.__load_patch('jest', (context, Zone, api) => { if (typeof jest === 'undefined' || jest['__zone_patch__']) { return; } // From jest 29 and jest-preset-angular v13, the module transform logic // changed, and now jest-preset-angular use the use the tsconfig target // other than the hardcoded one, https://github.com/thymikee/jest-preset-angular/issues/2010 // But jest-angular-preset doesn't introduce the @babel/plugin-transform-async-to-generator // which is needed by angular since `async/await` still need to be transformed // to promise for ES2017+ target. // So for now, we disable to output the uncaught error console log for a temp solution, // until jest-preset-angular find a proper solution. Zone[api.symbol('ignoreConsoleErrorUncaughtError')] = true; jest['__zone_patch__'] = true; const ProxyZoneSpec = Zone['ProxyZoneSpec']; const SyncTestZoneSpec = Zone['SyncTestZoneSpec']; if (!ProxyZoneSpec) { throw new Error('Missing ProxyZoneSpec'); } const rootZone = Zone.current; const syncZone = rootZone.fork(new SyncTestZoneSpec('jest.describe')); const proxyZoneSpec = new ProxyZoneSpec(); const proxyZone = rootZone.fork(proxyZoneSpec); function wrapDescribeFactoryInZone(originalJestFn) { return function (...tableArgs) { const originalDescribeFn = originalJestFn.apply(this, tableArgs); return function (...args) { args[1] = wrapDescribeInZone(args[1]); return originalDescribeFn.apply(this, args); }; }; } function wrapTestFactoryInZone(originalJestFn) { return function (...tableArgs) { return function (...args) { args[1] = wrapTestInZone(args[1]); return originalJestFn.apply(this, tableArgs).apply(this, args); }; }; } /** * Gets a function wrapping the body of a jest `describe` block to execute in a * synchronous-only zone. */ function wrapDescribeInZone(describeBody) { return function (...args) { return syncZone.run(describeBody, this, args); }; } /** * Gets a function wrapping the body of a jest `it/beforeEach/afterEach` block to * execute in a ProxyZone zone. * This will run in the `proxyZone`. */ function wrapTestInZone(testBody, isTestFunc = false) { if (typeof testBody !== 'function') { return testBody; } const wrappedFunc = function () { if (Zone[api.symbol('useFakeTimersCalled')] === true && testBody && !testBody.isFakeAsync) { // jest.useFakeTimers is called, run into fakeAsyncTest automatically. const fakeAsyncModule = Zone[Zone.__symbol__('fakeAsyncTest')]; if (fakeAsyncModule && typeof fakeAsyncModule.fakeAsync === 'function') { testBody = fakeAsyncModule.fakeAsync(testBody); } } proxyZoneSpec.isTestFunc = isTestFunc; return proxyZone.run(testBody, null, arguments); }; // Update the length of wrappedFunc to be the same as the length of the testBody // So jest core can handle whether the test function has `done()` or not correctly Object.defineProperty(wrappedFunc, 'length', { configurable: true, writable: true, enumerable: false, }); wrappedFunc.length = testBody.length; return wrappedFunc; } ['describe', 'xdescribe', 'fdescribe'].forEach((methodName) => { let originalJestFn = context[methodName]; if (context[Zone.__symbol__(methodName)]) { return; } context[Zone.__symbol__(methodName)] = originalJestFn; context[methodName] = function (...args) { args[1] = wrapDescribeInZone(args[1]); return originalJestFn.apply(this, args); }; context[methodName].each = wrapDescribeFactoryInZone(originalJestFn.each); }); context.describe.only = context.fdescribe; context.describe.skip = context.xdescribe; ['it', 'xit', 'fit', 'test', 'xtest'].forEach((methodName) => { let originalJestFn = context[methodName]; if (context[Zone.__symbol__(methodName)]) { return; } context[Zone.__symbol__(methodName)] = originalJestFn; context[methodName] = function (...args) { args[1] = wrapTestInZone(args[1], true); return originalJestFn.apply(this, args); }; context[methodName].each = wrapTestFactoryInZone(originalJestFn.each); context[methodName].todo = originalJestFn.todo; context[methodName].failing = originalJestFn.failing; }); context.it.only = context.fit; context.it.skip = context.xit; context.test.only = context.fit; context.test.skip = context.xit; ['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach((methodName) => { let originalJestFn = context[methodName]; if (context[Zone.__symbol__(methodName)]) { return; } context[Zone.__symbol__(methodName)] = originalJestFn; context[methodName] = function (...args) { args[0] = wrapTestInZone(args[0]); return originalJestFn.apply(this, args); }; }); Zone.patchJestObject = function patchJestObject(Timer, isModern = false) { // check whether currently the test is inside fakeAsync() function isPatchingFakeTimer() { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); return !!fakeAsyncZoneSpec; } // check whether the current function is inside `test/it` or other methods // such as `describe/beforeEach` function isInTestFunc() { const proxyZoneSpec = Zone.current.get('ProxyZoneSpec'); return proxyZoneSpec && proxyZoneSpec.isTestFunc; } if (Timer[api.symbol('fakeTimers')]) { return; } Timer[api.symbol('fakeTimers')] = true; // patch jest fakeTimer internal method to make sure no console.warn print out api.patchMethod(Timer, '_checkFakeTimers', (delegate) => { return function (self, args) { if (isPatchingFakeTimer()) { return true; } else { return delegate.apply(self, args); } }; }); // patch useFakeTimers(), set useFakeTimersCalled flag, and make test auto run into fakeAsync api.patchMethod(Timer, 'useFakeTimers', (delegate) => { return function (self, args) { Zone[api.symbol('useFakeTimersCalled')] = true; if (isModern || isInTestFunc()) { return delegate.apply(self, args); } return self; }; }); // patch useRealTimers(), unset useFakeTimers flag api.patchMethod(Timer, 'useRealTimers', (delegate) => { return function (self, args) { Zone[api.symbol('useFakeTimersCalled')] = false; if (isModern || isInTestFunc()) { return delegate.apply(self, args); } return self; }; }); // patch setSystemTime(), call setCurrentRealTime() in the fakeAsyncTest api.patchMethod(Timer, 'setSystemTime', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec && isPatchingFakeTimer()) { fakeAsyncZoneSpec.setFakeBaseSystemTime(args[0]); } else { return delegate.apply(self, args); } }; }); // patch getSystemTime(), call getCurrentRealTime() in the fakeAsyncTest api.patchMethod(Timer, 'getRealSystemTime', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec && isPatchingFakeTimer()) { return fakeAsyncZoneSpec.getRealSystemTime(); } else { return delegate.apply(self, args); } }; }); // patch runAllTicks(), run all microTasks inside fakeAsync api.patchMethod(Timer, 'runAllTicks', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { fakeAsyncZoneSpec.flushMicrotasks(); } else { return delegate.apply(self, args); } }; }); // patch runAllTimers(), run all macroTasks inside fakeAsync api.patchMethod(Timer, 'runAllTimers', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { fakeAsyncZoneSpec.flush(100, true); } else { return delegate.apply(self, args); } }; }); // patch advanceTimersByTime(), call tick() in the fakeAsyncTest api.patchMethod(Timer, 'advanceTimersByTime', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { fakeAsyncZoneSpec.tick(args[0]); } else { return delegate.apply(self, args); } }; }); // patch runOnlyPendingTimers(), call flushOnlyPendingTimers() in the fakeAsyncTest api.patchMethod(Timer, 'runOnlyPendingTimers', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { fakeAsyncZoneSpec.flushOnlyPendingTimers(); } else { return delegate.apply(self, args); } }; }); // patch advanceTimersToNextTimer(), call tickToNext() in the fakeAsyncTest api.patchMethod(Timer, 'advanceTimersToNextTimer', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { fakeAsyncZoneSpec.tickToNext(args[0]); } else { return delegate.apply(self, args); } }; }); // patch clearAllTimers(), call removeAllTimers() in the fakeAsyncTest api.patchMethod(Timer, 'clearAllTimers', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { fakeAsyncZoneSpec.removeAllTimers(); } else { return delegate.apply(self, args); } }; }); // patch getTimerCount(), call getTimerCount() in the fakeAsyncTest api.patchMethod(Timer, 'getTimerCount', (delegate) => { return function (self, args) { const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); if (fakeAsyncZoneSpec) { return fakeAsyncZoneSpec.getTimerCount(); } else { return delegate.apply(self, args); } }; }); }; }); } function patchMocha(Zone) { Zone.__load_patch('mocha', (global, Zone) => { const Mocha = global.Mocha; if (typeof Mocha === 'undefined') { // return if Mocha is not available, because now zone-testing // will load mocha patch with jasmine/jest patch return; } if (typeof Zone === 'undefined') { throw new Error('Missing Zone.js'); } const ProxyZoneSpec = Zone['ProxyZoneSpec']; const SyncTestZoneSpec = Zone['SyncTestZoneSpec']; if (!ProxyZoneSpec) { throw new Error('Missing ProxyZoneSpec'); } if (Mocha['__zone_patch__']) { throw new Error('"Mocha" has already been patched with "Zone".'); } Mocha['__zone_patch__'] = true; const rootZone = Zone.current; const syncZone = rootZone.fork(new SyncTestZoneSpec('Mocha.describe')); let testZone = null; const suiteZone = rootZone.fork(new ProxyZoneSpec()); const mochaOriginal = { after: global.after, afterEach: global.afterEach, before: global.before, beforeEach: global.beforeEach, describe: global.describe, it: global.it, }; function modifyArguments(args, syncTest, asyncTest) { for (let i = 0; i < args.length; i++) { let arg = args[i]; if (typeof arg === 'function') { // The `done` callback is only passed through if the function expects at // least one argument. // Note we have to make a function with correct number of arguments, // otherwise mocha will // think that all functions are sync or async. args[i] = arg.length === 0 ? syncTest(arg) : asyncTest(arg); // Mocha uses toString to view the test body in the result list, make sure we return the // correct function body args[i].toString = function () { return arg.toString(); }; } } return args; } function wrapDescribeInZone(args) { const syncTest = function (fn) { return function () { return syncZone.run(fn, this, arguments); }; }; return modifyArguments(args, syncTest); } function wrapTestInZone(args) { const asyncTest = function (fn) { return function (done) { return testZone.run(fn, this, [done]); }; }; const syncTest = function (fn) { return function () { return testZone.run(fn, this); }; }; return modifyArguments(args, syncTest, asyncTest); } function wrapSuiteInZone(args) { const asyncTest = function (fn) { return function (done) { return suiteZone.run(fn, this, [done]); }; }; const syncTest = function (fn) { return function () { return suiteZone.run(fn, this); }; }; return modifyArguments(args, syncTest, asyncTest); } global.describe = global.suite = function () { return mochaOriginal.describe.apply(this, wrapDescribeInZone(arguments)); }; global.xdescribe = global.suite.skip = global.describe.skip = function () { return mochaOriginal.describe.skip.apply(this, wrapDescribeInZone(arguments)); }; global.describe.only = global.suite.only = function () { return mochaOriginal.describe.only.apply(this, wrapDescribeInZone(arguments)); }; global.it = global.specify = global.test = function () { return mochaOriginal.it.apply(this, wrapTestInZone(arguments)); }; global.xit = global.xspecify = global.it.skip = function () { return mochaOriginal.it.skip.apply(this, wrapTestInZone(arguments)); }; global.it.only = global.test.only = function () { return mochaOriginal.it.only.apply(this, wrapTestInZone(arguments)); }; global.after = global.suiteTeardown = function () { return mochaOriginal.after.apply(this, wrapSuiteInZone(arguments)); }; global.afterEach = global.teardown = function () { return mochaOriginal.afterEach.apply(this, wrapTestInZone(arguments)); }; global.before = global.suiteSetup = function () { return mochaOriginal.before.apply(this, wrapSuiteInZone(arguments)); }; global.beforeEach = global.setup = function () { return mochaOriginal.beforeEach.apply(this, wrapTestInZone(arguments)); }; ((originalRunTest, originalRun) => { Mocha.Runner.prototype.runTest = function (fn) { Zone.current.scheduleMicroTask('mocha.forceTask', () => { originalRunTest.call(this, fn); }); }; Mocha.Runner.prototype.run = function (fn) { this.on('test', (e) => { testZone = rootZone.fork(new ProxyZoneSpec()); }); this.on('fail', (test, err) => { const proxyZoneSpec = testZone && testZone.get('ProxyZoneSpec'); if (proxyZoneSpec && err) { try { // try catch here in case err.message is not writable err.message += proxyZoneSpec.getAndClearPendingTasksInfo(); } catch (error) { } } }); return originalRun.call(this, fn); }; })(Mocha.Runner.prototype.runTest, Mocha.Runner.prototype.run); }); } const global$2 = globalThis; // __Zone_symbol_prefix global can be used to override the default zone // symbol prefix with a custom one if needed. function __symbol__(name) { const symbolPrefix = global$2['__Zone_symbol_prefix'] || '__zone_symbol__'; return symbolPrefix + name; } const __global = (typeof window !== 'undefined' && window) || (typeof self !== 'undefined' && self) || global; class AsyncTestZoneSpec { // Needs to be a getter and not a plain property in order run this just-in-time. Otherwise // `__symbol__` would be evaluated during top-level execution prior to the Zone prefix being // changed for tests. static get symbolParentUnresolved() { return __symbol__('parentUnresolved'); } constructor(finishCallback, failCallback, namePrefix) { this.finishCallback = finishCallback; this.failCallback = failCallback; this._pendingMicroTasks = false; this._pendingMacroTasks = false; this._alreadyErrored = false; this._isSync = false; this._existingFinishTimer = null; this.entryFunction = null; this.runZone = Zone.current; this.unresolvedChainedPromiseCount = 0; this.supportWaitUnresolvedChainedPromise = false; this.name = 'asyncTestZone for ' + namePrefix; this.properties = { 'AsyncTestZoneSpec': this }; this.supportWaitUnresolvedChainedPromise = __global[__symbol__('supportWaitUnResolvedChainedPromise')] === true; } isUnresolvedChainedPromisePending() { return this.unresolvedChainedPromiseCount > 0; } _finishCallbackIfDone() { // NOTE: Technically the `onHasTask` could fire together with the initial synchronous // completion in `onInvoke`. `onHasTask` might call this method when it captured e.g. // microtasks in the proxy zone that now complete as part of this async zone run. // Consider the following scenario: // 1. A test `beforeEach` schedules a microtask in the ProxyZone. // 2. An actual empty `it` spec executes in the AsyncTestZone` (using e.g. `waitForAsync`). // 3. The `onInvoke` invokes `_finishCallbackIfDone` because the spec runs synchronously. // 4. We wait the scheduled timeout (see below) to account for unhandled promises. // 5. The microtask from (1) finishes and `onHasTask` is invoked. // --> We register a second `_finishCallbackIfDone` even though we have scheduled a timeout. // If the finish timeout from below is already scheduled, terminate the existing scheduled // finish invocation, avoiding calling `jasmine` `done` multiple times. *Note* that we would // want to schedule a new finish callback in case the task state changes again. if (this._existingFinishTimer !== null) { clearTimeout(this._existingFinishTimer); this._existingFinishTimer = null; } if (!(this._pendingMicroTasks || this._pendingMacroTasks || (this.supportWaitUnresolvedChainedPromise && this.isUnresolvedChainedPromisePending()))) { // We wait until the next tick because we would like to catch unhandled promises which could // cause test logic to be executed. In such cases we cannot finish with tasks pending then. this.runZone.run(() => { this._existingFinishTimer = setTimeout(() => { if (!this._alreadyErrored && !(this._pendingMicroTasks || this._pendingMacroTasks)) { this.finishCallback(); } }, 0); }); } } patchPromiseForTest() { if (!this.supportWaitUnresolvedChainedPromise) { return; } const patchPromiseForTest = Promise[Zone.__symbol__('patchPromiseForTest')]; if (patchPromiseForTest) { patchPromiseForTest(); } } unPatchPromiseForTest() { if (!this.supportWaitUnresolvedChainedPromise) { return; } const unPatchPromiseForTest = Promise[Zone.__symbol__('unPatchPromiseForTest')]; if (unPatchPromiseForTest) { unPatchPromiseForTest(); } } onScheduleTask(delegate, current, target, task) { if (task.type !== 'eventTask') { this._isSync = false; } if (task.type === 'microTask' && task.data && task.data instanceof Promise) { // check whether the promise is a chained promise if (task.data[AsyncTestZoneSpec.symbolParentUnresolved] === true) { // chained promise is being scheduled this.unresolvedChainedPromiseCount--; } } return delegate.scheduleTask(target, task); } onInvokeTask(delegate, current, target, task, applyThis, applyArgs) { if (task.type !== 'eventTask') { this._isSync = false; } return delegate.invokeTask(target, task, applyThis, applyArgs); } onCancelTask(delegate, current, target, task) { if (task.type !== 'eventTask') { this._isSync = false; } return delegate.cancelTask(target, task); } // Note - we need to use onInvoke at the moment to call finish when a test is // fully synchronous. TODO(juliemr): remove this when the logic for // onHasTask changes and it calls whenever the task queues are dirty. // updated by(JiaLiPassion), only call finish callback when no task // was scheduled/invoked/canceled. onInvoke(parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) { if (!this.entryFunction) { this.entryFunction = delegate; } try { this._isSync = true; return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source); } finally { // We need to check the delegate is the same as entryFunction or not. // Consider the following case. // // asyncTestZone.run(() => { // Here the delegate will be the entryFunction // Zone.current.run(() => { // Here the delegate will not be the entryFunction // }); // }); // // We only want to check whether there are async tasks scheduled // for the entry function. if (this._isSync && this.entryFunction === delegate) { this._finishCallbackIfDone(); } } } onHandleError(parentZoneDelegate, currentZone, targetZone, error) { // Let the parent try to handle the error. const result = parentZoneDelegate.handleError(targetZone, error); if (result) { this.failCallback(error); this._alreadyErrored = true; } return false; } onHasTask(delegate, current, target, hasTaskState) { delegate.hasTask(target, hasTaskState); // We should only trigger finishCallback when the target zone is the AsyncTestZone // Consider the following cases. // // const childZone = asyncTestZone.fork({ // name: 'child', // onHasTask: ... // }); // // So we have nested zones declared the onHasTask hook, in this case, // the onHasTask will be triggered twice, and cause the finishCallbackIfDone() // is also be invoked twice. So we need to only trigger the finishCallbackIfDone() // when the current zone is the same as the target zone. if (current !== target) { return; } if (hasTaskState.change == 'microTask') { this._pendingMicroTasks = hasTaskState.microTask; this._finishCallbackIfDone(); } else if (hasTaskState.change == 'macroTask') { this._pendingMacroTasks = hasTaskState.macroTask; this._finishCallbackIfDone(); } } } function patchAsyncTest(Zone) { // Export the class so that new instances can be created with proper // constructor params. Zone['AsyncTestZoneSpec'] = AsyncTestZoneSpec; Zone.__load_patch('asynctest', (global, Zone, api) => { /** * Wraps a test function in an asynchronous test zone. The test will automatically * complete when all asynchronous calls within this zone are done. */ Zone[api.symbol('asyncTest')] = function asyncTest(fn) { // If we're running using the Jasmine test framework, adapt to call the 'done' // function when asynchronous activity is finished. if (global.jasmine) { // Not using an arrow function to preserve context passed from call site return function (done) { if (!done) { // if we run beforeEach in @angular/core/testing/testing_internal then we get no done // fake it here and assume sync. done = function () { }; done.fail = function (e) { throw e; }; } runInTestZone(fn, this, done, (err) => { if (typeof err === 'string') { return done.fail(new Error(err)); } else { done.fail(err); } }); }; } // Otherwise, return a promise which will resolve when asynchronous activity // is finished. This will be correctly consumed by the Mocha framework with // it('...', async(myFn)); or can be used in a custom framework. // Not using an arrow function to preserve context passed from call site return function () { return new Promise((finishCallback, failCallback) => { runInTestZone(fn, this, finishCallback, failCallback); }); }; }; function runInTestZone(fn, context, finishCallback, failCallback) { const currentZone = Zone.current; const AsyncTestZoneSpec = Zone['AsyncTestZoneSpec']; if (AsyncTestZoneSpec === undefined) { throw new Error('AsyncTestZoneSpec is needed for the async() test helper but could not be found. ' + 'Please make sure that your environment includes zone.js/plugins/async-test'); } const ProxyZoneSpec = Zone['ProxyZoneSpec']; if (!ProxyZoneSpec) { throw new Error('ProxyZoneSpec is needed for the async() test helper but could not be found. ' + 'Please make sure that your environment includes zone.js/plugins/proxy'); } const proxyZoneSpec = ProxyZoneSpec.get(); ProxyZoneSpec.assertPresent(); // We need to create the AsyncTestZoneSpec outside the ProxyZone. // If we do it in ProxyZone then we will get to infinite recursion. const proxyZone = Zone.current.getZoneWith('ProxyZoneSpec'); const previousDelegate = proxyZoneSpec.getDelegate(); proxyZone.parent.run(() => { const testZoneSpec = new AsyncTestZoneSpec(() => { // Need to restore the original zone. if (proxyZoneSpec.getDelegate() == testZoneSpec) { // Only reset the zone spec if it's // still this one. Otherwise, assume // it's OK. proxyZoneSpec.setDelegate(previousDelegate); } testZoneSpec.unPatchPromiseForTest(); currentZone.run(() => { finishCallback(); }); }, (error) => { // Need to restore the original zone. if (proxyZoneSpec.getDelegate() == testZoneSpec) { // Only reset the zone spec if it's sill this one. Otherwise, assume it's OK. proxyZoneSpec.setDelegate(previousDelegate); } testZoneSpec.unPatchPromiseForTest(); currentZone.run(() => { failCallback(error); });