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

Commit

Permalink
feat(testing): can display pending tasks info when test timeout in ja…
Browse files Browse the repository at this point in the history
…smine/mocha (#1038)
  • Loading branch information
JiaLiPassion authored and mhevery committed Mar 30, 2018
1 parent c8c5990 commit 57bc80c
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 69 deletions.
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 @@ -161,6 +161,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

0 comments on commit 57bc80c

Please sign in to comment.