From a1fa2e472fc50ceecef37e7c5da2ae760a8ad88c Mon Sep 17 00:00:00 2001 From: Julie Ralph Date: Thu, 8 Oct 2015 15:33:17 -0700 Subject: [PATCH] feat(test): Add an external version of the test library Adds test adapters for TypeScript and JavaScript only, exported as part of the test_lib module. These work with the Jasmine test framework, and allow use of the test injector within test blocks via the `inject` function. See #4572, #4177, #4035, #2783 This includes the TestComponentBuilder. It allows using the test injector with Jasmine bindings, and waits for returned promises before completing async test blocks. --- modules/angular2/src/test_lib/matchers.dart | 112 ++++++ modules/angular2/src/test_lib/matchers.ts | 191 ++++++++++ .../angular2/src/test_lib/test_injector.ts | 8 +- modules/angular2/src/test_lib/test_lib.dart | 122 +------ modules/angular2/src/test_lib/test_lib.ts | 188 +--------- .../src/test_lib/test_lib_public.dart | 3 + .../angular2/src/test_lib/test_lib_public.ts | 165 +++++++++ modules/angular2/test.ts | 3 +- .../test/test_lib/test_lib_public_spec.dart | 7 + .../test/test_lib/test_lib_public_spec.ts | 342 ++++++++++++++++++ modules/angular2/test_lib.ts | 5 +- tools/broccoli/trees/node_tree.ts | 1 + 12 files changed, 844 insertions(+), 303 deletions(-) create mode 100644 modules/angular2/src/test_lib/matchers.dart create mode 100644 modules/angular2/src/test_lib/matchers.ts create mode 100644 modules/angular2/src/test_lib/test_lib_public.dart create mode 100644 modules/angular2/src/test_lib/test_lib_public.ts create mode 100644 modules/angular2/test/test_lib/test_lib_public_spec.dart create mode 100644 modules/angular2/test/test_lib/test_lib_public_spec.ts diff --git a/modules/angular2/src/test_lib/matchers.dart b/modules/angular2/src/test_lib/matchers.dart new file mode 100644 index 0000000000..29c117f405 --- /dev/null +++ b/modules/angular2/src/test_lib/matchers.dart @@ -0,0 +1,112 @@ +library test_lib.matchers; + +import 'dart:async'; + +import 'package:guinness/guinness.dart' as gns; + +import 'package:angular2/src/core/dom/dom_adapter.dart' show DOM; + +Expect expect(actual, [matcher]) { + final expect = new Expect(actual); + if (matcher != null) expect.to(matcher); + return expect; +} + +const _u = const Object(); + +expectErrorMessage(actual, expectedMessage) { + expect(actual.toString()).toContain(expectedMessage); +} + +expectException(Function actual, expectedMessage) { + try { + actual(); + } catch (e, s) { + expectErrorMessage(e, expectedMessage); + } +} + +class Expect extends gns.Expect { + Expect(actual) : super(actual); + + NotExpect get not => new NotExpect(actual); + + void toEqual(expected) => toHaveSameProps(expected); + void toContainError(message) => expectErrorMessage(this.actual, message); + void toThrowError([message = ""]) => toThrowWith(message: message); + void toThrowErrorWith(message) => expectException(this.actual, message); + void toBePromise() => gns.guinness.matchers.toBeTrue(actual is Future); + void toHaveCssClass(className) => + gns.guinness.matchers.toBeTrue(DOM.hasClass(actual, className)); + void toImplement(expected) => toBeA(expected); + void toBeNaN() => + gns.guinness.matchers.toBeTrue(double.NAN.compareTo(actual) == 0); + void toHaveText(expected) => _expect(elementText(actual), expected); + void toHaveBeenCalledWith([a = _u, b = _u, c = _u, d = _u, e = _u, f = _u]) => + _expect(_argsMatch(actual, a, b, c, d, e, f), true, + reason: 'method invoked with correct arguments'); + Function get _expect => gns.guinness.matchers.expect; + + // TODO(tbosch): move this hack into Guinness + _argsMatch(spyFn, [a0 = _u, a1 = _u, a2 = _u, a3 = _u, a4 = _u, a5 = _u]) { + var calls = spyFn.calls; + final toMatch = _takeDefined([a0, a1, a2, a3, a4, a5]); + if (calls.isEmpty) { + return false; + } else { + gns.SamePropsMatcher matcher = new gns.SamePropsMatcher(toMatch); + for (var i = 0; i < calls.length; i++) { + var call = calls[i]; + // TODO: create a better error message, not just 'Expected: Actual: '. + // For hacking this is good: + // print(call.positionalArguments); + if (matcher.matches(call.positionalArguments, null)) { + return true; + } + } + return false; + } + } + + List _takeDefined(List iter) => iter.takeWhile((_) => _ != _u).toList(); +} + +class NotExpect extends gns.NotExpect { + NotExpect(actual) : super(actual); + + void toEqual(expected) => toHaveSameProps(expected); + void toBePromise() => gns.guinness.matchers.toBeFalse(actual is Future); + void toHaveCssClass(className) => + gns.guinness.matchers.toBeFalse(DOM.hasClass(actual, className)); + void toBeNull() => gns.guinness.matchers.toBeFalse(actual == null); + Function get _expect => gns.guinness.matchers.expect; +} + +String elementText(n) { + hasNodes(n) { + var children = DOM.childNodes(n); + return children != null && children.length > 0; + } + + if (n is Iterable) { + return n.map(elementText).join(""); + } + + if (DOM.isCommentNode(n)) { + return ''; + } + + if (DOM.isElementNode(n) && DOM.tagName(n) == 'CONTENT') { + return elementText(DOM.getDistributedNodes(n)); + } + + if (DOM.hasShadowRoot(n)) { + return elementText(DOM.childNodesAsList(DOM.getShadowRoot(n))); + } + + if (hasNodes(n)) { + return elementText(DOM.childNodesAsList(n)); + } + + return DOM.getText(n); +} diff --git a/modules/angular2/src/test_lib/matchers.ts b/modules/angular2/src/test_lib/matchers.ts new file mode 100644 index 0000000000..0ddcfc8885 --- /dev/null +++ b/modules/angular2/src/test_lib/matchers.ts @@ -0,0 +1,191 @@ +import {DOM} from 'angular2/src/core/dom/dom_adapter'; +import {global} from 'angular2/src/core/facade/lang'; + + +export interface NgMatchers extends jasmine.Matchers { + toBePromise(): boolean; + toBeAnInstanceOf(expected: any): boolean; + toHaveText(expected: any): boolean; + toHaveCssClass(expected: any): boolean; + toImplement(expected: any): boolean; + toContainError(expected: any): boolean; + toThrowErrorWith(expectedMessage: any): boolean; + not: NgMatchers; +} + +var _global: jasmine.GlobalPolluter = (typeof window === 'undefined' ? global : window); + +export var expect: (actual: any) => NgMatchers = _global.expect; + + +// Some Map polyfills don't polyfill Map.toString correctly, which +// gives us bad error messages in tests. +// The only way to do this in Jasmine is to monkey patch a method +// to the object :-( +Map.prototype['jasmineToString'] = function() { + var m = this; + if (!m) { + return '' + m; + } + var res = []; + m.forEach((v, k) => { res.push(`${k}:${v}`); }); + return `{ ${res.join(',')} }`; +}; + +_global.beforeEach(function() { + jasmine.addMatchers({ + // Custom handler for Map as Jasmine does not support it yet + toEqual: function(util, customEqualityTesters) { + return { + compare: function(actual, expected) { + return {pass: util.equals(actual, expected, [compareMap])}; + } + }; + + function compareMap(actual, expected) { + if (actual instanceof Map) { + var pass = actual.size === expected.size; + if (pass) { + actual.forEach((v, k) => { pass = pass && util.equals(v, expected.get(k)); }); + } + return pass; + } else { + return undefined; + } + } + }, + + toBePromise: function() { + return { + compare: function(actual, expectedClass) { + var pass = typeof actual === 'object' && typeof actual.then === 'function'; + return {pass: pass, get message() { return 'Expected ' + actual + ' to be a promise'; }}; + } + }; + }, + + toBeAnInstanceOf: function() { + return { + compare: function(actual, expectedClass) { + var pass = typeof actual === 'object' && actual instanceof expectedClass; + return { + pass: pass, + get message() { + return 'Expected ' + actual + ' to be an instance of ' + expectedClass; + } + }; + } + }; + }, + + toHaveText: function() { + return { + compare: function(actual, expectedText) { + var actualText = elementText(actual); + return { + pass: actualText == expectedText, + get message() { return 'Expected ' + actualText + ' to be equal to ' + expectedText; } + }; + } + }; + }, + + toHaveCssClass: function() { + return {compare: buildError(false), negativeCompare: buildError(true)}; + + function buildError(isNot) { + return function(actual, className) { + return { + pass: DOM.hasClass(actual, className) == !isNot, + get message() { + return `Expected ${actual.outerHTML} ${isNot ? 'not ' : ''}to contain the CSS class "${className}"`; + } + }; + }; + } + }, + + toContainError: function() { + return { + compare: function(actual, expectedText) { + var errorMessage = actual.toString(); + return { + pass: errorMessage.indexOf(expectedText) > -1, + get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; } + }; + } + }; + }, + + toThrowErrorWith: function() { + return { + compare: function(actual, expectedText) { + try { + actual(); + return { + pass: false, + get message() { return "Was expected to throw, but did not throw"; } + }; + } catch (e) { + var errorMessage = e.toString(); + return { + pass: errorMessage.indexOf(expectedText) > -1, + get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; } + }; + } + } + }; + }, + + toImplement: function() { + return { + compare: function(actualObject, expectedInterface) { + var objProps = Object.keys(actualObject.constructor.prototype); + var intProps = Object.keys(expectedInterface.prototype); + + var missedMethods = []; + intProps.forEach((k) => { + if (!actualObject.constructor.prototype[k]) missedMethods.push(k); + }); + + return { + pass: missedMethods.length == 0, + get message() { + return 'Expected ' + actualObject + ' to have the following methods: ' + + missedMethods.join(", "); + } + }; + } + }; + } + }); +}); + +function elementText(n) { + var hasNodes = (n) => { + var children = DOM.childNodes(n); + return children && children.length > 0; + }; + + if (n instanceof Array) { + return n.map(elementText).join(""); + } + + if (DOM.isCommentNode(n)) { + return ''; + } + + if (DOM.isElementNode(n) && DOM.tagName(n) == 'CONTENT') { + return elementText(Array.prototype.slice.apply(DOM.getDistributedNodes(n))); + } + + if (DOM.hasShadowRoot(n)) { + return elementText(DOM.childNodesAsList(DOM.getShadowRoot(n))); + } + + if (hasNodes(n)) { + return elementText(DOM.childNodesAsList(n)); + } + + return DOM.getText(n); +} diff --git a/modules/angular2/src/test_lib/test_injector.ts b/modules/angular2/src/test_lib/test_injector.ts index 61af114df4..3e2e30c18e 100644 --- a/modules/angular2/src/test_lib/test_injector.ts +++ b/modules/angular2/src/test_lib/test_injector.ts @@ -159,11 +159,15 @@ export function createTestInjector(providers: Array): I * @return {FunctionWithParamTokens} */ export function inject(tokens: any[], fn: Function): FunctionWithParamTokens { - return new FunctionWithParamTokens(tokens, fn); + return new FunctionWithParamTokens(tokens, fn, false); +} + +export function injectAsync(tokens: any[], fn: Function): FunctionWithParamTokens { + return new FunctionWithParamTokens(tokens, fn, true); } export class FunctionWithParamTokens { - constructor(private _tokens: any[], private _fn: Function) {} + constructor(private _tokens: any[], private _fn: Function, public isAsync: boolean) {} /** * Returns the value of the executed function. diff --git a/modules/angular2/src/test_lib/test_lib.dart b/modules/angular2/src/test_lib/test_lib.dart index d010951d43..b692cb2d8f 100644 --- a/modules/angular2/src/test_lib/test_lib.dart +++ b/modules/angular2/src/test_lib/test_lib.dart @@ -15,7 +15,7 @@ export 'package:guinness/guinness.dart' SpyObject, SpyFunction; -import 'package:angular2/src/core/dom/dom_adapter.dart' show DOM; +export 'matchers.dart' show expect, Expect, NotExpect; import 'package:angular2/src/core/reflection/reflection.dart'; import 'package:angular2/src/core/reflection/reflection_capabilities.dart'; @@ -67,88 +67,6 @@ void testSetup() { }, priority: 1); } -Expect expect(actual, [matcher]) { - final expect = new Expect(actual); - if (matcher != null) expect.to(matcher); - return expect; -} - -const _u = const Object(); - -expectErrorMessage(actual, expectedMessage) { - expect(actual.toString()).toContain(expectedMessage); -} - -expectException(Function actual, expectedMessage) { - try { - actual(); - } catch (e, s) { - expectErrorMessage(e, expectedMessage); - } -} - -class Expect extends gns.Expect { - Expect(actual) : super(actual); - - NotExpect get not => new NotExpect(actual); - - void toEqual(expected) => toHaveSameProps(expected); - void toContainError(message) => expectErrorMessage(this.actual, message); - void toThrowError([message = ""]) => toThrowWith(message: message); - void toThrowErrorWith(message) => expectException(this.actual, message); - void toBePromise() => gns.guinness.matchers.toBeTrue(actual is Future); - void toHaveCssClass(className) => - gns.guinness.matchers.toBeTrue(DOM.hasClass(actual, className)); - void toImplement(expected) => toBeA(expected); - void toBeNaN() => - gns.guinness.matchers.toBeTrue(double.NAN.compareTo(actual) == 0); - void toHaveText(expected) => _expect(elementText(actual), expected); - void toHaveBeenCalledWith([a = _u, b = _u, c = _u, d = _u, e = _u, f = _u]) => - _expect(_argsMatch(actual, a, b, c, d, e, f), true, - reason: 'method invoked with correct arguments'); - Function get _expect => gns.guinness.matchers.expect; - - // TODO(tbosch): move this hack into Guinness - _argsMatch(spyFn, [a0 = _u, a1 = _u, a2 = _u, a3 = _u, a4 = _u, a5 = _u]) { - var calls = spyFn.calls; - final toMatch = _takeDefined([a0, a1, a2, a3, a4, a5]); - if (calls.isEmpty) { - return false; - } else { - gns.SamePropsMatcher matcher = new gns.SamePropsMatcher(toMatch); - for (var i = 0; i < calls.length; i++) { - var call = calls[i]; - // TODO: create a better error message, not just 'Expected: Actual: '. - // For hacking this is good: - // print(call.positionalArguments); - if (matcher.matches(call.positionalArguments, null)) { - return true; - } - } - return false; - } - } - - List _takeDefined(List iter) => iter.takeWhile((_) => _ != _u).toList(); -} - -class NotExpect extends gns.NotExpect { - NotExpect(actual) : super(actual); - - void toEqual(expected) => toHaveSameProps(expected); - void toBePromise() => gns.guinness.matchers.toBeFalse(actual is Future); - void toHaveCssClass(className) => - gns.guinness.matchers.toBeFalse(DOM.hasClass(actual, className)); - void toBeNull() => gns.guinness.matchers.toBeFalse(actual == null); - Function get _expect => gns.guinness.matchers.expect; -} - -void beforeEach(fn) { - if (fn is! FunctionWithParamTokens) fn = new FunctionWithParamTokens([], fn); - gns.beforeEach(() { - fn.execute(_injector); - }); -} /** * Allows overriding default bindings defined in test_injector.js. @@ -174,8 +92,15 @@ void beforeEachBindings(Function fn) { beforeEachProviders(fn); } +void beforeEach(fn) { + if (fn is! FunctionWithParamTokens) fn = new FunctionWithParamTokens([], fn, false); + gns.beforeEach(() { + fn.execute(_injector); + }); +} + void _it(gnsFn, name, fn) { - if (fn is! FunctionWithParamTokens) fn = new FunctionWithParamTokens([], fn); + if (fn is! FunctionWithParamTokens) fn = new FunctionWithParamTokens([], fn, false); gnsFn(name, () { _inIt = true; fn.execute(_injector); @@ -232,33 +157,4 @@ class SpyObject extends gns.SpyObject { } } -String elementText(n) { - hasNodes(n) { - var children = DOM.childNodes(n); - return children != null && children.length > 0; - } - - if (n is Iterable) { - return n.map((nn) => elementText(nn)).join(""); - } - - if (DOM.isCommentNode(n)) { - return ''; - } - - if (DOM.isElementNode(n) && DOM.tagName(n) == 'CONTENT') { - return elementText(DOM.getDistributedNodes(n)); - } - - if (DOM.hasShadowRoot(n)) { - return elementText(DOM.childNodesAsList(DOM.getShadowRoot(n))); - } - - if (hasNodes(n)) { - return elementText(DOM.childNodesAsList(n)); - } - - return DOM.getText(n); -} - bool isInInnerZone() => Zone.current['_innerZone'] == true; diff --git a/modules/angular2/src/test_lib/test_lib.ts b/modules/angular2/src/test_lib/test_lib.ts index 5beadbfe5c..03c542c504 100644 --- a/modules/angular2/src/test_lib/test_lib.ts +++ b/modules/angular2/src/test_lib/test_lib.ts @@ -10,6 +10,8 @@ import {browserDetection} from './utils'; export {inject} from './test_injector'; +export {expect, NgMatchers} from './matchers'; + export var proxy: ClassDecorator = (t) => t; var _global: jasmine.GlobalPolluter = (typeof window === 'undefined' ? global : window); @@ -20,21 +22,6 @@ export type SyncTestFn = () => void; type AsyncTestFn = (done: () => void) => void; type AnyTestFn = SyncTestFn | AsyncTestFn; -export interface NgMatchers extends jasmine.Matchers { - toBe(expected: any): boolean; - toEqual(expected: any): boolean; - toBePromise(): boolean; - toBeAnInstanceOf(expected: any): boolean; - toHaveText(expected: any): boolean; - toHaveCssClass(expected: any): boolean; - toImplement(expected: any): boolean; - toContainError(expected: any): boolean; - toThrowErrorWith(expectedMessage: any): boolean; - not: NgMatchers; -} - -export var expect: (actual: any) => NgMatchers = _global.expect; - export class AsyncTestCompleter { constructor(private _done: Function) {} @@ -201,148 +188,6 @@ export function iit(name, fn, timeOut = null): void { return _it(jsmIIt, name, fn, timeOut); } -// Some Map polyfills don't polyfill Map.toString correctly, which -// gives us bad error messages in tests. -// The only way to do this in Jasmine is to monkey patch a method -// to the object :-( -Map.prototype['jasmineToString'] = function() { - var m = this; - if (!m) { - return '' + m; - } - var res = []; - m.forEach((v, k) => { res.push(`${k}:${v}`); }); - return `{ ${res.join(',')} }`; -}; - -_global.beforeEach(function() { - jasmine.addMatchers({ - // Custom handler for Map as Jasmine does not support it yet - toEqual: function(util, customEqualityTesters) { - return { - compare: function(actual, expected) { - return {pass: util.equals(actual, expected, [compareMap])}; - } - }; - - function compareMap(actual, expected) { - if (actual instanceof Map) { - var pass = actual.size === expected.size; - if (pass) { - actual.forEach((v, k) => { pass = pass && util.equals(v, expected.get(k)); }); - } - return pass; - } else { - return undefined; - } - } - }, - - toBePromise: function() { - return { - compare: function(actual, expectedClass) { - var pass = typeof actual === 'object' && typeof actual.then === 'function'; - return {pass: pass, get message() { return 'Expected ' + actual + ' to be a promise'; }}; - } - }; - }, - - toBeAnInstanceOf: function() { - return { - compare: function(actual, expectedClass) { - var pass = typeof actual === 'object' && actual instanceof expectedClass; - return { - pass: pass, - get message() { - return 'Expected ' + actual + ' to be an instance of ' + expectedClass; - } - }; - } - }; - }, - - toHaveText: function() { - return { - compare: function(actual, expectedText) { - var actualText = elementText(actual); - return { - pass: actualText == expectedText, - get message() { return 'Expected ' + actualText + ' to be equal to ' + expectedText; } - }; - } - }; - }, - - toHaveCssClass: function() { - return {compare: buildError(false), negativeCompare: buildError(true)}; - - function buildError(isNot) { - return function(actual, className) { - return { - pass: DOM.hasClass(actual, className) == !isNot, - get message() { - return `Expected ${actual.outerHTML} ${isNot ? 'not ' : ''}to contain the CSS class "${className}"`; - } - }; - }; - } - }, - - toContainError: function() { - return { - compare: function(actual, expectedText) { - var errorMessage = actual.toString(); - return { - pass: errorMessage.indexOf(expectedText) > -1, - get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; } - }; - } - }; - }, - - toThrowErrorWith: function() { - return { - compare: function(actual, expectedText) { - try { - actual(); - return { - pass: false, - get message() { return "Was expected to throw, but did not throw"; } - }; - } catch (e) { - var errorMessage = e.toString(); - return { - pass: errorMessage.indexOf(expectedText) > -1, - get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; } - }; - } - } - }; - }, - - toImplement: function() { - return { - compare: function(actualObject, expectedInterface) { - var objProps = Object.keys(actualObject.constructor.prototype); - var intProps = Object.keys(expectedInterface.prototype); - - var missedMethods = []; - intProps.forEach((k) => { - if (!actualObject.constructor.prototype[k]) missedMethods.push(k); - }); - - return { - pass: missedMethods.length == 0, - get message() { - return 'Expected ' + actualObject + ' to have the following methods: ' + - missedMethods.join(", "); - } - }; - } - }; - } - }); -}); export interface GuinessCompatibleSpy extends jasmine.Spy { /** By chaining the spy with and.returnValue, all calls to the function will return a specific @@ -410,35 +255,6 @@ export class SpyObject { } } -function elementText(n) { - var hasNodes = (n) => { - var children = DOM.childNodes(n); - return children && children.length > 0; - }; - - if (n instanceof Array) { - return n.map((nn) => elementText(nn)).join(""); - } - - if (DOM.isCommentNode(n)) { - return ''; - } - - if (DOM.isElementNode(n) && DOM.tagName(n) == 'CONTENT') { - return elementText(Array.prototype.slice.apply(DOM.getDistributedNodes(n))); - } - - if (DOM.hasShadowRoot(n)) { - return elementText(DOM.childNodesAsList(DOM.getShadowRoot(n))); - } - - if (hasNodes(n)) { - return elementText(DOM.childNodesAsList(n)); - } - - return DOM.getText(n); -} - export function isInInnerZone(): boolean { return (global.zone)._innerZone === true; } diff --git a/modules/angular2/src/test_lib/test_lib_public.dart b/modules/angular2/src/test_lib/test_lib_public.dart new file mode 100644 index 0000000000..71d885f002 --- /dev/null +++ b/modules/angular2/src/test_lib/test_lib_public.dart @@ -0,0 +1,3 @@ +library angular2.test_lib_public; + +// empty as this file is for external TS/js users and should not be transpiled to dart diff --git a/modules/angular2/src/test_lib/test_lib_public.ts b/modules/angular2/src/test_lib/test_lib_public.ts new file mode 100644 index 0000000000..f73af19511 --- /dev/null +++ b/modules/angular2/src/test_lib/test_lib_public.ts @@ -0,0 +1,165 @@ +/** + * Public Test Library for unit testing Angular2 Applications. Uses the + * Jasmine framework. + */ +import {global} from 'angular2/src/core/facade/lang'; + +import {bind} from 'angular2/src/core/di'; + +import {createTestInjector, FunctionWithParamTokens, inject, injectAsync} from './test_injector'; + +export {inject, injectAsync} from './test_injector'; + +export {expect} from './matchers'; + +var _global: jasmine.GlobalPolluter = (typeof window === 'undefined' ? global : window); + +export var afterEach: Function = _global.afterEach; +export var describe: Function = _global.describe; +export var ddescribe: Function = _global.fdescribe; +export var fdescribe: Function = _global.fdescribe; +export var xdescribe: Function = _global.xdescribe; + +export type SyncTestFn = () => void; +export type AsyncTestFn = (done: () => void) => void; +export type AnyTestFn = SyncTestFn | AsyncTestFn; + +var jsmBeforeEach = _global.beforeEach; +var jsmIt = _global.it; +var jsmIIt = _global.fit; +var jsmXIt = _global.xit; + +var testProviders; +var injector; + +// Reset the test providers before each test. +jsmBeforeEach(() => { + testProviders = []; + injector = null; +}); + +/** + * Allows overriding default providers of the test injector, + * 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(() => { + var providers = fn(); + if (!providers) return; + testProviders = [...testProviders, ...providers]; + if (injector !== null) { + throw new Error('beforeEachProviders was called after the injector had ' + + 'been used in a beforeEach or it block. This invalidates the ' + + 'test injector'); + } + }); +} + +function _isPromiseLike(input): boolean { + return input && !!(input.then); +} + +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?'); + }; + }); + } + } else { + // The test case doesn't use inject(). ie `it('test', (done) => { ... }));` + jsmFn(name, testFn, timeOut); + } +} + + +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?'); + }; + }); + } + } else { + // The test case doesn't use inject(). ie `beforeEach((done) => { ... }));` + if ((fn).length === 0) { + jsmBeforeEach(() => { (fn)(); }); + } else { + jsmBeforeEach((done) => { (fn)(done); }); + } + } +} + +export function it(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): + void { + return _it(jsmIt, name, fn, timeOut); +} + +export function xit(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): + void { + return _it(jsmXIt, name, fn, timeOut); +} + +export function iit(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): + void { + return _it(jsmIIt, name, fn, timeOut); +} + +export function fit(name: string, fn: FunctionWithParamTokens | AnyTestFn, timeOut: number = null): + void { + return _it(jsmIIt, name, fn, timeOut); +} diff --git a/modules/angular2/test.ts b/modules/angular2/test.ts index 8378079cbe..379a657b09 100644 --- a/modules/angular2/test.ts +++ b/modules/angular2/test.ts @@ -6,7 +6,8 @@ * This module is not included in the `angular2` module; you must import the test module explicitly. * */ -export * from './src/test_lib/test_lib'; +export * from './src/test_lib/test_lib_public'; export * from './src/test_lib/test_component_builder'; export * from './src/test_lib/test_injector'; export * from './src/test_lib/fake_async'; +export * from './src/test_lib/utils'; diff --git a/modules/angular2/test/test_lib/test_lib_public_spec.dart b/modules/angular2/test/test_lib/test_lib_public_spec.dart new file mode 100644 index 0000000000..0be27dcb8e --- /dev/null +++ b/modules/angular2/test/test_lib/test_lib_public_spec.dart @@ -0,0 +1,7 @@ +library angular2.test.test_lib.test_lib_public_spec; + +/** + * This is intentionally left blank. The public test lib is only for TS/JS + * apps. + */ +main() {} diff --git a/modules/angular2/test/test_lib/test_lib_public_spec.ts b/modules/angular2/test/test_lib/test_lib_public_spec.ts new file mode 100644 index 0000000000..8503182fdc --- /dev/null +++ b/modules/angular2/test/test_lib/test_lib_public_spec.ts @@ -0,0 +1,342 @@ +import { + it, + iit, + xit, + describe, + ddescribe, + xdescribe, + expect, + tick, + beforeEach, + dispatchEvent, + inject, + injectAsync, + beforeEachProviders, + TestComponentBuilder +} from 'angular2/test'; + +import {Injectable, NgIf, bind} from 'angular2/core'; +import {Directive, Component, View, ViewMetadata} from 'angular2/angular2'; + +// Services, and components for the tests. + +@Component({selector: 'child-comp'}) +@View({template: `Original {{childBinding}}`, directives: []}) +@Injectable() +class ChildComp { + childBinding: string; + constructor() { this.childBinding = 'Child'; } +} + +@Component({selector: 'child-comp'}) +@View({template: `Mock`}) +@Injectable() +class MockChildComp { +} + +@Component({selector: 'parent-comp'}) +@View({template: `Parent()`, directives: [ChildComp]}) +@Injectable() +class ParentComp { +} + +@Component({selector: 'my-if-comp'}) +@View({template: `MyIf(More)`, directives: [NgIf]}) +@Injectable() +class MyIfComp { + showMore: boolean = false; +} + +@Component({selector: 'child-child-comp'}) +@View({template: `ChildChild`}) +@Injectable() +class ChildChildComp { +} + +@Component({selector: 'child-comp'}) +@View({ + template: `Original {{childBinding}}()`, + directives: [ChildChildComp] +}) +@Injectable() +class ChildWithChildComp { + childBinding: string; + constructor() { this.childBinding = 'Child'; } +} + +@Component({selector: 'child-child-comp'}) +@View({template: `ChildChild Mock`}) +@Injectable() +class MockChildChildComp { +} + +class FancyService { + value: string = 'real value'; + getAsyncValue() { return Promise.resolve('async value'); } +} + +class MockFancyService extends FancyService { + value: string = 'mocked out value'; +} + +@Component({selector: 'my-service-comp', providers: [FancyService]}) +@View({template: `injected value: {{fancyService.value}}`}) +class TestProvidersComp { + constructor(private fancyService: FancyService) {} +} + +@Component({selector: 'my-service-comp', viewProviders: [FancyService]}) +@View({template: `injected value: {{fancyService.value}}`}) +class TestViewProvidersComp { + constructor(private fancyService: FancyService) {} +} + + +export function main() { + describe('angular2 jasmine matchers', () => { + describe('toHaveCssClass', () => { + it('should assert that the CSS class is present', () => { + var el = document.createElement('div'); + el.classList.add('matias'); + expect(el).toHaveCssClass('matias'); + }); + + it('should assert that the CSS class is not present', () => { + var el = document.createElement('div'); + el.classList.add('matias'); + expect(el).not.toHaveCssClass('fatias'); + }); + }); + }); + + describe('using the test injector with the inject helper', () => { + it('should run normal tests', () => { expect(true).toEqual(true); }); + + it('should run normal async tests', (done) => { + setTimeout(() => { + expect(true).toEqual(true); + done(); + }, 0); + }); + + describe('setting up Providers', () => { + beforeEachProviders(() => [bind(FancyService).toValue(new FancyService())]); + + 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'); }); + })); + + describe('using beforeEach', () => { + beforeEach(inject([FancyService], + (service) => { service.value = 'value modified in beforeEach'; })); + + it('should use modified providers', inject([FancyService], (service) => { + expect(service.value).toEqual('value modified in beforeEach'); + })); + }); + + describe('using async beforeEach', () => { + beforeEach(injectAsync([FancyService], (service) => { + return service.getAsyncValue().then((value) => { service.value = value; }); + })); + + it('should use asynchronously modified value', + inject([FancyService], (service) => { expect(service.value).toEqual('async value'); })); + }); + }); + }); + + describe('errors', () => { + var originalJasmineIt: any; + var originalJasmineBeforeEach: any; + var patchJasmineIt = () => { + originalJasmineIt = jasmine.getEnv().it; + jasmine.getEnv().it = (description: string, fn) => { + var done = () => {}; + (done).fail = (err) => { throw new Error(err) }; + fn(done); + return null; + } + }; + + var restoreJasmineIt = () => { jasmine.getEnv().it = originalJasmineIt; }; + + var patchJasmineBeforeEach = () => { + originalJasmineBeforeEach = jasmine.getEnv().beforeEach; + jasmine.getEnv().beforeEach = (fn: any) => { + var done = () => {}; + (done).fail = (err) => { throw new Error(err) }; + fn(done); + return null; + } + }; + + 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'); + 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?'); + 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 synchronous beforeEach returns promise', () => { + expect(() => { + patchJasmineBeforeEach(); + beforeEach(inject([], () => { return Promise.resolve('true'); })); + }).toThrowError('inject returned a promise. Did you mean to use injectAsync?'); + restoreJasmineBeforeEach(); + }); + + describe('using beforeEachProviders', () => { + beforeEachProviders(() => [bind(FancyService).toValue(new FancyService())]); + + beforeEach( + inject([FancyService], (service) => { expect(service.value).toEqual('real value'); })); + + describe('nested beforeEachProviders', () => { + + it('should fail when the injector has already been used', () => { + expect(() => { + patchJasmineBeforeEach(); + beforeEachProviders(() => [bind(FancyService).toValue(new FancyService())]); + }) + .toThrowError('beforeEachProviders was called after the injector had been used ' + + 'in a beforeEach or it block. This invalidates the test injector'); + restoreJasmineBeforeEach(); + }); + }); + }); + }); + + describe('test component builder', function() { + it('should instantiate a component with valid DOM', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.createAsync(ChildComp).then((rootTestComponent) => { + rootTestComponent.detectChanges(); + + expect(rootTestComponent.debugElement.nativeElement).toHaveText('Original Child'); + }); + })); + + it('should allow changing members of the component', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.createAsync(MyIfComp).then((rootTestComponent) => { + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement).toHaveText('MyIf()'); + + rootTestComponent.debugElement.componentInstance.showMore = true; + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement).toHaveText('MyIf(More)'); + }); + })); + + it('should override a template', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideTemplate(MockChildComp, 'Mock') + .createAsync(MockChildComp) + .then((rootTestComponent) => { + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement).toHaveText('Mock'); + + }); + })); + + it('should override a view', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideView( + ChildComp, + new ViewMetadata({template: 'Modified {{childBinding}}'})) + .createAsync(ChildComp) + .then((rootTestComponent) => { + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement).toHaveText('Modified Child'); + + }); + })); + + it('should override component dependencies', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideDirective(ParentComp, ChildComp, MockChildComp) + .createAsync(ParentComp) + .then((rootTestComponent) => { + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement).toHaveText('Parent(Mock)'); + + }); + })); + + + it("should override child component's dependencies", + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideDirective(ParentComp, ChildComp, ChildWithChildComp) + .overrideDirective(ChildWithChildComp, ChildChildComp, MockChildChildComp) + .createAsync(ParentComp) + .then((rootTestComponent) => { + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement) + .toHaveText('Parent(Original Child(ChildChild Mock))'); + + }); + })); + + it('should override a provider', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideProviders(TestProvidersComp, + [bind(FancyService).toClass(MockFancyService)]) + .createAsync(TestProvidersComp) + .then((rootTestComponent) => { + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement) + .toHaveText('injected value: mocked out value'); + }); + })); + + + it('should override a viewProvider', + injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => { + + return tcb.overrideViewProviders(TestViewProvidersComp, + [bind(FancyService).toClass(MockFancyService)]) + .createAsync(TestViewProvidersComp) + .then((rootTestComponent) => { + rootTestComponent.detectChanges(); + expect(rootTestComponent.debugElement.nativeElement) + .toHaveText('injected value: mocked out value'); + }); + })); + }); +} diff --git a/modules/angular2/test_lib.ts b/modules/angular2/test_lib.ts index 632922737d..79dafb21cb 100644 --- a/modules/angular2/test_lib.ts +++ b/modules/angular2/test_lib.ts @@ -1,3 +1,6 @@ // Test library and utilities for internal use. -export * from './test'; +export * from './src/test_lib/test_lib'; +export * from './src/test_lib/test_component_builder'; +export * from './src/test_lib/test_injector'; +export * from './src/test_lib/fake_async'; export * from './src/test_lib/utils'; diff --git a/tools/broccoli/trees/node_tree.ts b/tools/broccoli/trees/node_tree.ts index 9bc62d74d3..f1ec77be10 100644 --- a/tools/broccoli/trees/node_tree.ts +++ b/tools/broccoli/trees/node_tree.ts @@ -23,6 +23,7 @@ module.exports = function makeNodeTree(destinationPath) { 'angular2/test/animate/**', 'angular2/test/core/zone/**', 'angular2/test/test_lib/fake_async_spec.ts', + 'angular2/test/test_lib/test_lib_public_spec.ts', 'angular2/test/core/compiler/xhr_impl_spec.ts', 'angular2/test/core/forms/**', 'angular2/test/tools/tools_spec.ts',