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

Commit 57bc80c

Browse files
JiaLiPassionmhevery
authored andcommitted
feat(testing): can display pending tasks info when test timeout in jasmine/mocha (#1038)
1 parent c8c5990 commit 57bc80c

File tree

4 files changed

+102
-69
lines changed

4 files changed

+102
-69
lines changed

lib/jasmine/jasmine.ts

Lines changed: 50 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@
99
'use strict';
1010
(() => {
1111
const __extends = function(d: any, b: any) {
12-
for (const p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
12+
for (const p in b)
13+
if (b.hasOwnProperty(p)) d[p] = b[p];
1314
function __() {
1415
this.constructor = d;
1516
}
16-
d.prototype =
17-
b === null
18-
? Object.create(b)
19-
: ((__.prototype = b.prototype), new (__ as any)());
17+
d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new (__ as any)());
2018
};
21-
const _global: any = typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;
19+
const _global: any =
20+
typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global;
2221
// Patch jasmine's describe/it/beforeEach/afterEach functions so test code always runs
2322
// in a testZone (ProxyZone). (See: angular/zone.js#91 & angular/angular#10503)
2423
if (!Zone) throw new Error('Missing: zone.js');
@@ -27,10 +26,8 @@
2726
throw new Error(`'jasmine' has already been patched with 'Zone'.`);
2827
(jasmine as any)['__zone_patch__'] = true;
2928

30-
const SyncTestZoneSpec: { new (name: string): ZoneSpec } = (Zone as any)[
31-
'SyncTestZoneSpec'
32-
];
33-
const ProxyZoneSpec: { new (): ZoneSpec } = (Zone as any)['ProxyZoneSpec'];
29+
const SyncTestZoneSpec: {new (name: string): ZoneSpec} = (Zone as any)['SyncTestZoneSpec'];
30+
const ProxyZoneSpec: {new (): ZoneSpec} = (Zone as any)['ProxyZoneSpec'];
3431
if (!SyncTestZoneSpec) throw new Error('Missing: SyncTestZoneSpec');
3532
if (!ProxyZoneSpec) throw new Error('Missing: ProxyZoneSpec');
3633

@@ -46,42 +43,28 @@
4643
const jasmineEnv: any = jasmine.getEnv();
4744
['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {
4845
let originalJasmineFn: Function = jasmineEnv[methodName];
49-
jasmineEnv[methodName] = function(
50-
description: string,
51-
specDefinitions: Function
52-
) {
53-
return originalJasmineFn.call(
54-
this,
55-
description,
56-
wrapDescribeInZone(specDefinitions)
57-
);
46+
jasmineEnv[methodName] = function(description: string, specDefinitions: Function) {
47+
return originalJasmineFn.call(this, description, wrapDescribeInZone(specDefinitions));
5848
};
5949
});
6050
['it', 'xit', 'fit'].forEach(methodName => {
6151
let originalJasmineFn: Function = jasmineEnv[methodName];
6252
jasmineEnv[symbol(methodName)] = originalJasmineFn;
6353
jasmineEnv[methodName] = function(
64-
description: string,
65-
specDefinitions: Function,
66-
timeout: number
67-
) {
54+
description: string, specDefinitions: Function, timeout: number) {
6855
arguments[1] = wrapTestInZone(specDefinitions);
6956
return originalJasmineFn.apply(this, arguments);
7057
};
7158
});
7259
['beforeEach', 'afterEach'].forEach(methodName => {
7360
let originalJasmineFn: Function = jasmineEnv[methodName];
7461
jasmineEnv[symbol(methodName)] = originalJasmineFn;
75-
jasmineEnv[methodName] = function(
76-
specDefinitions: Function,
77-
timeout: number
78-
) {
62+
jasmineEnv[methodName] = function(specDefinitions: Function, timeout: number) {
7963
arguments[0] = wrapTestInZone(specDefinitions);
8064
return originalJasmineFn.apply(this, arguments);
8165
};
8266
});
83-
const originalClockFn: Function = ((jasmine as any)[symbol('clock')] =
84-
jasmine['clock']);
67+
const originalClockFn: Function = ((jasmine as any)[symbol('clock')] = jasmine['clock']);
8568
(jasmine as any)['clock'] = function() {
8669
const clock = originalClockFn.apply(this, arguments);
8770
const originalTick = (clock[symbol('tick')] = clock.tick);
@@ -98,17 +81,13 @@
9881
if (fakeAsyncZoneSpec) {
9982
const dateTime = arguments[0];
10083
return fakeAsyncZoneSpec.setCurrentRealTime.apply(
101-
fakeAsyncZoneSpec,
102-
dateTime && typeof dateTime.getTime === 'function'
103-
? [dateTime.getTime()]
104-
: arguments
105-
);
84+
fakeAsyncZoneSpec,
85+
dateTime && typeof dateTime.getTime === 'function' ? [dateTime.getTime()] : arguments);
10686
}
10787
return originalMockDate.apply(this, arguments);
10888
};
10989
['install', 'uninstall'].forEach(methodName => {
110-
const originalClockFn: Function = (clock[symbol(methodName)] =
111-
clock[methodName]);
90+
const originalClockFn: Function = (clock[symbol(methodName)] = clock[methodName]);
11291
clock[methodName] = function() {
11392
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
11493
if (FakeAsyncTestZoneSpec) {
@@ -131,11 +110,7 @@
131110
};
132111
}
133112

134-
function runInTestZone(
135-
testBody: Function,
136-
queueRunner: any,
137-
done?: Function
138-
) {
113+
function runInTestZone(testBody: Function, queueRunner: any, done?: Function) {
139114
const isClockInstalled = !!(jasmine as any)[symbol('clockInstalled')];
140115
const testProxyZoneSpec = queueRunner.testProxyZoneSpec;
141116
const testProxyZone = queueRunner.testProxyZone;
@@ -170,28 +145,23 @@
170145
// The `done` callback is only passed through if the function expects at least one argument.
171146
// Note we have to make a function with correct number of arguments, otherwise jasmine will
172147
// think that all functions are sync or async.
173-
return (
174-
testBody &&
175-
(testBody.length
176-
? function(done: Function) {
177-
return runInTestZone(testBody, this.queueRunner, done);
178-
}
179-
: function() {
180-
return runInTestZone(testBody, this.queueRunner);
181-
})
182-
);
148+
return (testBody && (testBody.length ? function(done: Function) {
149+
return runInTestZone(testBody, this.queueRunner, done);
150+
} : function() {
151+
return runInTestZone(testBody, this.queueRunner);
152+
}));
183153
}
184154
interface QueueRunner {
185155
execute(): void;
186156
}
187157
interface QueueRunnerAttrs {
188-
queueableFns: { fn: Function }[];
158+
queueableFns: {fn: Function}[];
189159
onComplete: () => void;
190160
clearStack: (fn: any) => void;
191161
onException: (error: any) => void;
192162
catchException: () => boolean;
193163
userContext: any;
194-
timeout: { setTimeout: Function; clearTimeout: Function };
164+
timeout: {setTimeout: Function; clearTimeout: Function};
195165
fail: () => void;
196166
}
197167

@@ -201,9 +171,9 @@
201171
(jasmine as any).QueueRunner = (function(_super) {
202172
__extends(ZoneQueueRunner, _super);
203173
function ZoneQueueRunner(attrs: {
204-
onComplete: Function;
205-
userContext?: any;
206-
timeout?: { setTimeout: Function; clearTimeout: Function };
174+
onComplete: Function; userContext?: any;
175+
timeout?: {setTimeout: Function; clearTimeout: Function};
176+
onException?: (error: any) => void;
207177
}) {
208178
attrs.onComplete = (fn => () => {
209179
// All functions are done, clear the test zone.
@@ -221,6 +191,7 @@
221191
clearTimeout: nativeClearTimeout ? nativeClearTimeout : _global.clearTimeout
222192
};
223193
}
194+
224195
// create a userContext to hold the queueRunner itself
225196
// so we can access the testProxy in it/xit/beforeEach ...
226197
if ((jasmine as any).UserContext) {
@@ -234,6 +205,26 @@
234205
}
235206
attrs.userContext.queueRunner = this;
236207
}
208+
209+
// patch attrs.onException
210+
const onException = attrs.onException;
211+
attrs.onException = function(error: any) {
212+
if (error &&
213+
error.message ===
214+
'Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.') {
215+
// jasmine timeout, we can make the error message more
216+
// reasonable to tell what tasks are pending
217+
const proxyZoneSpec: any = this && this.testProxyZoneSpec;
218+
if (proxyZoneSpec) {
219+
const pendingTasksInfo = proxyZoneSpec.getAndClearPendingTasksInfo();
220+
error.message += pendingTasksInfo;
221+
}
222+
}
223+
if (onException) {
224+
onException.call(this, error);
225+
}
226+
};
227+
237228
_super.call(this, attrs);
238229
}
239230
ZoneQueueRunner.prototype.execute = function() {
@@ -247,8 +238,7 @@
247238
zone = zone.parent;
248239
}
249240

250-
if (!isChildOfAmbientZone)
251-
throw new Error('Unexpected Zone: ' + Zone.current.name);
241+
if (!isChildOfAmbientZone) throw new Error('Unexpected Zone: ' + Zone.current.name);
252242

253243
// This is the zone which will be used for running individual tests.
254244
// It will be a proxy zone, so that the tests function can retroactively install
@@ -268,9 +258,8 @@
268258
// addEventListener callback would think that it is the top most task and would
269259
// drain the microtask queue on element.click() which would be incorrect.
270260
// For this reason we always force a task when running jasmine tests.
271-
Zone.current.scheduleMicroTask('jasmine.execute().forceTask', () =>
272-
QueueRunner.prototype.execute.call(this)
273-
);
261+
Zone.current.scheduleMicroTask(
262+
'jasmine.execute().forceTask', () => QueueRunner.prototype.execute.call(this));
274263
} else {
275264
_super.prototype.execute.call(this);
276265
}

lib/mocha/mocha.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@
161161
testZone = rootZone.fork(new ProxyZoneSpec());
162162
});
163163

164+
this.on('fail', (test:any, err: any) => {
165+
const proxyZoneSpec = testZone && testZone.get('ProxyZoneSpec');
166+
if (proxyZoneSpec && err) {
167+
err.message += proxyZoneSpec.getAndClearPendingTasksInfo();
168+
}
169+
});
170+
164171
return originalRun.call(this, fn);
165172
};
166173

lib/zone-spec/async-test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,14 @@
99
class AsyncTestZoneSpec implements ZoneSpec {
1010
static symbolParentUnresolved = Zone.__symbol__('parentUnresolved');
1111

12-
_finishCallback: Function;
13-
_failCallback: Function;
1412
_pendingMicroTasks: boolean = false;
1513
_pendingMacroTasks: boolean = false;
1614
_alreadyErrored: boolean = false;
1715
_isSync: boolean = false;
1816
runZone = Zone.current;
1917
unresolvedChainedPromiseCount = 0;
2018

21-
constructor(finishCallback: Function, failCallback: Function, namePrefix: string) {
22-
this._finishCallback = finishCallback;
23-
this._failCallback = failCallback;
19+
constructor(private finishCallback: Function, private failCallback: Function, namePrefix: string) {
2420
this.name = 'asyncTestZone for ' + namePrefix;
2521
this.properties = {
2622
'AsyncTestZoneSpec': this
@@ -33,7 +29,7 @@ class AsyncTestZoneSpec implements ZoneSpec {
3329
this.runZone.run(() => {
3430
setTimeout(() => {
3531
if (!this._alreadyErrored && !(this._pendingMicroTasks || this._pendingMacroTasks)) {
36-
this._finishCallback();
32+
this.finishCallback();
3733
}
3834
}, 0);
3935
});
@@ -115,7 +111,7 @@ class AsyncTestZoneSpec implements ZoneSpec {
115111
// Let the parent try to handle the error.
116112
const result = parentZoneDelegate.handleError(targetZone, error);
117113
if (result) {
118-
this._failCallback(error);
114+
this.failCallback(error);
119115
this._alreadyErrored = true;
120116
}
121117
return false;

lib/zone-spec/proxy.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class ProxyZoneSpec implements ZoneSpec {
1717
lastTaskState: HasTaskState = null;
1818
isNeedToTriggerHasTask = false;
1919

20+
private tasks: Task[] = [];
21+
2022
static get(): ProxyZoneSpec {
2123
return Zone.current.get('ProxyZoneSpec');
2224
}
@@ -36,7 +38,6 @@ class ProxyZoneSpec implements ZoneSpec {
3638
this.setDelegate(defaultSpecDelegate);
3739
}
3840

39-
4041
setDelegate(delegateSpec: ZoneSpec) {
4142
const isNewDelegate = this._delegateSpec !== delegateSpec;
4243
this._delegateSpec = delegateSpec;
@@ -72,6 +73,37 @@ class ProxyZoneSpec implements ZoneSpec {
7273
}
7374
}
7475

76+
removeFromTasks(task: Task) {
77+
if (!this.tasks) {
78+
return;
79+
}
80+
for (let i = 0; i < this.tasks.length; i ++) {
81+
if (this.tasks[i] === task) {
82+
this.tasks.splice(i, 1);
83+
return;
84+
}
85+
}
86+
}
87+
88+
getAndClearPendingTasksInfo() {
89+
if (this.tasks.length === 0) {
90+
return '';
91+
}
92+
const taskInfo = this.tasks.map((task: Task) => {
93+
const dataInfo = task.data &&
94+
Object.keys(task.data)
95+
.map((key: string) => {
96+
return key + ':' + (task.data as any)[key];
97+
})
98+
.join(',');
99+
return `type: ${task.type}, source: ${task.source}, args: {${dataInfo}}`;
100+
});
101+
const pendingTasksInfo = '--Pendng async tasks are: [' + taskInfo + ']';
102+
// clear tasks
103+
this.tasks = [];
104+
105+
return pendingTasksInfo;
106+
}
75107

76108
onFork(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, zoneSpec: ZoneSpec):
77109
Zone {
@@ -118,6 +150,9 @@ class ProxyZoneSpec implements ZoneSpec {
118150

119151
onScheduleTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
120152
Task {
153+
if (task.type !== 'eventTask') {
154+
this.tasks.push(task);
155+
}
121156
if (this._delegateSpec && this._delegateSpec.onScheduleTask) {
122157
return this._delegateSpec.onScheduleTask(parentZoneDelegate, currentZone, targetZone, task);
123158
} else {
@@ -128,6 +163,9 @@ class ProxyZoneSpec implements ZoneSpec {
128163
onInvokeTask(
129164
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
130165
applyThis: any, applyArgs: any): any {
166+
if (task.type !== 'eventTask') {
167+
this.removeFromTasks(task);
168+
}
131169
this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
132170
if (this._delegateSpec && this._delegateSpec.onInvokeTask) {
133171
return this._delegateSpec.onInvokeTask(
@@ -139,6 +177,9 @@ class ProxyZoneSpec implements ZoneSpec {
139177

140178
onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
141179
any {
180+
if (task.type !== 'eventTask') {
181+
this.removeFromTasks(task);
182+
}
142183
this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
143184
if (this._delegateSpec && this._delegateSpec.onCancelTask) {
144185
return this._delegateSpec.onCancelTask(parentZoneDelegate, currentZone, targetZone, task);

0 commit comments

Comments
 (0)