Skip to content

Commit d207139

Browse files
juliemrRobert Messerle
authored andcommitted
feat(tests): manage asynchronous tests using zones
Instead of using injectAsync and returning a promise, use the `async` function to wrap tests. This will run the test inside a zone which does not complete the test until all asynchronous tasks have been completed. `async` may be used with the `inject` function, or separately. BREAKING CHANGE: `injectAsync` is now deprecated. Instead, use the `async` function to wrap any asynchronous tests. Before: ``` it('should wait for returned promises', injectAsync([FancyService], (service) => { return service.getAsyncValue().then((value) => { expect(value).toEqual('async value'); }); })); it('should wait for returned promises', injectAsync([], () => { return somePromise.then(() => { expect(true).toEqual(true); }); })); ``` After: ``` it('should wait for returned promises', async(inject([FancyService], (service) => { service.getAsyncValue().then((value) => { expect(value).toEqual('async value'); }); }))); // Note that if there is no injection, we no longer need `inject` OR `injectAsync`. it('should wait for returned promises', async(() => { somePromise.then() => { expect(true).toEqual(true); }); })); ```
1 parent fe6e410 commit d207139

File tree

11 files changed

+289
-254
lines changed

11 files changed

+289
-254
lines changed

karma-js.conf.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = function(config) {
2020
'node_modules/zone.js/dist/zone.js',
2121
'node_modules/zone.js/dist/long-stack-trace-zone.js',
2222
'node_modules/zone.js/dist/jasmine-patch.js',
23+
'node_modules/zone.js/dist/async-test.js',
2324

2425
// Including systemjs because it defines `__eval`, which produces correct stack traces.
2526
'modules/angular2/src/testing/shims_for_IE.js',

modules/angular2/src/testing/test_injector.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export class InjectSetupWrapper {
130130
return new FunctionWithParamTokens(tokens, fn, false, this._providers);
131131
}
132132

133+
/** @Deprecated {use async(withProviders().inject())} */
133134
injectAsync(tokens: any[], fn: Function): FunctionWithParamTokens {
134135
return new FunctionWithParamTokens(tokens, fn, true, this._providers);
135136
}
@@ -140,6 +141,8 @@ export function withProviders(providers: () => any) {
140141
}
141142

142143
/**
144+
* @Deprecated {use async(inject())}
145+
*
143146
* Allows injecting dependencies in `beforeEach()` and `it()`. The test must return
144147
* a promise which will resolve when all asynchronous activity is complete.
145148
*
@@ -161,6 +164,32 @@ export function injectAsync(tokens: any[], fn: Function): FunctionWithParamToken
161164
return new FunctionWithParamTokens(tokens, fn, true);
162165
}
163166

167+
/**
168+
* Wraps a test function in an asynchronous test zone. The test will automatically
169+
* complete when all asynchronous calls within this zone are done. Can be used
170+
* to wrap an {@link inject} call.
171+
*
172+
* Example:
173+
*
174+
* ```
175+
* it('...', async(inject([AClass], (object) => {
176+
* object.doSomething.then(() => {
177+
* expect(...);
178+
* })
179+
* });
180+
* ```
181+
*/
182+
export function async(fn: Function | FunctionWithParamTokens): FunctionWithParamTokens {
183+
if (fn instanceof FunctionWithParamTokens) {
184+
fn.isAsync = true;
185+
return fn;
186+
} else if (fn instanceof Function) {
187+
return new FunctionWithParamTokens([], fn, true);
188+
} else {
189+
throw new BaseException('argument to async must be a function or inject(<Function>)');
190+
}
191+
}
192+
164193
function emptyArray(): Array<any> {
165194
return [];
166195
}

modules/angular2/src/testing/testing.ts

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import {bind} from 'angular2/core';
99
import {
1010
FunctionWithParamTokens,
1111
inject,
12+
async,
1213
injectAsync,
1314
TestInjector,
1415
getTestInjector
1516
} from './test_injector';
1617

17-
export {inject, injectAsync} from './test_injector';
18+
export {inject, async, injectAsync} from './test_injector';
1819

1920
export {expect, NgMatchers} from './matchers';
2021

@@ -122,37 +123,28 @@ export function beforeEachProviders(fn): void {
122123
});
123124
}
124125

126+
function runInAsyncTestZone(fnToExecute, finishCallback: Function, failCallback: Function,
127+
testName = ''): any {
128+
var AsyncTestZoneSpec = Zone['AsyncTestZoneSpec'];
129+
var testZoneSpec = new AsyncTestZoneSpec(finishCallback, failCallback, testName);
130+
var testZone = Zone.current.fork(testZoneSpec);
131+
return testZone.run(fnToExecute);
132+
}
133+
125134
function _isPromiseLike(input): boolean {
126135
return input && !!(input.then);
127136
}
128137

129138
function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | AnyTestFn,
130139
testTimeOut: number): void {
131140
var timeOut = testTimeOut;
132-
133141
if (testFn instanceof FunctionWithParamTokens) {
134142
let testFnT = testFn;
135143
jsmFn(name, (done) => {
136-
var returnedTestValue;
137-
try {
138-
returnedTestValue = testInjector.execute(testFnT);
139-
} catch (err) {
140-
done.fail(err);
141-
return;
142-
}
143-
144144
if (testFnT.isAsync) {
145-
if (_isPromiseLike(returnedTestValue)) {
146-
(<Promise<any>>returnedTestValue).then(() => { done(); }, (err) => { done.fail(err); });
147-
} else {
148-
done.fail('Error: injectAsync was expected to return a promise, but the ' +
149-
' returned value was: ' + returnedTestValue);
150-
}
145+
runInAsyncTestZone(() => testInjector.execute(testFnT), done, done.fail, name);
151146
} else {
152-
if (!(returnedTestValue === undefined)) {
153-
done.fail('Error: inject returned a value. Did you mean to use injectAsync? Returned ' +
154-
'value was: ' + returnedTestValue);
155-
}
147+
testInjector.execute(testFnT);
156148
done();
157149
}
158150
}, timeOut);
@@ -166,8 +158,6 @@ function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | An
166158
* Wrapper around Jasmine beforeEach function.
167159
*
168160
* beforeEach may be used with the `inject` function to fetch dependencies.
169-
* The test will automatically wait for any asynchronous calls inside the
170-
* injected test function to complete.
171161
*
172162
* See http://jasmine.github.io/ for more details.
173163
*
@@ -181,26 +171,10 @@ export function beforeEach(fn: FunctionWithParamTokens | AnyTestFn): void {
181171
// }));`
182172
let fnT = fn;
183173
jsmBeforeEach((done) => {
184-
185-
var returnedTestValue;
186-
try {
187-
returnedTestValue = testInjector.execute(fnT);
188-
} catch (err) {
189-
done.fail(err);
190-
return;
191-
}
192174
if (fnT.isAsync) {
193-
if (_isPromiseLike(returnedTestValue)) {
194-
(<Promise<any>>returnedTestValue).then(() => { done(); }, (err) => { done.fail(err); });
195-
} else {
196-
done.fail('Error: injectAsync was expected to return a promise, but the ' +
197-
' returned value was: ' + returnedTestValue);
198-
}
175+
runInAsyncTestZone(() => testInjector.execute(fnT), done, done.fail, 'beforeEach');
199176
} else {
200-
if (!(returnedTestValue === undefined)) {
201-
done.fail('Error: inject returned a value. Did you mean to use injectAsync? Returned ' +
202-
'value was: ' + returnedTestValue);
203-
}
177+
testInjector.execute(fnT);
204178
done();
205179
}
206180
});
@@ -217,10 +191,8 @@ export function beforeEach(fn: FunctionWithParamTokens | AnyTestFn): void {
217191
/**
218192
* Define a single test case with the given test name and execution function.
219193
*
220-
* The test function can be either a synchronous function, an asynchronous function
221-
* that takes a completion callback, or an injected function created via {@link inject}
222-
* or {@link injectAsync}. The test will automatically wait for any asynchronous calls
223-
* inside the injected test function to complete.
194+
* The test function can be either a synchronous function, the result of {@link async},
195+
* or an injected function created via {@link inject}.
224196
*
225197
* Wrapper around Jasmine it function. See http://jasmine.github.io/ for more details.
226198
*
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
library angular2.test.testing.testing_browser_pec;
2+
3+
/**
4+
* This is intentionally left blank. The public test lib is only for TS/JS
5+
* apps.
6+
*/
7+
main() {}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
it,
3+
iit,
4+
xit,
5+
describe,
6+
ddescribe,
7+
xdescribe,
8+
expect,
9+
beforeEach,
10+
beforeEachProviders,
11+
inject,
12+
async,
13+
TestComponentBuilder,
14+
fakeAsync,
15+
tick
16+
} from 'angular2/testing';
17+
18+
import {Injectable, bind} from 'angular2/core';
19+
import {Directive, Component, ViewMetadata} from 'angular2/core';
20+
import {PromiseWrapper} from 'angular2/src/facade/promise';
21+
import {XHR} from 'angular2/src/compiler/xhr';
22+
import {XHRImpl} from 'angular2/src/platform/browser/xhr_impl';
23+
24+
// Components for the tests.
25+
class FancyService {
26+
value: string = 'real value';
27+
getAsyncValue() { return Promise.resolve('async value'); }
28+
getTimeoutValue() {
29+
return new Promise((resolve, reject) => { setTimeout(() => {resolve('timeout value')}, 10); })
30+
}
31+
}
32+
33+
@Component({
34+
selector: 'external-template-comp',
35+
templateUrl: '/base/modules/angular2/test/testing/static_assets/test.html'
36+
})
37+
class ExternalTemplateComp {
38+
}
39+
40+
@Component({selector: 'bad-template-comp', templateUrl: 'non-existant.html'})
41+
class BadTemplateUrl {
42+
}
43+
44+
// Tests for angular2/testing bundle specific to the browser environment.
45+
// For general tests, see test/testing/testing_public_spec.ts.
46+
export function main() {
47+
describe('test APIs for the browser', () => {
48+
describe('angular2 jasmine matchers', () => {
49+
describe('toHaveCssClass', () => {
50+
it('should assert that the CSS class is present', () => {
51+
var el = document.createElement('div');
52+
el.classList.add('matias');
53+
expect(el).toHaveCssClass('matias');
54+
});
55+
56+
it('should assert that the CSS class is not present', () => {
57+
var el = document.createElement('div');
58+
el.classList.add('matias');
59+
expect(el).not.toHaveCssClass('fatias');
60+
});
61+
});
62+
63+
describe('toHaveCssStyle', () => {
64+
it('should assert that the CSS style is present', () => {
65+
var el = document.createElement('div');
66+
expect(el).not.toHaveCssStyle('width');
67+
68+
el.style.setProperty('width', '100px');
69+
expect(el).toHaveCssStyle('width');
70+
});
71+
72+
it('should assert that the styles are matched against the element', () => {
73+
var el = document.createElement('div');
74+
expect(el).not.toHaveCssStyle({width: '100px', height: '555px'});
75+
76+
el.style.setProperty('width', '100px');
77+
expect(el).toHaveCssStyle({width: '100px'});
78+
expect(el).not.toHaveCssStyle({width: '100px', height: '555px'});
79+
80+
el.style.setProperty('height', '555px');
81+
expect(el).toHaveCssStyle({height: '555px'});
82+
expect(el).toHaveCssStyle({width: '100px', height: '555px'});
83+
});
84+
});
85+
});
86+
87+
describe('using the async helper', () => {
88+
var actuallyDone: boolean;
89+
90+
beforeEach(() => { actuallyDone = false; });
91+
92+
afterEach(() => { expect(actuallyDone).toEqual(true); });
93+
94+
it('should run async tests with XHRs', async(() => {
95+
var xhr = new XHRImpl();
96+
xhr.get('/base/modules/angular2/test/testing/static_assets/test.html')
97+
.then(() => { actuallyDone = true; });
98+
}),
99+
10000); // Long timeout here because this test makes an actual XHR.
100+
});
101+
102+
describe('using the test injector with the inject helper', () => {
103+
describe('setting up Providers', () => {
104+
beforeEachProviders(() => [bind(FancyService).toValue(new FancyService())]);
105+
106+
it('provides a real XHR instance',
107+
inject([XHR], (xhr) => { expect(xhr).toBeAnInstanceOf(XHRImpl); }));
108+
109+
it('should allow the use of fakeAsync',
110+
inject([FancyService], fakeAsync((service) => {
111+
var value;
112+
service.getAsyncValue().then(function(val) { value = val; });
113+
tick();
114+
expect(value).toEqual('async value');
115+
})));
116+
});
117+
});
118+
119+
describe('errors', () => {
120+
var originalJasmineIt: any;
121+
122+
var patchJasmineIt = () => {
123+
var deferred = PromiseWrapper.completer();
124+
originalJasmineIt = jasmine.getEnv().it;
125+
jasmine.getEnv().it = (description: string, fn) => {
126+
var done = () => { deferred.resolve() };
127+
(<any>done).fail = (err) => { deferred.reject(err) };
128+
fn(done);
129+
return null;
130+
};
131+
return deferred.promise;
132+
};
133+
134+
var restoreJasmineIt = () => { jasmine.getEnv().it = originalJasmineIt; };
135+
136+
it('should fail when an XHR fails', (done) => {
137+
var itPromise = patchJasmineIt();
138+
139+
it('should fail with an error from a promise',
140+
async(inject([TestComponentBuilder],
141+
(tcb) => { return tcb.createAsync(BadTemplateUrl); })));
142+
143+
itPromise.then(() => { done.fail('Expected test to fail, but it did not'); }, (err) => {
144+
expect(err).toEqual('Uncaught (in promise): Failed to load non-existant.html');
145+
done();
146+
});
147+
restoreJasmineIt();
148+
}, 10000);
149+
});
150+
151+
describe('test component builder', function() {
152+
it('should allow an external templateUrl',
153+
async(inject([TestComponentBuilder],
154+
(tcb: TestComponentBuilder) => {
155+
156+
tcb.createAsync(ExternalTemplateComp)
157+
.then((componentFixture) => {
158+
componentFixture.detectChanges();
159+
expect(componentFixture.debugElement.nativeElement)
160+
.toHaveText('from external template\n');
161+
});
162+
})),
163+
10000); // Long timeout here because this test makes an actual XHR, and is slow on Edge.
164+
});
165+
});
166+
}

0 commit comments

Comments
 (0)