Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 80 additions & 68 deletions packages/zone.js/lib/jasmine/jasmine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,18 @@

import {ZoneType} from '../zone-impl';

'use strict';
('use strict');
declare let jest: any;

export function patchJasmine(Zone: ZoneType): void {
Zone.__load_patch('jasmine', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
const __extends = function(d: any, b: any) {
for (const p in b)
if (b.hasOwnProperty(p)) d[p] = b[p];
const __extends = function (d: any, b: any) {
for (const p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __(this: Object) {
this.constructor = d;
}
d.prototype =
b === null ? Object.create(b) : ((__.prototype = b.prototype), new (__ as any)());
b === null ? Object.create(b) : ((__.prototype = b.prototype), new (__ as any)());
};
// 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)
Expand Down Expand Up @@ -51,36 +50,39 @@ export function patchJasmine(Zone: ZoneType): void {
// 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 enableAutoFakeAsyncWhenClockPatched =
!disablePatchingJasmineClock &&
(global[symbol('fakeAsyncPatchLock')] === true ||
global[symbol('fakeAsyncAutoFakeAsyncWhenClockPatched')] === true);

const ignoreUnhandledRejection = global[symbol('ignoreUnhandledRejection')] === true;

if (!ignoreUnhandledRejection) {
const globalErrors = (jasmine as any).GlobalErrors;
if (globalErrors && !(jasmine as any)[symbol('GlobalErrors')]) {
(jasmine as any)[symbol('GlobalErrors')] = globalErrors;
(jasmine as any).GlobalErrors = function() {
(jasmine as any).GlobalErrors = function () {
const instance = new globalErrors();
const originalInstall = instance.install;
if (originalInstall && !instance[symbol('install')]) {
instance[symbol('install')] = originalInstall;
instance.install = function() {
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: any[] = isNode ? process.listeners('unhandledRejection') :
global.eventListeners('unhandledrejection');
const originalHandlers: any[] = isNode
? process.listeners('unhandledRejection')
: global.eventListeners('unhandledrejection');
const result = originalInstall.apply(this, arguments);
isNode ? process.removeAllListeners('unhandledRejection') :
global.removeAllListeners('unhandledrejection');
isNode
? process.removeAllListeners('unhandledRejection')
: global.removeAllListeners('unhandledrejection');
if (originalHandlers) {
originalHandlers.forEach(handler => {
originalHandlers.forEach((handler) => {
if (isNode) {
process.on('unhandledRejection', handler);
} else {
Expand All @@ -98,26 +100,32 @@ export function patchJasmine(Zone: ZoneType): void {

// Monkey patch all of the jasmine DSL so that each function runs in appropriate zone.
const jasmineEnv: any = jasmine.getEnv();
['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {
['describe', 'xdescribe', 'fdescribe'].forEach((methodName) => {
let originalJasmineFn: Function = jasmineEnv[methodName];
jasmineEnv[methodName] = function(description: string, specDefinitions: Function) {
jasmineEnv[methodName] = function (description: string, specDefinitions: Function) {
return originalJasmineFn.call(
this, description, wrapDescribeInZone(description, specDefinitions));
this,
description,
wrapDescribeInZone(description, specDefinitions),
);
};
});
['it', 'xit', 'fit'].forEach(methodName => {
['it', 'xit', 'fit'].forEach((methodName) => {
let originalJasmineFn: Function = jasmineEnv[methodName];
jasmineEnv[symbol(methodName)] = originalJasmineFn;
jasmineEnv[methodName] = function(
description: string, specDefinitions: Function, timeout: number) {
jasmineEnv[methodName] = function (
description: string,
specDefinitions: Function,
timeout: number,
) {
arguments[1] = wrapTestInZone(specDefinitions);
return originalJasmineFn.apply(this, arguments);
};
});
['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach(methodName => {
['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach((methodName) => {
let originalJasmineFn: Function = jasmineEnv[methodName];
jasmineEnv[symbol(methodName)] = originalJasmineFn;
jasmineEnv[methodName] = function(specDefinitions: Function, timeout: number) {
jasmineEnv[methodName] = function (specDefinitions: Function, timeout: number) {
arguments[0] = wrapTestInZone(specDefinitions);
return originalJasmineFn.apply(this, arguments);
};
Expand All @@ -127,35 +135,37 @@ export function patchJasmine(Zone: ZoneType): void {
// need to patch jasmine.clock().mockDate and jasmine.clock().tick() so
// they can work properly in FakeAsyncTest
const originalClockFn: Function = ((jasmine as any)[symbol('clock')] = jasmine['clock']);
(jasmine as any)['clock'] = function() {
(jasmine as any)['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() {
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() {
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);
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 => {
['install', 'uninstall'].forEach((methodName) => {
const originalClockFn: Function = (clock[symbol(methodName)] = clock[methodName]);
clock[methodName] = function() {
clock[methodName] = function () {
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
if (FakeAsyncTestZoneSpec) {
(jasmine as any)[symbol('clockInstalled')] = 'install' === methodName;
Expand All @@ -174,15 +184,18 @@ export function patchJasmine(Zone: ZoneType): void {
if (!(jasmine as any)[Zone.__symbol__('createSpyObj')]) {
const originalCreateSpyObj = jasmine.createSpyObj;
(jasmine as any)[Zone.__symbol__('createSpyObj')] = originalCreateSpyObj;
jasmine.createSpyObj = function() {
jasmine.createSpyObj = function () {
const args: any = Array.prototype.slice.call(arguments);
const propertyNames = args.length >= 3 ? args[2] : null;
let spyObj: any;
if (propertyNames) {
const defineProperty = Object.defineProperty;
Object.defineProperty = function<T>(obj: T, p: PropertyKey, attributes: any) {
return defineProperty.call(
this, obj, p, {...attributes, configurable: true, enumerable: true}) as T;
Object.defineProperty = function <T>(obj: T, p: PropertyKey, attributes: any) {
return defineProperty.call(this, obj, p, {
...attributes,
configurable: true,
enumerable: true,
}) as T;
};
try {
spyObj = originalCreateSpyObj.apply(this, args);
Expand All @@ -201,16 +214,20 @@ export function patchJasmine(Zone: ZoneType): void {
* synchronous-only zone.
*/
function wrapDescribeInZone(description: string, describeBody: Function): Function {
return function(this: unknown) {
return function (this: unknown) {
// 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 as any) as any[]);
return syncZone.run(describeBody, this, arguments as any as any[]);
};
}

function runInTestZone(
testBody: Function, applyThis: any, queueRunner: QueueRunner, done?: Function) {
testBody: Function,
applyThis: any,
queueRunner: QueueRunner,
done?: Function,
) {
const isClockInstalled = !!(jasmine as any)[symbol('clockInstalled')];
const testProxyZoneSpec = queueRunner.testProxyZoneSpec!;
const testProxyZone = queueRunner.testProxyZone!;
Expand Down Expand Up @@ -239,16 +256,20 @@ export function patchJasmine(Zone: ZoneType): void {
// 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(this: QueueRunnerUserContext, done: Function) {
return runInTestZone(testBody, this, this.queueRunner!, done);
} : function(this: QueueRunnerUserContext) {
return runInTestZone(testBody, this, this.queueRunner!);
}));
testBody &&
(testBody.length
? function (this: QueueRunnerUserContext, done: Function) {
return runInTestZone(testBody, this, this.queueRunner!, done);
}
: function (this: QueueRunnerUserContext) {
return runInTestZone(testBody, this, this.queueRunner!);
})
);
}
interface QueueRunner {
execute(): void;
testProxyZoneSpec: ZoneSpec|null;
testProxyZone: Zone|null;
testProxyZoneSpec: ZoneSpec | null;
testProxyZone: Zone | null;
}
interface QueueRunnerAttrs {
queueableFns: {fn: Function}[];
Expand All @@ -264,15 +285,15 @@ export function patchJasmine(Zone: ZoneType): void {
const QueueRunner = (jasmine as any).QueueRunner as {
new (attrs: QueueRunnerAttrs): QueueRunner;
};
(jasmine as any).QueueRunner = (function(_super) {
(jasmine as any).QueueRunner = (function (_super) {
__extends(ZoneQueueRunner, _super);
function ZoneQueueRunner(this: QueueRunner, attrs: QueueRunnerAttrs) {
if (attrs.onComplete) {
attrs.onComplete = (fn => () => {
attrs.onComplete = ((fn) => () => {
// All functions are done, clear the test zone.
this.testProxyZone = null;
this.testProxyZoneSpec = null;
ambientZone.scheduleMicroTask('jasmine.onComplete', fn);
ambientZone.run(fn);
})(attrs.onComplete);
}

Expand All @@ -282,7 +303,7 @@ export function patchJasmine(Zone: ZoneType): void {
// should run setTimeout inside jasmine outside of zone
attrs.timeout = {
setTimeout: nativeSetTimeout ? nativeSetTimeout : global.setTimeout,
clearTimeout: nativeClearTimeout ? nativeClearTimeout : global.clearTimeout
clearTimeout: nativeClearTimeout ? nativeClearTimeout : global.clearTimeout,
};
}

Expand All @@ -302,10 +323,12 @@ export function patchJasmine(Zone: ZoneType): void {

// patch attrs.onException
const onException = attrs.onException;
attrs.onException = function(this: undefined|QueueRunner, error: any) {
if (error &&
error.message ===
'Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.') {
attrs.onException = function (this: undefined | QueueRunner, error: any) {
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: any = this && this.testProxyZoneSpec;
Expand All @@ -314,8 +337,7 @@ export function patchJasmine(Zone: ZoneType): void {
try {
// try catch here in case error.message is not writable
error.message += pendingTasksInfo;
} catch (err) {
}
} catch (err) {}
}
}
if (onException) {
Expand All @@ -325,8 +347,8 @@ export function patchJasmine(Zone: ZoneType): void {

_super.call(this, attrs);
}
ZoneQueueRunner.prototype.execute = function() {
let zone: Zone|null = Zone.current;
ZoneQueueRunner.prototype.execute = function () {
let zone: Zone | null = Zone.current;
let isChildOfAmbientZone = false;
while (zone) {
if (zone === ambientZone) {
Expand All @@ -350,17 +372,7 @@ export function patchJasmine(Zone: ZoneType): void {

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);
}
_super.prototype.execute.call(this);
};
return ZoneQueueRunner;
})(QueueRunner);
Expand Down
Loading