Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

feat(testing): can display pending tasks info when test timeout in jasmine/mocha #1038

Merged
merged 1 commit into from
Mar 30, 2018
Merged
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
111 changes: 50 additions & 61 deletions lib/jasmine/jasmine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@
'use strict';
(() => {
const __extends = function(d: any, b: any) {
for (const p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
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 (__ as any)());
d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new (__ as any)());
};
const _global: any = typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;
const _global: any =
typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;
// 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');
Expand All @@ -27,10 +26,8 @@
throw new Error(`'jasmine' has already been patched with 'Zone'.`);
(jasmine as any)['__zone_patch__'] = true;

const SyncTestZoneSpec: { new (name: string): ZoneSpec } = (Zone as any)[
'SyncTestZoneSpec'
];
const ProxyZoneSpec: { new (): ZoneSpec } = (Zone as any)['ProxyZoneSpec'];
const SyncTestZoneSpec: {new (name: string): ZoneSpec} = (Zone as any)['SyncTestZoneSpec'];
const ProxyZoneSpec: {new (): ZoneSpec} = (Zone as any)['ProxyZoneSpec'];
if (!SyncTestZoneSpec) throw new Error('Missing: SyncTestZoneSpec');
if (!ProxyZoneSpec) throw new Error('Missing: ProxyZoneSpec');

Expand All @@ -46,42 +43,28 @@
const jasmineEnv: any = jasmine.getEnv();
['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {
let originalJasmineFn: Function = jasmineEnv[methodName];
jasmineEnv[methodName] = function(
description: string,
specDefinitions: Function
) {
return originalJasmineFn.call(
this,
description,
wrapDescribeInZone(specDefinitions)
);
jasmineEnv[methodName] = function(description: string, specDefinitions: Function) {
return originalJasmineFn.call(this, description, wrapDescribeInZone(specDefinitions));
};
});
['it', 'xit', 'fit'].forEach(methodName => {
let originalJasmineFn: Function = jasmineEnv[methodName];
jasmineEnv[symbol(methodName)] = originalJasmineFn;
jasmineEnv[methodName] = function(
description: string,
specDefinitions: Function,
timeout: number
) {
description: string, specDefinitions: Function, timeout: number) {
arguments[1] = wrapTestInZone(specDefinitions);
return originalJasmineFn.apply(this, arguments);
};
});
['beforeEach', 'afterEach'].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);
};
});
const originalClockFn: Function = ((jasmine as any)[symbol('clock')] =
jasmine['clock']);
const originalClockFn: Function = ((jasmine as any)[symbol('clock')] = jasmine['clock']);
(jasmine as any)['clock'] = function() {
const clock = originalClockFn.apply(this, arguments);
const originalTick = (clock[symbol('tick')] = clock.tick);
Expand All @@ -98,17 +81,13 @@
if (fakeAsyncZoneSpec) {
const dateTime = arguments[0];
return fakeAsyncZoneSpec.setCurrentRealTime.apply(
fakeAsyncZoneSpec,
dateTime && typeof dateTime.getTime === 'function'
? [dateTime.getTime()]
: arguments
);
fakeAsyncZoneSpec,
dateTime && typeof dateTime.getTime === 'function' ? [dateTime.getTime()] : arguments);
}
return originalMockDate.apply(this, arguments);
};
['install', 'uninstall'].forEach(methodName => {
const originalClockFn: Function = (clock[symbol(methodName)] =
clock[methodName]);
const originalClockFn: Function = (clock[symbol(methodName)] = clock[methodName]);
clock[methodName] = function() {
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
if (FakeAsyncTestZoneSpec) {
Expand All @@ -131,11 +110,7 @@
};
}

function runInTestZone(
testBody: Function,
queueRunner: any,
done?: Function
) {
function runInTestZone(testBody: Function, queueRunner: any, done?: Function) {
const isClockInstalled = !!(jasmine as any)[symbol('clockInstalled')];
const testProxyZoneSpec = queueRunner.testProxyZoneSpec;
const testProxyZone = queueRunner.testProxyZone;
Expand Down Expand Up @@ -170,28 +145,23 @@
// 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: Function) {
return runInTestZone(testBody, this.queueRunner, done);
}
: function() {
return runInTestZone(testBody, this.queueRunner);
})
);
return (testBody && (testBody.length ? function(done: Function) {
return runInTestZone(testBody, this.queueRunner, done);
} : function() {
return runInTestZone(testBody, this.queueRunner);
}));
}
interface QueueRunner {
execute(): void;
}
interface QueueRunnerAttrs {
queueableFns: { fn: Function }[];
queueableFns: {fn: Function}[];
onComplete: () => void;
clearStack: (fn: any) => void;
onException: (error: any) => void;
catchException: () => boolean;
userContext: any;
timeout: { setTimeout: Function; clearTimeout: Function };
timeout: {setTimeout: Function; clearTimeout: Function};
fail: () => void;
}

Expand All @@ -201,9 +171,9 @@
(jasmine as any).QueueRunner = (function(_super) {
__extends(ZoneQueueRunner, _super);
function ZoneQueueRunner(attrs: {
onComplete: Function;
userContext?: any;
timeout?: { setTimeout: Function; clearTimeout: Function };
onComplete: Function; userContext?: any;
timeout?: {setTimeout: Function; clearTimeout: Function};
onException?: (error: any) => void;
}) {
attrs.onComplete = (fn => () => {
// All functions are done, clear the test zone.
Expand All @@ -221,6 +191,7 @@
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 as any).UserContext) {
Expand All @@ -234,6 +205,26 @@
}
attrs.userContext.queueRunner = this;
}

// patch attrs.onException
const onException = attrs.onException;
attrs.onException = function(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;
if (proxyZoneSpec) {
const pendingTasksInfo = proxyZoneSpec.getAndClearPendingTasksInfo();
error.message += pendingTasksInfo;
}
}
if (onException) {
onException.call(this, error);
}
};

_super.call(this, attrs);
}
ZoneQueueRunner.prototype.execute = function() {
Expand All @@ -247,8 +238,7 @@
zone = zone.parent;
}

if (!isChildOfAmbientZone)
throw new Error('Unexpected Zone: ' + Zone.current.name);
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
Expand All @@ -268,9 +258,8 @@
// 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)
);
Zone.current.scheduleMicroTask(
'jasmine.execute().forceTask', () => QueueRunner.prototype.execute.call(this));
} else {
_super.prototype.execute.call(this);
}
Expand Down
7 changes: 7 additions & 0 deletions lib/mocha/mocha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@
testZone = rootZone.fork(new ProxyZoneSpec());
});

this.on('fail', (test:any, err: any) => {
const proxyZoneSpec = testZone && testZone.get('ProxyZoneSpec');
if (proxyZoneSpec && err) {
err.message += proxyZoneSpec.getAndClearPendingTasksInfo();
}
});

return originalRun.call(this, fn);
};

Expand Down
10 changes: 3 additions & 7 deletions lib/zone-spec/async-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,14 @@
class AsyncTestZoneSpec implements ZoneSpec {
static symbolParentUnresolved = Zone.__symbol__('parentUnresolved');

_finishCallback: Function;
_failCallback: Function;
_pendingMicroTasks: boolean = false;
_pendingMacroTasks: boolean = false;
_alreadyErrored: boolean = false;
_isSync: boolean = false;
runZone = Zone.current;
unresolvedChainedPromiseCount = 0;

constructor(finishCallback: Function, failCallback: Function, namePrefix: string) {
this._finishCallback = finishCallback;
this._failCallback = failCallback;
constructor(private finishCallback: Function, private failCallback: Function, namePrefix: string) {
this.name = 'asyncTestZone for ' + namePrefix;
this.properties = {
'AsyncTestZoneSpec': this
Expand All @@ -33,7 +29,7 @@ class AsyncTestZoneSpec implements ZoneSpec {
this.runZone.run(() => {
setTimeout(() => {
if (!this._alreadyErrored && !(this._pendingMicroTasks || this._pendingMacroTasks)) {
this._finishCallback();
this.finishCallback();
}
}, 0);
});
Expand Down Expand Up @@ -115,7 +111,7 @@ class AsyncTestZoneSpec implements ZoneSpec {
// Let the parent try to handle the error.
const result = parentZoneDelegate.handleError(targetZone, error);
if (result) {
this._failCallback(error);
this.failCallback(error);
this._alreadyErrored = true;
}
return false;
Expand Down
43 changes: 42 additions & 1 deletion lib/zone-spec/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class ProxyZoneSpec implements ZoneSpec {
lastTaskState: HasTaskState = null;
isNeedToTriggerHasTask = false;

private tasks: Task[] = [];

static get(): ProxyZoneSpec {
return Zone.current.get('ProxyZoneSpec');
}
Expand All @@ -36,7 +38,6 @@ class ProxyZoneSpec implements ZoneSpec {
this.setDelegate(defaultSpecDelegate);
}


setDelegate(delegateSpec: ZoneSpec) {
const isNewDelegate = this._delegateSpec !== delegateSpec;
this._delegateSpec = delegateSpec;
Expand Down Expand Up @@ -72,6 +73,37 @@ class ProxyZoneSpec implements ZoneSpec {
}
}

removeFromTasks(task: Task) {
if (!this.tasks) {
return;
}
for (let i = 0; i < this.tasks.length; i ++) {
if (this.tasks[i] === task) {
this.tasks.splice(i, 1);
return;
}
}
}

getAndClearPendingTasksInfo() {
if (this.tasks.length === 0) {
return '';
}
const taskInfo = this.tasks.map((task: Task) => {
const dataInfo = task.data &&
Object.keys(task.data)
.map((key: string) => {
return key + ':' + (task.data as any)[key];
})
.join(',');
return `type: ${task.type}, source: ${task.source}, args: {${dataInfo}}`;
});
const pendingTasksInfo = '--Pendng async tasks are: [' + taskInfo + ']';
// clear tasks
this.tasks = [];

return pendingTasksInfo;
}

onFork(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, zoneSpec: ZoneSpec):
Zone {
Expand Down Expand Up @@ -118,6 +150,9 @@ class ProxyZoneSpec implements ZoneSpec {

onScheduleTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
Task {
if (task.type !== 'eventTask') {
this.tasks.push(task);
}
if (this._delegateSpec && this._delegateSpec.onScheduleTask) {
return this._delegateSpec.onScheduleTask(parentZoneDelegate, currentZone, targetZone, task);
} else {
Expand All @@ -128,6 +163,9 @@ class ProxyZoneSpec implements ZoneSpec {
onInvokeTask(
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
applyThis: any, applyArgs: any): any {
if (task.type !== 'eventTask') {
this.removeFromTasks(task);
}
this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
if (this._delegateSpec && this._delegateSpec.onInvokeTask) {
return this._delegateSpec.onInvokeTask(
Expand All @@ -139,6 +177,9 @@ class ProxyZoneSpec implements ZoneSpec {

onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
any {
if (task.type !== 'eventTask') {
this.removeFromTasks(task);
}
this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
if (this._delegateSpec && this._delegateSpec.onCancelTask) {
return this._delegateSpec.onCancelTask(parentZoneDelegate, currentZone, targetZone, task);
Expand Down