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

Commit 0793637

Browse files
committed
add jasmine-mocha bridge
1 parent e60bdde commit 0793637

File tree

9 files changed

+311
-236
lines changed

9 files changed

+311
-236
lines changed

lib/jasmine/jasmine-patch.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
'use strict';
10+
import {patchJasmineClock} from './jasmine.clock';
11+
Zone.__load_patch('jasmine', (global: any) => {
12+
const __extends = function(d: any, b: any) {
13+
for (const p in b)
14+
if (b.hasOwnProperty(p)) d[p] = b[p];
15+
function __() {
16+
this.constructor = d;
17+
}
18+
d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new (__ as any)());
19+
};
20+
// Patch jasmine's describe/it/beforeEach/afterEach functions so test code always runs
21+
// in a testZone (ProxyZone). (See: angular/zone.js#91 & angular/angular#10503)
22+
if (!Zone) throw new Error('Missing: zone.js');
23+
if (typeof jasmine == 'undefined') {
24+
// not using jasmine, just return;
25+
return;
26+
}
27+
if ((jasmine as any)['__zone_symbol__isMochaBridge']) {
28+
// jasmine is a mock bridge
29+
return;
30+
}
31+
if ((jasmine as any)['__zone_patch__'])
32+
throw new Error(`'jasmine' has already been patched with 'Zone'.`);
33+
(jasmine as any)['__zone_patch__'] = true;
34+
35+
const SyncTestZoneSpec: {new (name: string): ZoneSpec} = (Zone as any)['SyncTestZoneSpec'];
36+
const ProxyZoneSpec: {new (): ZoneSpec} = (Zone as any)['ProxyZoneSpec'];
37+
if (!SyncTestZoneSpec) throw new Error('Missing: SyncTestZoneSpec');
38+
if (!ProxyZoneSpec) throw new Error('Missing: ProxyZoneSpec');
39+
40+
const ambientZone = Zone.current;
41+
// Create a synchronous-only zone in which to run `describe` blocks in order to raise an
42+
// error if any asynchronous operations are attempted inside of a `describe` but outside of
43+
// a `beforeEach` or `it`.
44+
const syncZone = ambientZone.fork(new SyncTestZoneSpec('jasmine.describe'));
45+
46+
const symbol = Zone.__symbol__;
47+
48+
// whether patch jasmine clock when in fakeAsync
49+
const enableClockPatch = global[symbol('fakeAsyncPatchLock')] === true;
50+
51+
// Monkey patch all of the jasmine DSL so that each function runs in appropriate zone.
52+
const jasmineEnv: any = jasmine.getEnv();
53+
['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {
54+
let originalJasmineFn: Function = jasmineEnv[methodName];
55+
jasmineEnv[methodName] = function(description: string, specDefinitions: Function) {
56+
return originalJasmineFn.call(this, description, wrapDescribeInZone(specDefinitions));
57+
};
58+
});
59+
['it', 'xit', 'fit'].forEach(methodName => {
60+
let originalJasmineFn: Function = jasmineEnv[methodName];
61+
jasmineEnv[symbol(methodName)] = originalJasmineFn;
62+
jasmineEnv[methodName] = function(
63+
description: string, specDefinitions: Function, timeout: number) {
64+
arguments[1] = wrapTestInZone(specDefinitions);
65+
return originalJasmineFn.apply(this, arguments);
66+
};
67+
});
68+
['beforeEach', 'afterEach'].forEach(methodName => {
69+
let originalJasmineFn: Function = jasmineEnv[methodName];
70+
jasmineEnv[symbol(methodName)] = originalJasmineFn;
71+
jasmineEnv[methodName] = function(specDefinitions: Function, timeout: number) {
72+
arguments[0] = wrapTestInZone(specDefinitions);
73+
return originalJasmineFn.apply(this, arguments);
74+
};
75+
});
76+
77+
patchJasmineClock(jasmine, enableClockPatch);
78+
/**
79+
* Gets a function wrapping the body of a Jasmine `describe` block to execute in a
80+
* synchronous-only zone.
81+
*/
82+
function wrapDescribeInZone(describeBody: Function): Function {
83+
return function() {
84+
return syncZone.run(describeBody, this, (arguments as any) as any[]);
85+
};
86+
}
87+
88+
function runInTestZone(testBody: Function, applyThis: any, queueRunner: any, done?: Function) {
89+
const isClockInstalled = !!(jasmine as any)[symbol('clockInstalled')];
90+
const testProxyZoneSpec = queueRunner.testProxyZoneSpec;
91+
const testProxyZone = queueRunner.testProxyZone;
92+
if (isClockInstalled && enableClockPatch) {
93+
// auto run a fakeAsync
94+
const fakeAsyncModule = (Zone as any)[Zone.__symbol__('fakeAsyncTest')];
95+
if (fakeAsyncModule && typeof fakeAsyncModule.fakeAsync === 'function') {
96+
testBody = fakeAsyncModule.fakeAsync(testBody);
97+
}
98+
}
99+
if (done) {
100+
return testProxyZone.run(testBody, applyThis, [done]);
101+
} else {
102+
return testProxyZone.run(testBody, applyThis);
103+
}
104+
}
105+
106+
/**
107+
* Gets a function wrapping the body of a Jasmine `it/beforeEach/afterEach` block to
108+
* execute in a ProxyZone zone.
109+
* This will run in `testProxyZone`. The `testProxyZone` will be reset by the `ZoneQueueRunner`
110+
*/
111+
function wrapTestInZone(testBody: Function): Function {
112+
// The `done` callback is only passed through if the function expects at least one argument.
113+
// Note we have to make a function with correct number of arguments, otherwise jasmine will
114+
// think that all functions are sync or async.
115+
return (testBody && (testBody.length ? function(done: Function) {
116+
return runInTestZone(testBody, this, this.queueRunner, done);
117+
} : function() {
118+
return runInTestZone(testBody, this, this.queueRunner);
119+
}));
120+
}
121+
interface QueueRunner {
122+
execute(): void;
123+
}
124+
interface QueueRunnerAttrs {
125+
queueableFns: {fn: Function}[];
126+
onComplete: () => void;
127+
clearStack: (fn: any) => void;
128+
onException: (error: any) => void;
129+
catchException: () => boolean;
130+
userContext: any;
131+
timeout: {setTimeout: Function; clearTimeout: Function};
132+
fail: () => void;
133+
}
134+
135+
const QueueRunner = (jasmine as any).QueueRunner as {
136+
new (attrs: QueueRunnerAttrs): QueueRunner;
137+
};
138+
(jasmine as any).QueueRunner = (function(_super) {
139+
__extends(ZoneQueueRunner, _super);
140+
function ZoneQueueRunner(attrs: {
141+
onComplete: Function;
142+
userContext?: any;
143+
timeout?: {setTimeout: Function; clearTimeout: Function};
144+
onException?: (error: any) => void;
145+
}) {
146+
attrs.onComplete = (fn => () => {
147+
// All functions are done, clear the test zone.
148+
this.testProxyZone = null;
149+
this.testProxyZoneSpec = null;
150+
ambientZone.scheduleMicroTask('jasmine.onComplete', fn);
151+
})(attrs.onComplete);
152+
153+
const nativeSetTimeout = global['__zone_symbol__setTimeout'];
154+
const nativeClearTimeout = global['__zone_symbol__clearTimeout'];
155+
if (nativeSetTimeout) {
156+
// should run setTimeout inside jasmine outside of zone
157+
attrs.timeout = {
158+
setTimeout: nativeSetTimeout ? nativeSetTimeout : global.setTimeout,
159+
clearTimeout: nativeClearTimeout ? nativeClearTimeout : global.clearTimeout
160+
};
161+
}
162+
163+
// create a userContext to hold the queueRunner itself
164+
// so we can access the testProxy in it/xit/beforeEach ...
165+
if ((jasmine as any).UserContext) {
166+
if (!attrs.userContext) {
167+
attrs.userContext = new (jasmine as any).UserContext();
168+
}
169+
attrs.userContext.queueRunner = this;
170+
} else {
171+
if (!attrs.userContext) {
172+
attrs.userContext = {};
173+
}
174+
attrs.userContext.queueRunner = this;
175+
}
176+
177+
// patch attrs.onException
178+
const onException = attrs.onException;
179+
attrs.onException = function(error: any) {
180+
if (error &&
181+
error.message ===
182+
'Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.') {
183+
// jasmine timeout, we can make the error message more
184+
// reasonable to tell what tasks are pending
185+
const proxyZoneSpec: any = this && this.testProxyZoneSpec;
186+
if (proxyZoneSpec) {
187+
const pendingTasksInfo = proxyZoneSpec.getAndClearPendingTasksInfo();
188+
error.message += pendingTasksInfo;
189+
}
190+
}
191+
if (onException) {
192+
onException.call(this, error);
193+
}
194+
};
195+
196+
_super.call(this, attrs);
197+
}
198+
ZoneQueueRunner.prototype.execute = function() {
199+
let zone: Zone = Zone.current;
200+
let isChildOfAmbientZone = false;
201+
while (zone) {
202+
if (zone === ambientZone) {
203+
isChildOfAmbientZone = true;
204+
break;
205+
}
206+
zone = zone.parent;
207+
}
208+
209+
if (!isChildOfAmbientZone) throw new Error('Unexpected Zone: ' + Zone.current.name);
210+
211+
// This is the zone which will be used for running individual tests.
212+
// It will be a proxy zone, so that the tests function can retroactively install
213+
// different zones.
214+
// Example:
215+
// - In beforeEach() do childZone = Zone.current.fork(...);
216+
// - In it() try to do fakeAsync(). The issue is that because the beforeEach forked the
217+
// zone outside of fakeAsync it will be able to escape the fakeAsync rules.
218+
// - Because ProxyZone is parent fo `childZone` fakeAsync can retroactively add
219+
// fakeAsync behavior to the childZone.
220+
221+
this.testProxyZoneSpec = new ProxyZoneSpec();
222+
this.testProxyZone = ambientZone.fork(this.testProxyZoneSpec);
223+
if (!Zone.currentTask) {
224+
// if we are not running in a task then if someone would register a
225+
// element.addEventListener and then calling element.click() the
226+
// addEventListener callback would think that it is the top most task and would
227+
// drain the microtask queue on element.click() which would be incorrect.
228+
// For this reason we always force a task when running jasmine tests.
229+
Zone.current.scheduleMicroTask(
230+
'jasmine.execute().forceTask', () => QueueRunner.prototype.execute.call(this));
231+
} else {
232+
_super.prototype.execute.call(this);
233+
}
234+
};
235+
return ZoneQueueRunner;
236+
})(QueueRunner);
237+
});

0 commit comments

Comments
 (0)