zone.js
Version:
Zones for JavaScript
1,029 lines (1,025 loc) • 97.8 kB
JavaScript
'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);
});