This repository has been archived by the owner on Feb 26, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 407
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(jasmine): patch jasmine to understand zones.
jasmine now understands zones and follows these rules: - Jasmine itself runs in ambient zone (most likely the root Zone). - Describe calls run in SyncZone which prevent async operations from being spawned from within the describe blocks. - beforeEach/it/afterEach run in ProxyZone, which allows tests to retroactively set zone rules. - Each test runs in a new instance of the ProxyZone.
- Loading branch information
Showing
8 changed files
with
168 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,108 @@ | ||
'use strict'; | ||
// Patch jasmine's it and fit functions so that the `done` wrapCallback always resets the zone | ||
// to the jasmine zone, which should be the root zone. (angular/zone.js#91) | ||
if (!Zone) { | ||
throw new Error('zone.js does not seem to be installed'); | ||
} | ||
// When you have in async test (test with `done` argument) jasmine will | ||
// execute the next test synchronously in the done handler. This makes sense | ||
// for most tests, but now with zones. With zones running next test | ||
// synchronously means that the current zone does not get cleared. This | ||
// results in a chain of nested zones, which makes it hard to reason about | ||
// it. We override the `clearStack` method which forces jasmine to always | ||
// drain the stack before next test gets executed. | ||
(<any>jasmine).QueueRunner = (function (SuperQueueRunner) { | ||
const originalZone = Zone.current; | ||
// Subclass the `QueueRunner` and override the `clearStack` method. | ||
|
||
function alwaysClearStack(fn) { | ||
const zone: Zone = Zone.current.getZoneWith('JasmineClearStackZone') | ||
|| Zone.current.getZoneWith('ProxyZoneSpec') | ||
|| originalZone; | ||
zone.scheduleMicroTask('jasmineCleanStack', fn); | ||
(() => { | ||
// 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"); | ||
if (!jasmine) throw new Error("Missing: jasmine.js"); | ||
if (jasmine['__zone_patch__']) throw new Error("'jasmine' has already been patched with 'Zone'."); | ||
jasmine['__zone_patch__'] = true; | ||
|
||
const SyncTestZoneSpec: {new (name: string): ZoneSpec} = Zone['SyncTestZoneSpec']; | ||
const ProxyZoneSpec: {new (): ZoneSpec} = Zone['ProxyZoneSpec']; | ||
if (!SyncTestZoneSpec) throw new Error("Missing: SyncTestZoneSpec"); | ||
if (!ProxyZoneSpec) throw new Error("Missing: ProxyZoneSpec"); | ||
|
||
const ambientZone = Zone.current; | ||
// 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` but outside of | ||
// a `beforeEach` or `it`. | ||
const syncZone = ambientZone.fork(new SyncTestZoneSpec('jasmine.describe')); | ||
|
||
// 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 | ||
// different zones. | ||
// Example: | ||
// - In beforeEach() do childZone = Zone.current.fork(...); | ||
// - In it() try to do fakeAsync(). The issue is that because the beforeEach forked the | ||
// zone outside of fakeAsync it will be able to escope the fakeAsync rules. | ||
// - Because ProxyZone is parent fo `childZone` fakeAsync can retroactively add | ||
// fakeAsync behavior to the childZone. | ||
let testProxyZone: Zone = null; | ||
|
||
// Monkey patch all of the jasmine DSL so that each function runs in appropriate zone. | ||
const jasmineEnv = jasmine.getEnv(); | ||
['desribe', 'xdescribe', 'fdescribe'].forEach((methodName) => { | ||
let originalJasmineFn: Function = jasmineEnv[methodName]; | ||
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[methodName] = function(description: string, specDefinitions: Function) { | ||
return originalJasmineFn.call(this, description, wrapTestInZone(specDefinitions)); | ||
} | ||
}); | ||
['beforeEach', 'afterEach'].forEach((methodName) => { | ||
let originalJasmineFn: Function = jasmineEnv[methodName]; | ||
jasmineEnv[methodName] = function(specDefinitions: Function) { | ||
return originalJasmineFn.call(this, wrapTestInZone(specDefinitions)); | ||
} | ||
}); | ||
|
||
/** | ||
* Gets a function wrapping the body of a Jasmine `describe` block to execute in a | ||
* synchronous-only zone. | ||
*/ | ||
function wrapDescribeInZone(describeBody: Function): Function { | ||
return function() { | ||
return syncZone.run(describeBody, this, arguments as any as any[]); | ||
} | ||
} | ||
|
||
function QueueRunner(options) { | ||
options.clearStack = alwaysClearStack; | ||
SuperQueueRunner.call(this, options); | ||
/** | ||
* Gets a function wrapping the body of a Jasmine `it/beforeEach/afterEach` block to | ||
* execute in a ProxyZone zone. | ||
* This will run in `testProxyZone`. The `testProxyZone` will be reset by the `ZoneQueueRunner` | ||
*/ | ||
function wrapTestInZone(testBody: Function): Function { | ||
// 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.length == 0) | ||
? function() { return testProxyZone.run(testBody, this); } | ||
: function(done) { return testProxyZone.run(testBody, this, [done]); }; | ||
} | ||
interface QueueRunner { | ||
execute(): void; | ||
} | ||
QueueRunner.prototype = SuperQueueRunner.prototype; | ||
return QueueRunner; | ||
})((<any>jasmine).QueueRunner); | ||
interface QueueRunnerAttrs { | ||
queueableFns: {fn: Function}[]; | ||
onComplete: () => void; | ||
clearStack: (fn) => void; | ||
onException: (error) => void; | ||
catchException: () => boolean; | ||
userContext: any; | ||
timeout: {setTimeout: Function, clearTimeout: Function}; | ||
fail: ()=> void; | ||
} | ||
|
||
const QueueRunner = (jasmine as any).QueueRunner as { new(attrs: QueueRunnerAttrs): QueueRunner }; | ||
(jasmine as any).QueueRunner = class ZoneQueueRunner extends QueueRunner { | ||
constructor(attrs: QueueRunnerAttrs) { | ||
attrs.clearStack = (fn) => fn(); // Don't clear since onComplete will clear. | ||
attrs.onComplete = ((fn) => () => { | ||
// All functions are done, clear the test zone. | ||
testProxyZone = null; | ||
ambientZone.scheduleMicroTask('jasmine.onComplete', fn); | ||
})(attrs.onComplete); | ||
super(attrs); | ||
} | ||
|
||
execute() { | ||
if(Zone.current !== ambientZone) throw new Error("Unexpected Zone: " + Zone.current.name); | ||
testProxyZone = ambientZone.fork(new ProxyZoneSpec()); | ||
super.execute(); | ||
} | ||
}; | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
describe('jasmine', () => { | ||
let throwOnAsync = false; | ||
let beforeEachZone: Zone = null; | ||
let itZone: Zone = null; | ||
const syncZone = Zone.current; | ||
try { | ||
Zone.current.scheduleMicroTask('dontallow', () => null); | ||
} catch(e) { | ||
throwOnAsync = true; | ||
} | ||
|
||
beforeEach(() => beforeEachZone = Zone.current); | ||
|
||
it('should throw on async in describe', () => { | ||
expect(throwOnAsync).toBe(true); | ||
expect(syncZone.name).toEqual('syncTestZone for jasmine.describe'); | ||
itZone = Zone.current; | ||
}); | ||
|
||
afterEach(() => { | ||
let zone = Zone.current; | ||
expect(zone.name).toEqual('ProxyZone'); | ||
expect(beforeEachZone).toBe(zone); | ||
expect(itZone).toBe(zone); | ||
}); | ||
|
||
}); | ||
|
||
export var _something_so_that_i_am_treated_as_es6_module; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters