Skip to content

Commit a1fa2e4

Browse files
committed
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 angular#4572, angular#4177, angular#4035, angular#2783 This includes the TestComponentBuilder. It allows using the test injector with Jasmine bindings, and waits for returned promises before completing async test blocks.
1 parent eb2c157 commit a1fa2e4

File tree

12 files changed

+844
-303
lines changed

12 files changed

+844
-303
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
library test_lib.matchers;
2+
3+
import 'dart:async';
4+
5+
import 'package:guinness/guinness.dart' as gns;
6+
7+
import 'package:angular2/src/core/dom/dom_adapter.dart' show DOM;
8+
9+
Expect expect(actual, [matcher]) {
10+
final expect = new Expect(actual);
11+
if (matcher != null) expect.to(matcher);
12+
return expect;
13+
}
14+
15+
const _u = const Object();
16+
17+
expectErrorMessage(actual, expectedMessage) {
18+
expect(actual.toString()).toContain(expectedMessage);
19+
}
20+
21+
expectException(Function actual, expectedMessage) {
22+
try {
23+
actual();
24+
} catch (e, s) {
25+
expectErrorMessage(e, expectedMessage);
26+
}
27+
}
28+
29+
class Expect extends gns.Expect {
30+
Expect(actual) : super(actual);
31+
32+
NotExpect get not => new NotExpect(actual);
33+
34+
void toEqual(expected) => toHaveSameProps(expected);
35+
void toContainError(message) => expectErrorMessage(this.actual, message);
36+
void toThrowError([message = ""]) => toThrowWith(message: message);
37+
void toThrowErrorWith(message) => expectException(this.actual, message);
38+
void toBePromise() => gns.guinness.matchers.toBeTrue(actual is Future);
39+
void toHaveCssClass(className) =>
40+
gns.guinness.matchers.toBeTrue(DOM.hasClass(actual, className));
41+
void toImplement(expected) => toBeA(expected);
42+
void toBeNaN() =>
43+
gns.guinness.matchers.toBeTrue(double.NAN.compareTo(actual) == 0);
44+
void toHaveText(expected) => _expect(elementText(actual), expected);
45+
void toHaveBeenCalledWith([a = _u, b = _u, c = _u, d = _u, e = _u, f = _u]) =>
46+
_expect(_argsMatch(actual, a, b, c, d, e, f), true,
47+
reason: 'method invoked with correct arguments');
48+
Function get _expect => gns.guinness.matchers.expect;
49+
50+
// TODO(tbosch): move this hack into Guinness
51+
_argsMatch(spyFn, [a0 = _u, a1 = _u, a2 = _u, a3 = _u, a4 = _u, a5 = _u]) {
52+
var calls = spyFn.calls;
53+
final toMatch = _takeDefined([a0, a1, a2, a3, a4, a5]);
54+
if (calls.isEmpty) {
55+
return false;
56+
} else {
57+
gns.SamePropsMatcher matcher = new gns.SamePropsMatcher(toMatch);
58+
for (var i = 0; i < calls.length; i++) {
59+
var call = calls[i];
60+
// TODO: create a better error message, not just 'Expected: <true> Actual: <false>'.
61+
// For hacking this is good:
62+
// print(call.positionalArguments);
63+
if (matcher.matches(call.positionalArguments, null)) {
64+
return true;
65+
}
66+
}
67+
return false;
68+
}
69+
}
70+
71+
List _takeDefined(List iter) => iter.takeWhile((_) => _ != _u).toList();
72+
}
73+
74+
class NotExpect extends gns.NotExpect {
75+
NotExpect(actual) : super(actual);
76+
77+
void toEqual(expected) => toHaveSameProps(expected);
78+
void toBePromise() => gns.guinness.matchers.toBeFalse(actual is Future);
79+
void toHaveCssClass(className) =>
80+
gns.guinness.matchers.toBeFalse(DOM.hasClass(actual, className));
81+
void toBeNull() => gns.guinness.matchers.toBeFalse(actual == null);
82+
Function get _expect => gns.guinness.matchers.expect;
83+
}
84+
85+
String elementText(n) {
86+
hasNodes(n) {
87+
var children = DOM.childNodes(n);
88+
return children != null && children.length > 0;
89+
}
90+
91+
if (n is Iterable) {
92+
return n.map(elementText).join("");
93+
}
94+
95+
if (DOM.isCommentNode(n)) {
96+
return '';
97+
}
98+
99+
if (DOM.isElementNode(n) && DOM.tagName(n) == 'CONTENT') {
100+
return elementText(DOM.getDistributedNodes(n));
101+
}
102+
103+
if (DOM.hasShadowRoot(n)) {
104+
return elementText(DOM.childNodesAsList(DOM.getShadowRoot(n)));
105+
}
106+
107+
if (hasNodes(n)) {
108+
return elementText(DOM.childNodesAsList(n));
109+
}
110+
111+
return DOM.getText(n);
112+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import {DOM} from 'angular2/src/core/dom/dom_adapter';
2+
import {global} from 'angular2/src/core/facade/lang';
3+
4+
5+
export interface NgMatchers extends jasmine.Matchers {
6+
toBePromise(): boolean;
7+
toBeAnInstanceOf(expected: any): boolean;
8+
toHaveText(expected: any): boolean;
9+
toHaveCssClass(expected: any): boolean;
10+
toImplement(expected: any): boolean;
11+
toContainError(expected: any): boolean;
12+
toThrowErrorWith(expectedMessage: any): boolean;
13+
not: NgMatchers;
14+
}
15+
16+
var _global: jasmine.GlobalPolluter = <any>(typeof window === 'undefined' ? global : window);
17+
18+
export var expect: (actual: any) => NgMatchers = <any>_global.expect;
19+
20+
21+
// Some Map polyfills don't polyfill Map.toString correctly, which
22+
// gives us bad error messages in tests.
23+
// The only way to do this in Jasmine is to monkey patch a method
24+
// to the object :-(
25+
Map.prototype['jasmineToString'] = function() {
26+
var m = this;
27+
if (!m) {
28+
return '' + m;
29+
}
30+
var res = [];
31+
m.forEach((v, k) => { res.push(`${k}:${v}`); });
32+
return `{ ${res.join(',')} }`;
33+
};
34+
35+
_global.beforeEach(function() {
36+
jasmine.addMatchers({
37+
// Custom handler for Map as Jasmine does not support it yet
38+
toEqual: function(util, customEqualityTesters) {
39+
return {
40+
compare: function(actual, expected) {
41+
return {pass: util.equals(actual, expected, [compareMap])};
42+
}
43+
};
44+
45+
function compareMap(actual, expected) {
46+
if (actual instanceof Map) {
47+
var pass = actual.size === expected.size;
48+
if (pass) {
49+
actual.forEach((v, k) => { pass = pass && util.equals(v, expected.get(k)); });
50+
}
51+
return pass;
52+
} else {
53+
return undefined;
54+
}
55+
}
56+
},
57+
58+
toBePromise: function() {
59+
return {
60+
compare: function(actual, expectedClass) {
61+
var pass = typeof actual === 'object' && typeof actual.then === 'function';
62+
return {pass: pass, get message() { return 'Expected ' + actual + ' to be a promise'; }};
63+
}
64+
};
65+
},
66+
67+
toBeAnInstanceOf: function() {
68+
return {
69+
compare: function(actual, expectedClass) {
70+
var pass = typeof actual === 'object' && actual instanceof expectedClass;
71+
return {
72+
pass: pass,
73+
get message() {
74+
return 'Expected ' + actual + ' to be an instance of ' + expectedClass;
75+
}
76+
};
77+
}
78+
};
79+
},
80+
81+
toHaveText: function() {
82+
return {
83+
compare: function(actual, expectedText) {
84+
var actualText = elementText(actual);
85+
return {
86+
pass: actualText == expectedText,
87+
get message() { return 'Expected ' + actualText + ' to be equal to ' + expectedText; }
88+
};
89+
}
90+
};
91+
},
92+
93+
toHaveCssClass: function() {
94+
return {compare: buildError(false), negativeCompare: buildError(true)};
95+
96+
function buildError(isNot) {
97+
return function(actual, className) {
98+
return {
99+
pass: DOM.hasClass(actual, className) == !isNot,
100+
get message() {
101+
return `Expected ${actual.outerHTML} ${isNot ? 'not ' : ''}to contain the CSS class "${className}"`;
102+
}
103+
};
104+
};
105+
}
106+
},
107+
108+
toContainError: function() {
109+
return {
110+
compare: function(actual, expectedText) {
111+
var errorMessage = actual.toString();
112+
return {
113+
pass: errorMessage.indexOf(expectedText) > -1,
114+
get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; }
115+
};
116+
}
117+
};
118+
},
119+
120+
toThrowErrorWith: function() {
121+
return {
122+
compare: function(actual, expectedText) {
123+
try {
124+
actual();
125+
return {
126+
pass: false,
127+
get message() { return "Was expected to throw, but did not throw"; }
128+
};
129+
} catch (e) {
130+
var errorMessage = e.toString();
131+
return {
132+
pass: errorMessage.indexOf(expectedText) > -1,
133+
get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; }
134+
};
135+
}
136+
}
137+
};
138+
},
139+
140+
toImplement: function() {
141+
return {
142+
compare: function(actualObject, expectedInterface) {
143+
var objProps = Object.keys(actualObject.constructor.prototype);
144+
var intProps = Object.keys(expectedInterface.prototype);
145+
146+
var missedMethods = [];
147+
intProps.forEach((k) => {
148+
if (!actualObject.constructor.prototype[k]) missedMethods.push(k);
149+
});
150+
151+
return {
152+
pass: missedMethods.length == 0,
153+
get message() {
154+
return 'Expected ' + actualObject + ' to have the following methods: ' +
155+
missedMethods.join(", ");
156+
}
157+
};
158+
}
159+
};
160+
}
161+
});
162+
});
163+
164+
function elementText(n) {
165+
var hasNodes = (n) => {
166+
var children = DOM.childNodes(n);
167+
return children && children.length > 0;
168+
};
169+
170+
if (n instanceof Array) {
171+
return n.map(elementText).join("");
172+
}
173+
174+
if (DOM.isCommentNode(n)) {
175+
return '';
176+
}
177+
178+
if (DOM.isElementNode(n) && DOM.tagName(n) == 'CONTENT') {
179+
return elementText(Array.prototype.slice.apply(DOM.getDistributedNodes(n)));
180+
}
181+
182+
if (DOM.hasShadowRoot(n)) {
183+
return elementText(DOM.childNodesAsList(DOM.getShadowRoot(n)));
184+
}
185+
186+
if (hasNodes(n)) {
187+
return elementText(DOM.childNodesAsList(n));
188+
}
189+
190+
return DOM.getText(n);
191+
}

modules/angular2/src/test_lib/test_injector.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,15 @@ export function createTestInjector(providers: Array<Type | Provider | any[]>): I
159159
* @return {FunctionWithParamTokens}
160160
*/
161161
export function inject(tokens: any[], fn: Function): FunctionWithParamTokens {
162-
return new FunctionWithParamTokens(tokens, fn);
162+
return new FunctionWithParamTokens(tokens, fn, false);
163+
}
164+
165+
export function injectAsync(tokens: any[], fn: Function): FunctionWithParamTokens {
166+
return new FunctionWithParamTokens(tokens, fn, true);
163167
}
164168

165169
export class FunctionWithParamTokens {
166-
constructor(private _tokens: any[], private _fn: Function) {}
170+
constructor(private _tokens: any[], private _fn: Function, public isAsync: boolean) {}
167171

168172
/**
169173
* Returns the value of the executed function.

0 commit comments

Comments
 (0)