Skip to content

Commit dbcf82e

Browse files
committed
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 fdf9989 commit dbcf82e

8 files changed

+307
-2
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var angularFiles = {
3434
'src/ng/sanitizeUri.js',
3535
'src/ng/sce.js',
3636
'src/ng/sniffer.js',
37+
'src/ng/testability.js',
3738
'src/ng/timeout.js',
3839
'src/ng/urlUtils.js',
3940
'src/ng/window.js',

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

+14
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
encodeUriQuery: true,
7676
angularInit: true,
7777
bootstrap: true,
78+
getTestability: true,
7879
snake_case: true,
7980
bindJQuery: true,
8081
assertArg: true,
@@ -1442,6 +1443,19 @@ function bootstrap(element, modules, config) {
14421443
};
14431444
}
14441445

1446+
/**
1447+
* @ngdoc function
1448+
* @name angular.getTestability
1449+
* @module ng
1450+
* @description
1451+
* Get the testability service for the instance of Angular on the given
1452+
* element.
1453+
* @param {DOMElement} element DOM element which is the root of angular application.
1454+
*/
1455+
function getTestability(rootElement) {
1456+
return angular.element(rootElement).injector().get('$testability');
1457+
}
1458+
14451459
var SNAKE_CASE_REGEXP = /[A-Z]/g;
14461460
function snake_case(name, separator) {
14471461
separator = separator || '_';

src/AngularPublic.js

+3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
$SceDelegateProvider,
7979
$SnifferProvider,
8080
$TemplateCacheProvider,
81+
$TestabilityProvider,
8182
$TimeoutProvider,
8283
$$RAFProvider,
8384
$$AsyncCallbackProvider,
@@ -135,6 +136,7 @@ function publishExternalAPI(angular){
135136
'lowercase': lowercase,
136137
'uppercase': uppercase,
137138
'callbacks': {counter: 0},
139+
'getTestability': getTestability,
138140
'$$minErr': minErr,
139141
'$$csp': csp
140142
});
@@ -227,6 +229,7 @@ function publishExternalAPI(angular){
227229
$sceDelegate: $SceDelegateProvider,
228230
$sniffer: $SnifferProvider,
229231
$templateCache: $TemplateCacheProvider,
232+
$testability: $TestabilityProvider,
230233
$timeout: $TimeoutProvider,
231234
$window: $WindowProvider,
232235
$$rAF: $$RAFProvider,

src/ng/testability.js

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

test/ng/testabilitySpec.js

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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 find bindings by class', function() {
43+
element =
44+
'<div>' +
45+
' <span ng-bind="name"></span>' +
46+
' <span>{{username}}</span>' +
47+
'</div>';
48+
element = $compile(element)(scope);
49+
var names = $testability.findBindings(element[0], 'name');
50+
expect(names.length).toBe(2);
51+
expect(names[0]).toBe(element.find('span')[0]);
52+
expect(names[1]).toBe(element.find('span')[1]);
53+
});
54+
55+
it('should only search within the context element', function() {
56+
element =
57+
'<div>' +
58+
' <ul><li>{{name}}</li></ul>' +
59+
' <ul><li>{{name}}</li></ul>' +
60+
'</div>';
61+
element = $compile(element)(scope);
62+
var names = $testability.findBindings(element.find('ul')[0], 'name');
63+
expect(names.length).toBe(1);
64+
expect(names[0]).toBe(element.find('li')[0]);
65+
});
66+
67+
it('should include the root element in the context', function() {
68+
element =
69+
'<div>{{name}}</div>';
70+
element = $compile(element)(scope);
71+
var names = $testability.findBindings(element[0], 'name');
72+
expect(names.length).toBe(1);
73+
expect(names[0]).toBe(element[0]);
74+
});
75+
76+
it('should find partial models', function() {
77+
element =
78+
'<div>' +
79+
' <input type="text" ng-model="name"/>' +
80+
' <input type="text" ng-model="username"/>' +
81+
'</div>';
82+
element = $compile(element)(scope);
83+
var names = $testability.findModels(element[0], 'name');
84+
expect(names.length).toBe(2);
85+
expect(names[0]).toBe(element.find('input')[0]);
86+
expect(names[1]).toBe(element.find('input')[1]);
87+
});
88+
89+
it('should find exact models', function() {
90+
element =
91+
'<div>' +
92+
' <input type="text" ng-model="name"/>' +
93+
' <input type="text" ng-model="username"/>' +
94+
'</div>';
95+
element = $compile(element)(scope);
96+
var users = $testability.findModels(element[0], 'name', true);
97+
expect(users.length).toBe(1);
98+
expect(users[0]).toBe(element.find('input')[0]);
99+
});
100+
101+
it('should find models in different input types', function() {
102+
element =
103+
'<div>' +
104+
' <input type="text" ng-model="name"/>' +
105+
' <textarea ng-model="username"/>' +
106+
'</div>';
107+
element = $compile(element)(scope);
108+
var names = $testability.findModels(element[0], 'name');
109+
expect(names.length).toBe(2);
110+
expect(names[0]).toBe(element.find('input')[0]);
111+
expect(names[1]).toBe(element.find('textarea')[0]);
112+
});
113+
114+
it('should only search for models within the context element', function() {
115+
element =
116+
'<div>' +
117+
' <ul><li><input type="text" ng-model="name"/></li></ul>' +
118+
' <ul><li><input type="text" ng-model="name"/></li></ul>' +
119+
'</div>';
120+
element = $compile(element)(scope);
121+
var names = $testability.findModels(element.find('ul')[0], 'name');
122+
expect(names.length).toBe(1);
123+
expect(names[0]).toBe(angular.element(element.find('li')[0]).find('input')[0]);
124+
});
125+
});
126+
127+
describe('location', function() {
128+
beforeEach(module(function() {
129+
return function($httpBackend) {
130+
$httpBackend.when('GET', 'foo.html').respond('foo');
131+
$httpBackend.when('GET', 'baz.html').respond('baz');
132+
$httpBackend.when('GET', 'bar.html').respond('bar');
133+
$httpBackend.when('GET', '404.html').respond('not found');
134+
};
135+
}));
136+
137+
it('should return the current URL', inject(function($location, $testability) {
138+
$location.path('/bar.html');
139+
expect($testability.getLocation()).toMatch(/bar.html$/);
140+
}));
141+
142+
it('should change the URL', inject(function($location, $testability) {
143+
$location.path('/bar.html');
144+
$testability.setLocation('foo.html');
145+
expect($location.path()).toEqual('/foo.html');
146+
}));
147+
});
148+
149+
describe('waiting for stability', function() {
150+
it('should process callbacks immediately with no outstanding requests',
151+
inject(function($testability) {
152+
var callback = jasmine.createSpy('callback');
153+
$testability.whenStable(callback);
154+
expect(callback).toHaveBeenCalled();
155+
}));
156+
});
157+
});

0 commit comments

Comments
 (0)