Skip to content

Commit a2c5af4

Browse files
committedAug 28, 2014
feat(testability): add $$testability service
The $$testability service is a collection of methods for use when debugging or by automated testing tools. It is available globally through the function `angular.getTestability`. For reference, see the Angular.Dart version at dart-archive/angular.dart#1191
1 parent c6bde52 commit a2c5af4

15 files changed

+327
-16
lines changed
 

‎angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var angularFiles = {
3535
'src/ng/sce.js',
3636
'src/ng/sniffer.js',
3737
'src/ng/templateRequest.js',
38+
'src/ng/testability.js',
3839
'src/ng/timeout.js',
3940
'src/ng/urlUtils.js',
4041
'src/ng/window.js',

‎docs/content/guide/expression.ngdoc

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ the method from your view. If you want to `eval()` an Angular expression yoursel
3838
## Example
3939
<example>
4040
<file name="index.html">
41-
1+2={{1+2}}
41+
<span>
42+
1+2={{1+2}}
43+
</span>
4244
</file>
4345

4446
<file name="protractor.js" type="protractor">

‎docs/content/guide/module.ngdoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ I'm in a hurry. How do I get a Hello World module working?
5050

5151
<file name="protractor.js" type="protractor">
5252
it('should add Hello to the name', function() {
53-
expect(element(by.binding(" 'World' | greet ")).getText()).toEqual('Hello, World!');
53+
expect(element(by.binding("'World' | greet")).getText()).toEqual('Hello, World!');
5454
});
5555
</file>
5656
</example>
@@ -128,7 +128,7 @@ The above is a suggestion. Tailor it to your needs.
128128

129129
<file name="protractor.js" type="protractor">
130130
it('should add Hello to the name', function() {
131-
expect(element(by.binding(" greeting ")).getText()).toEqual('Bonjour World!');
131+
expect(element(by.binding("greeting")).getText()).toEqual('Bonjour World!');
132132
});
133133
</file>
134134

‎npm-shrinkwrap.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"karma-sauce-launcher": "0.2.0",
3434
"karma-script-launcher": "0.1.0",
3535
"karma-browserstack-launcher": "0.0.7",
36-
"protractor": "1.0.0",
36+
"protractor": "1.2.0-beta1",
3737
"yaml-js": "~0.0.8",
3838
"rewire": "1.1.3",
3939
"promises-aplus-tests": "~2.0.4",

‎src/.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"encodeUriQuery": false,
8080
"angularInit": false,
8181
"bootstrap": false,
82+
"getTestability": false,
8283
"snake_case": false,
8384
"bindJQuery": false,
8485
"assertArg": false,

‎src/Angular.js

+13
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
encodeUriQuery: true,
7575
angularInit: true,
7676
bootstrap: true,
77+
getTestability: true,
7778
snake_case: true,
7879
bindJQuery: true,
7980
assertArg: true,
@@ -1459,6 +1460,18 @@ function reloadWithDebugInfo() {
14591460
window.location.reload();
14601461
}
14611462

1463+
/*
1464+
* @name angular.getTestability
1465+
* @module ng
1466+
* @description
1467+
* Get the testability service for the instance of Angular on the given
1468+
* element.
1469+
* @param {DOMElement} element DOM element which is the root of angular application.
1470+
*/
1471+
function getTestability(rootElement) {
1472+
return angular.element(rootElement).injector().get('$$testability');
1473+
}
1474+
14621475
var SNAKE_CASE_REGEXP = /[A-Z]/g;
14631476
function snake_case(name, separator) {
14641477
separator = separator || '_';

‎src/AngularPublic.js

+3
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
$SnifferProvider,
8080
$TemplateCacheProvider,
8181
$TemplateRequestProvider,
82+
$$TestabilityProvider,
8283
$TimeoutProvider,
8384
$$RAFProvider,
8485
$$AsyncCallbackProvider,
@@ -136,6 +137,7 @@ function publishExternalAPI(angular){
136137
'lowercase': lowercase,
137138
'uppercase': uppercase,
138139
'callbacks': {counter: 0},
140+
'getTestability': getTestability,
139141
'$$minErr': minErr,
140142
'$$csp': csp,
141143
'reloadWithDebugInfo': reloadWithDebugInfo
@@ -230,6 +232,7 @@ function publishExternalAPI(angular){
230232
$sniffer: $SnifferProvider,
231233
$templateCache: $TemplateCacheProvider,
232234
$templateRequest: $TemplateRequestProvider,
235+
$$testability: $$TestabilityProvider,
233236
$timeout: $TimeoutProvider,
234237
$window: $WindowProvider,
235238
$$rAF: $$RAFProvider,

‎src/ng/directive/input.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ var inputType = {
814814
</file>
815815
<file name="protractor.js" type="protractor">
816816
it('should change state', function() {
817-
var color = element(by.binding('color | json'));
817+
var color = element(by.binding('color'));
818818
819819
expect(color.getText()).toContain('blue');
820820
@@ -1313,7 +1313,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
13131313
</div>
13141314
</file>
13151315
<file name="protractor.js" type="protractor">
1316-
var user = element(by.binding('user'));
1316+
var user = element(by.exactBinding('user'));
13171317
var userNameValid = element(by.binding('myForm.userName.$valid'));
13181318
var lastNameValid = element(by.binding('myForm.lastName.$valid'));
13191319
var lastNameError = element(by.binding('myForm.lastName.$error'));
@@ -2542,7 +2542,7 @@ var minlengthDirective = function() {
25422542
* </file>
25432543
* <file name="protractor.js" type="protractor">
25442544
* var listInput = element(by.model('names'));
2545-
* var names = element(by.binding('names'));
2545+
* var names = element(by.exactBinding('names'));
25462546
* var valid = element(by.binding('myForm.namesInput.$valid'));
25472547
* var error = element(by.css('span.error'));
25482548
*
@@ -2572,7 +2572,7 @@ var minlengthDirective = function() {
25722572
* <file name="protractor.js" type="protractor">
25732573
* it("should split the text by newlines", function() {
25742574
* var listInput = element(by.model('list'));
2575-
* var output = element(by.binding(' list | json '));
2575+
* var output = element(by.binding('list | json'));
25762576
* listInput.sendKeys('abc\ndef\nghi');
25772577
* expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
25782578
* });

‎src/ng/directive/ngEventDirs.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
<button ng-click="count = count + 1" ng-init="count=0">
2020
Increment
2121
</button>
22-
count: {{count}}
22+
<span>
23+
count: {{count}}
24+
</span>
2325
</file>
2426
<file name="protractor.js" type="protractor">
2527
it('should check ng-click', function() {

‎src/ng/directive/select.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,13 @@ var ngOptionsMinErr = minErr('ngOptions');
123123
</file>
124124
<file name="protractor.js" type="protractor">
125125
it('should check ng-options', function() {
126-
expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('red');
126+
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
127127
element.all(by.model('myColor')).first().click();
128128
element.all(by.css('select[ng-model="myColor"] option')).first().click();
129-
expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('black');
129+
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
130130
element(by.css('.nullable select[ng-model="myColor"]')).click();
131131
element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
132-
expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('null');
132+
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
133133
});
134134
</file>
135135
</example>

‎src/ng/filter/filters.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ function dateFilter($locale) {
490490
</file>
491491
<file name="protractor.js" type="protractor">
492492
it('should jsonify filtered objects', function() {
493-
expect(element(by.binding(" {'name':'value'} | json ")).getText()).toMatch(/\{\n "name": ?"value"\n}/);
493+
expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/);
494494
});
495495
</file>
496496
</example>

‎src/ng/filter/limitTo.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
<file name="protractor.js" type="protractor">
4141
var numLimitInput = element(by.model('numLimit'));
4242
var letterLimitInput = element(by.model('letterLimit'));
43-
var limitedNumbers = element(by.binding(' numbers | limitTo:numLimit '));
44-
var limitedLetters = element(by.binding(' letters | limitTo:letterLimit '));
43+
var limitedNumbers = element(by.binding('numbers | limitTo:numLimit'));
44+
var limitedLetters = element(by.binding('letters | limitTo:letterLimit'));
4545
4646
it('should limit the number array to first three items', function() {
4747
expect(numLimitInput.getAttribute('value')).toBe('3');

‎src/ng/testability.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
'use strict';
2+
3+
4+
function $$TestabilityProvider() {
5+
this.$get = ['$rootScope', '$browser', '$location',
6+
function($rootScope, $browser, $location) {
7+
8+
/**
9+
* @name $testability
10+
*
11+
* @description
12+
* The private $$testability service provides a collection of methods for use when debugging
13+
* or by automated test and debugging tools.
14+
*/
15+
var testability = {};
16+
17+
/**
18+
* @name $$testability#findBindings
19+
*
20+
* @description
21+
* Returns an array of elements that are bound (via ng-bind or {{}})
22+
* to expressions matching the input.
23+
*
24+
* @param {Element} element The element root to search from.
25+
* @param {string} expression The binding expression to match.
26+
* @param {boolean} opt_exactMatch If true, only returns exact matches
27+
* for the expression. Filters and whitespace are ignored.
28+
*/
29+
testability.findBindings = function(element, expression, opt_exactMatch) {
30+
var bindings = element.getElementsByClassName('ng-binding');
31+
var matches = [];
32+
forEach(bindings, function(binding) {
33+
var dataBinding = angular.element(binding).data('$binding');
34+
if (dataBinding) {
35+
forEach(dataBinding, function(bindingName) {
36+
if (opt_exactMatch) {
37+
var matcher = new RegExp('(^|\\s)' + expression + '(\\s|\\||$)');
38+
if (matcher.test(bindingName)) {
39+
matches.push(binding);
40+
}
41+
} else {
42+
if (bindingName.indexOf(expression) != -1) {
43+
matches.push(binding);
44+
}
45+
}
46+
});
47+
}
48+
});
49+
return matches;
50+
};
51+
52+
/**
53+
* @name $$testability#findModels
54+
*
55+
* @description
56+
* Returns an array of elements that are two-way found via ng-model to
57+
* expressions matching the input.
58+
*
59+
* @param {Element} element The element root to search from.
60+
* @param {string} expression The model expression to match.
61+
* @param {boolean} opt_exactMatch If true, only returns exact matches
62+
* for the expression.
63+
*/
64+
testability.findModels = function(element, expression, opt_exactMatch) {
65+
var prefixes = ['ng-', 'data-ng-', 'ng\\:'];
66+
for (var p = 0; p < prefixes.length; ++p) {
67+
var attributeEquals = opt_exactMatch ? '=' : '*=';
68+
var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
69+
var elements = element.querySelectorAll(selector);
70+
if (elements.length) {
71+
return elements;
72+
}
73+
}
74+
};
75+
76+
/**
77+
* @name $$testability#getLocation
78+
*
79+
* @description
80+
* Shortcut for getting the location in a browser agnostic way. Returns
81+
* the path, search, and hash. (e.g. /path?a=b#hash)
82+
*/
83+
testability.getLocation = function() {
84+
return $location.url();
85+
};
86+
87+
/**
88+
* @name $$testability#setLocation
89+
*
90+
* @description
91+
* Shortcut for navigating to a location without doing a full page reload.
92+
*
93+
* @param {string} url The location url (path, search and hash,
94+
* e.g. /path?a=b#hash) to go to.
95+
*/
96+
testability.setLocation = function(url) {
97+
if (url !== $location.url()) {
98+
$location.url(url);
99+
$rootScope.$digest();
100+
}
101+
};
102+
103+
/**
104+
* @name $$testability#whenStable
105+
*
106+
* @description
107+
* Calls the callback when $timeout and $http requests are completed.
108+
*
109+
* @param {function} callback
110+
*/
111+
testability.whenStable = function(callback) {
112+
$browser.notifyWhenNoOutstandingRequests(callback);
113+
};
114+
115+
return testability;
116+
}];
117+
}

‎test/ng/testabilitySpec.js

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
'use strict';
2+
3+
describe('$$testability', function() {
4+
describe('finding elements', function() {
5+
var $$testability, $compile, scope, element;
6+
7+
beforeEach(inject(function(_$$testability_, _$compile_, $rootScope) {
8+
$$testability = _$$testability_;
9+
$compile = _$compile_;
10+
scope = $rootScope.$new();
11+
}));
12+
13+
afterEach(function() {
14+
dealoc(element);
15+
});
16+
17+
it('should find partial bindings', function() {
18+
element =
19+
'<div>' +
20+
' <span>{{name}}</span>' +
21+
' <span>{{username}}</span>' +
22+
'</div>';
23+
element = $compile(element)(scope);
24+
var names = $$testability.findBindings(element[0], 'name');
25+
expect(names.length).toBe(2);
26+
expect(names[0]).toBe(element.find('span')[0]);
27+
expect(names[1]).toBe(element.find('span')[1]);
28+
});
29+
30+
it('should find exact bindings', function() {
31+
element =
32+
'<div>' +
33+
' <span>{{name}}</span>' +
34+
' <span>{{username}}</span>' +
35+
'</div>';
36+
element = $compile(element)(scope);
37+
var users = $$testability.findBindings(element[0], 'name', true);
38+
expect(users.length).toBe(1);
39+
expect(users[0]).toBe(element.find('span')[0]);
40+
});
41+
42+
it('should ignore filters for exact bindings', function() {
43+
element =
44+
'<div>' +
45+
' <span>{{name | uppercase}}</span>' +
46+
' <span>{{username}}</span>' +
47+
'</div>';
48+
element = $compile(element)(scope);
49+
var users = $$testability.findBindings(element[0], 'name', true);
50+
expect(users.length).toBe(1);
51+
expect(users[0]).toBe(element.find('span')[0]);
52+
});
53+
54+
it('should ignore whitespace for exact bindings', function() {
55+
element =
56+
'<div>' +
57+
' <span>{{ name }}</span>' +
58+
' <span>{{username}}</span>' +
59+
'</div>';
60+
element = $compile(element)(scope);
61+
var users = $$testability.findBindings(element[0], 'name', true);
62+
expect(users.length).toBe(1);
63+
expect(users[0]).toBe(element.find('span')[0]);
64+
});
65+
66+
it('should find bindings by class', function() {
67+
element =
68+
'<div>' +
69+
' <span ng-bind="name"></span>' +
70+
' <span>{{username}}</span>' +
71+
'</div>';
72+
element = $compile(element)(scope);
73+
var names = $$testability.findBindings(element[0], 'name');
74+
expect(names.length).toBe(2);
75+
expect(names[0]).toBe(element.find('span')[0]);
76+
expect(names[1]).toBe(element.find('span')[1]);
77+
});
78+
79+
it('should only search within the context element', function() {
80+
element =
81+
'<div>' +
82+
' <ul><li>{{name}}</li></ul>' +
83+
' <ul><li>{{name}}</li></ul>' +
84+
'</div>';
85+
element = $compile(element)(scope);
86+
var names = $$testability.findBindings(element.find('ul')[0], 'name');
87+
expect(names.length).toBe(1);
88+
expect(names[0]).toBe(element.find('li')[0]);
89+
});
90+
91+
it('should find partial models', function() {
92+
element =
93+
'<div>' +
94+
' <input type="text" ng-model="name"/>' +
95+
' <input type="text" ng-model="username"/>' +
96+
'</div>';
97+
element = $compile(element)(scope);
98+
var names = $$testability.findModels(element[0], 'name');
99+
expect(names.length).toBe(2);
100+
expect(names[0]).toBe(element.find('input')[0]);
101+
expect(names[1]).toBe(element.find('input')[1]);
102+
});
103+
104+
it('should find exact models', function() {
105+
element =
106+
'<div>' +
107+
' <input type="text" ng-model="name"/>' +
108+
' <input type="text" ng-model="username"/>' +
109+
'</div>';
110+
element = $compile(element)(scope);
111+
var users = $$testability.findModels(element[0], 'name', true);
112+
expect(users.length).toBe(1);
113+
expect(users[0]).toBe(element.find('input')[0]);
114+
});
115+
116+
it('should find models in different input types', function() {
117+
element =
118+
'<div>' +
119+
' <input type="text" ng-model="name"/>' +
120+
' <textarea ng-model="username"/>' +
121+
'</div>';
122+
element = $compile(element)(scope);
123+
var names = $$testability.findModels(element[0], 'name');
124+
expect(names.length).toBe(2);
125+
expect(names[0]).toBe(element.find('input')[0]);
126+
expect(names[1]).toBe(element.find('textarea')[0]);
127+
});
128+
129+
it('should only search for models within the context element', function() {
130+
element =
131+
'<div>' +
132+
' <ul><li><input type="text" ng-model="name"/></li></ul>' +
133+
' <ul><li><input type="text" ng-model="name"/></li></ul>' +
134+
'</div>';
135+
element = $compile(element)(scope);
136+
var names = $$testability.findModels(element.find('ul')[0], 'name');
137+
expect(names.length).toBe(1);
138+
expect(names[0]).toBe(angular.element(element.find('li')[0]).find('input')[0]);
139+
});
140+
});
141+
142+
describe('location', function() {
143+
beforeEach(module(function() {
144+
return function($httpBackend) {
145+
$httpBackend.when('GET', 'foo.html').respond('foo');
146+
$httpBackend.when('GET', 'baz.html').respond('baz');
147+
$httpBackend.when('GET', 'bar.html').respond('bar');
148+
$httpBackend.when('GET', '404.html').respond('not found');
149+
};
150+
}));
151+
152+
it('should return the current URL', inject(function($location, $$testability) {
153+
$location.path('/bar.html');
154+
expect($$testability.getLocation()).toMatch(/bar.html$/);
155+
}));
156+
157+
it('should change the URL', inject(function($location, $$testability) {
158+
$location.path('/bar.html');
159+
$$testability.setLocation('foo.html');
160+
expect($location.path()).toEqual('/foo.html');
161+
}));
162+
});
163+
164+
describe('waiting for stability', function() {
165+
it('should process callbacks immediately with no outstanding requests',
166+
inject(function($$testability) {
167+
var callback = jasmine.createSpy('callback');
168+
$$testability.whenStable(callback);
169+
expect(callback).toHaveBeenCalled();
170+
}));
171+
});
172+
});

0 commit comments

Comments
 (0)
Please sign in to comment.