diff --git a/modules/angular2/src/testing/test_injector.ts b/modules/angular2/src/testing/test_injector.ts index c5e23a6a0b..21bef44e65 100644 --- a/modules/angular2/src/testing/test_injector.ts +++ b/modules/angular2/src/testing/test_injector.ts @@ -123,7 +123,9 @@ export function createTestInjector(providers: Array): I } /** - * Allows injecting dependencies in `beforeEach()` and `it()`. + * Allows injecting dependencies in `beforeEach()` and `it()`. When using with the + * `angular2/testing` library, the test function will be run within a zone and will + * automatically complete when all asynchronous tests have finished. * * Example: * @@ -133,17 +135,14 @@ export function createTestInjector(providers: Array): I * // ... * })); * - * it('...', inject([AClass, AsyncTestCompleter], (object, async) => { + * it('...', inject([AClass], (object) => { * object.doSomething().then(() => { * expect(...); - * async.done(); * }); * }) * ``` * * Notes: - * - injecting an `AsyncTestCompleter` allow completing async tests - this is the equivalent of - * adding a `done` parameter in Jasmine, * - inject is currently a function because of some Traceur limitation the syntax should eventually * becomes `it('...', @Inject (object: AClass, async: AsyncTestCompleter) => { ... });` * @@ -155,6 +154,9 @@ export function inject(tokens: any[], fn: Function): FunctionWithParamTokens { return new FunctionWithParamTokens(tokens, fn, false); } +/** + * @deprecated Use inject instead, which now supports both synchronous and asynchronous tests. + */ export function injectAsync(tokens: any[], fn: Function): FunctionWithParamTokens { return new FunctionWithParamTokens(tokens, fn, true); } diff --git a/modules/angular2/src/testing/testing.ts b/modules/angular2/src/testing/testing.ts index 3aa571bf88..f6cacfb5f1 100644 --- a/modules/angular2/src/testing/testing.ts +++ b/modules/angular2/src/testing/testing.ts @@ -3,7 +3,7 @@ * Jasmine framework. */ import {global} from 'angular2/src/facade/lang'; - +import {ListWrapper} from 'angular2/src/facade/collection'; import {bind} from 'angular2/src/core/di'; import {createTestInjector, FunctionWithParamTokens, inject, injectAsync} from './test_injector'; @@ -14,10 +14,29 @@ export {expect, NgMatchers} from './matchers'; var _global: jasmine.GlobalPolluter = (typeof window === 'undefined' ? global : window); +/** + * See http://jasmine.github.io/ + */ export var afterEach: Function = _global.afterEach; + +/** + * See http://jasmine.github.io/ + */ export var describe: Function = _global.describe; + +/** + * See http://jasmine.github.io/ + */ export var ddescribe: Function = _global.fdescribe; + +/** + * See http://jasmine.github.io/ + */ export var fdescribe: Function = _global.fdescribe; + +/** + * See http://jasmine.github.io/ + */ export var xdescribe: Function = _global.xdescribe; export type SyncTestFn = () => void; @@ -40,16 +59,18 @@ jsmBeforeEach(() => { /** * Allows overriding default providers of the test injector, - * defined in test_injector.js. + * which are defined in test_injector.js. * * The given function must return a list of DI providers. * * Example: * + * ``` * beforeEachProviders(() => [ * bind(Compiler).toClass(MockCompiler), * bind(SomeToken).toValue(myValue), * ]); + * ``` */ export function beforeEachProviders(fn): void { jsmBeforeEach(() => { @@ -68,72 +89,101 @@ function _isPromiseLike(input): boolean { return input && !!(input.then); } +function runInTestZone(fnToExecute, finishCallback, failCallback): any { + var pendingMicrotasks = 0; + var pendingTimeouts = []; + + var ngTestZone = (global.zone) + .fork({ + onError: function(e) { failCallback(e); }, + '$run': function(parentRun) { + return function() { + try { + return parentRun.apply(this, arguments); + } finally { + if (pendingMicrotasks == 0 && pendingTimeouts.length == 0) { + finishCallback(); + } + } + }; + }, + '$scheduleMicrotask': function(parentScheduleMicrotask) { + return function(fn) { + pendingMicrotasks++; + var microtask = function() { + try { + fn(); + } finally { + pendingMicrotasks--; + } + }; + parentScheduleMicrotask.call(this, microtask); + }; + }, + '$setTimeout': function(parentSetTimeout) { + return function(fn: Function, delay: number, ...args) { + var id; + var cb = function() { + fn(); + ListWrapper.remove(pendingTimeouts, id); + }; + id = parentSetTimeout(cb, delay, args); + pendingTimeouts.push(id); + return id; + }; + }, + '$clearTimeout': function(parentClearTimeout) { + return function(id: number) { + parentClearTimeout(id); + ListWrapper.remove(pendingTimeouts, id); + }; + }, + }); + + return ngTestZone.run(fnToExecute); +} + function _it(jsmFn: Function, name: string, testFn: FunctionWithParamTokens | AnyTestFn, testTimeOut: number): void { var timeOut = testTimeOut; if (testFn instanceof FunctionWithParamTokens) { - // The test case uses inject(). ie `it('test', inject([ClassA], (a) => { ... - // }));` - if (testFn.isAsync) { - jsmFn(name, (done) => { - if (!injector) { - injector = createTestInjector(testProviders); - } - var returned = testFn.execute(injector); - if (_isPromiseLike(returned)) { - returned.then(done, done.fail); - } else { - done.fail('Error: injectAsync was expected to return a promise, but the ' + - ' returned value was: ' + returned); - } - }, timeOut); - } else { - jsmFn(name, () => { - if (!injector) { - injector = createTestInjector(testProviders); - } - var returned = testFn.execute(injector); - if (_isPromiseLike(returned)) { - throw new Error('inject returned a promise. Did you mean to use injectAsync?'); - }; - }); - } + jsmFn(name, (done) => { + if (!injector) { + injector = createTestInjector(testProviders); + } + + var returnedTestValue = runInTestZone(() => testFn.execute(injector), done, done.fail); + if (_isPromiseLike(returnedTestValue)) { + (>returnedTestValue).then(null, (err) => { done.fail(err); }); + } + }, timeOut); } else { // The test case doesn't use inject(). ie `it('test', (done) => { ... }));` jsmFn(name, testFn, timeOut); } } - +/** + * Wrapper around Jasmine beforeEach function. + * See http://jasmine.github.io/ + * + * beforeEach may be used with the `inject` function to fetch dependencies. + * The test will automatically wait for any asynchronous calls inside the + * injected test function to complete. + */ export function beforeEach(fn: FunctionWithParamTokens | AnyTestFn): void { if (fn instanceof FunctionWithParamTokens) { // The test case uses inject(). ie `beforeEach(inject([ClassA], (a) => { ... // }));` - if (fn.isAsync) { - jsmBeforeEach((done) => { - if (!injector) { - injector = createTestInjector(testProviders); - } - var returned = fn.execute(injector); - if (_isPromiseLike(returned)) { - returned.then(done, done.fail); - } else { - done.fail('Error: injectAsync was expected to return a promise, but the ' + - ' returned value was: ' + returned); - } - }); - } else { - jsmBeforeEach(() => { - if (!injector) { - injector = createTestInjector(testProviders); - } - var returned = fn.execute(injector); - if (_isPromiseLike(returned)) { - throw new Error('inject returned a promise. Did you mean to use injectAsync?'); - }; - }); - } + + jsmBeforeEach((done) => { + if (!injector) { + injector = createTestInjector(testProviders); + } + + runInTestZone(() => fn.execute(injector), done, done.fail); + }); } else { // The test case doesn't use inject(). ie `beforeEach((done) => { ... }));` if ((fn).length === 0) { @@ -144,21 +194,53 @@ export function beforeEach(fn: FunctionWithParamTokens | AnyTestFn): void { } } +/** + * Wrapper around Jasmine it function. + * See http://jasmine.github.io/ + * + * it may be used with the `inject` function to fetch dependencies. + * The test will automatically wait for any asynchronous calls inside the + * injected test function to complete. + */ export function it(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): void { return _it(jsmIt, name, fn, timeOut); } +/** + * Wrapper around Jasmine xit (skipped it) function. + * See http://jasmine.github.io/ + * + * it may be used with the `inject` function to fetch dependencies. + * The test will automatically wait for any asynchronous calls inside the + * injected test function to complete. + */ export function xit(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): void { return _it(jsmXIt, name, fn, timeOut); } +/** + * Wrapper around Jasmine iit (focused it) function. + * See http://jasmine.github.io/ + * + * it may be used with the `inject` function to fetch dependencies. + * The test will automatically wait for any asynchronous calls inside the + * injected test function to complete. + */ export function iit(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): void { return _it(jsmIIt, name, fn, timeOut); } +/** + * Wrapper around Jasmine fit (focused it) function. + * See http://jasmine.github.io/ + * + * it may be used with the `inject` function to fetch dependencies. + * The test will automatically wait for any asynchronous calls inside the + * injected test function to complete. + */ export function fit(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): void { return _it(jsmIIt, name, fn, timeOut); diff --git a/modules/angular2/test/testing/testing_public_spec.ts b/modules/angular2/test/testing/testing_public_spec.ts index 055d85611b..d02917009e 100644 --- a/modules/angular2/test/testing/testing_public_spec.ts +++ b/modules/angular2/test/testing/testing_public_spec.ts @@ -9,7 +9,6 @@ import { tick, beforeEach, inject, - injectAsync, beforeEachProviders, TestComponentBuilder } from 'angular2/testing'; @@ -17,6 +16,7 @@ import { import {Injectable, bind} from 'angular2/core'; import {NgIf} from 'angular2/common'; import {Directive, Component, View, ViewMetadata} from 'angular2/core'; +import {PromiseWrapper} from 'angular2/src/facade/promise'; import {XHR} from 'angular2/src/compiler/xhr'; import {XHRImpl} from 'angular2/src/platform/browser/xhr_impl'; @@ -153,9 +153,8 @@ export function main() { it('should use set up providers', inject([FancyService], (service) => { expect(service.value).toEqual('real value'); })); - it('should wait until returned promises', injectAsync([FancyService], (service) => { - return service.getAsyncValue().then( - (value) => { expect(value).toEqual('async value'); }); + it('should wait until returned promises', inject([FancyService], (service) => { + service.getAsyncValue().then((value) => { expect(value).toEqual('async value'); }); })); describe('using beforeEach', () => { @@ -168,8 +167,8 @@ export function main() { }); describe('using async beforeEach', () => { - beforeEach(injectAsync([FancyService], (service) => { - return service.getAsyncValue().then((value) => { service.value = value; }); + beforeEach(inject([FancyService], (service) => { + service.getAsyncValue().then((value) => { service.value = value; }); })); it('should use asynchronously modified value', @@ -181,14 +180,17 @@ export function main() { describe('errors', () => { var originalJasmineIt: any; var originalJasmineBeforeEach: any; + var patchJasmineIt = () => { + var deferred = PromiseWrapper.completer(); originalJasmineIt = jasmine.getEnv().it; jasmine.getEnv().it = (description: string, fn) => { - var done = () => {}; - (done).fail = (err) => { throw new Error(err) }; + var done = () => { deferred.resolve() }; + (done).fail = (err) => { deferred.reject(err) }; fn(done); return null; - } + }; + return deferred.promise; }; var restoreJasmineIt = () => { jasmine.getEnv().it = originalJasmineIt; }; @@ -206,40 +208,46 @@ export function main() { var restoreJasmineBeforeEach = () => { jasmine.getEnv().beforeEach = originalJasmineBeforeEach; } - it('should fail when return was forgotten in it', () => { - expect(() => { - patchJasmineIt(); - it('forgets to return a promise', injectAsync([], () => { return true; })); - }) - .toThrowError('Error: injectAsync was expected to return a promise, but the ' + - ' returned value was: true'); + it('should fail when an error occurs inside inject', (done) => { + var itPromise = patchJasmineIt(); + it('throws an error', inject([], () => { throw new Error('foo'); })); + + itPromise.then(() => { done.fail('Expected function to throw, but it did not'); }, (err) => { + expect(err.message).toEqual('foo'); + done(); + }); restoreJasmineIt(); }); - it('should fail when synchronous spec returns promise', () => { - expect(() => { - patchJasmineIt(); - it('returns an extra promise', inject([], () => { return Promise.resolve('true'); })); - }).toThrowError('inject returned a promise. Did you mean to use injectAsync?'); + it('should fail when an asynchronous error is thrown', (done) => { + var itPromise = patchJasmineIt(); + + it('throws an async error', + inject([], () => { setTimeout(() => { throw new Error('bar'); }, 0); })); + + itPromise.then(() => { done.fail('Expected test to fail, but it did not'); }, (err) => { + expect(err.message).toEqual('bar'); + done(); + }); restoreJasmineIt(); }); - it('should fail when return was forgotten in beforeEach', () => { - expect(() => { - patchJasmineBeforeEach(); - beforeEach(injectAsync([], () => { return true; })); - }) - .toThrowError('Error: injectAsync was expected to return a promise, but the ' + - ' returned value was: true'); - restoreJasmineBeforeEach(); - }); + it('should fail when a returned promise is rejected', (done) => { + var itPromise = patchJasmineIt(); - it('should fail when synchronous beforeEach returns promise', () => { - expect(() => { - patchJasmineBeforeEach(); - beforeEach(inject([], () => { return Promise.resolve('true'); })); - }).toThrowError('inject returned a promise. Did you mean to use injectAsync?'); - restoreJasmineBeforeEach(); + it('should fail with an error from a promise', inject([], () => { + var deferred = PromiseWrapper.completer(); + var p = deferred.promise.then(() => { expect(1).toEqual(2); }); + + deferred.reject('baz'); + return p; + })); + + itPromise.then(() => { done.fail('Expected test to fail, but it did not'); }, (err) => { + expect(err).toEqual('baz'); + done(); + }); + restoreJasmineIt(); }); describe('using beforeEachProviders', () => { @@ -265,9 +273,9 @@ export function main() { describe('test component builder', function() { it('should instantiate a component with valid DOM', - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.createAsync(ChildComp).then((componentFixture) => { + tcb.createAsync(ChildComp).then((componentFixture) => { componentFixture.detectChanges(); expect(componentFixture.debugElement.nativeElement).toHaveText('Original Child'); @@ -275,9 +283,9 @@ export function main() { })); it('should allow changing members of the component', - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.createAsync(MyIfComp).then((componentFixture) => { + tcb.createAsync(MyIfComp).then((componentFixture) => { componentFixture.detectChanges(); expect(componentFixture.debugElement.nativeElement).toHaveText('MyIf()'); @@ -287,10 +295,9 @@ export function main() { }); })); - it('should override a template', - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + it('should override a template', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.overrideTemplate(MockChildComp, 'Mock') + tcb.overrideTemplate(MockChildComp, 'Mock') .createAsync(MockChildComp) .then((componentFixture) => { componentFixture.detectChanges(); @@ -299,12 +306,10 @@ export function main() { }); })); - it('should override a view', - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + it('should override a view', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.overrideView( - ChildComp, - new ViewMetadata({template: 'Modified {{childBinding}}'})) + tcb.overrideView(ChildComp, + new ViewMetadata({template: 'Modified {{childBinding}}'})) .createAsync(ChildComp) .then((componentFixture) => { componentFixture.detectChanges(); @@ -314,9 +319,9 @@ export function main() { })); it('should override component dependencies', - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.overrideDirective(ParentComp, ChildComp, MockChildComp) + tcb.overrideDirective(ParentComp, ChildComp, MockChildComp) .createAsync(ParentComp) .then((componentFixture) => { componentFixture.detectChanges(); @@ -327,9 +332,9 @@ export function main() { it("should override child component's dependencies", - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.overrideDirective(ParentComp, ChildComp, ChildWithChildComp) + tcb.overrideDirective(ParentComp, ChildComp, ChildWithChildComp) .overrideDirective(ChildWithChildComp, ChildChildComp, MockChildChildComp) .createAsync(ParentComp) .then((componentFixture) => { @@ -340,11 +345,9 @@ export function main() { }); })); - it('should override a provider', - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + it('should override a provider', inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.overrideProviders(TestProvidersComp, - [bind(FancyService).toClass(MockFancyService)]) + tcb.overrideProviders(TestProvidersComp, [bind(FancyService).toClass(MockFancyService)]) .createAsync(TestProvidersComp) .then((componentFixture) => { componentFixture.detectChanges(); @@ -355,10 +358,10 @@ export function main() { it('should override a viewProvider', - injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { - return tcb.overrideViewProviders(TestViewProvidersComp, - [bind(FancyService).toClass(MockFancyService)]) + tcb.overrideViewProviders(TestViewProvidersComp, + [bind(FancyService).toClass(MockFancyService)]) .createAsync(TestViewProvidersComp) .then((componentFixture) => { componentFixture.detectChanges();