Skip to content

Commit d4f6e87

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 dfbe69c commit d4f6e87

File tree

5 files changed

+290
-0
lines changed

5 files changed

+290
-0
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',

src/Angular.js

+13
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,19 @@ function bootstrap(element, modules, config) {
14411441
};
14421442
}
14431443

1444+
/**
1445+
* @ngdoc function
1446+
* @name angular.getTestability
1447+
* @module ng
1448+
* @description
1449+
* Get the testability service for the instance of Angular on the given
1450+
* element.
1451+
* @param {DOMElement} element DOM element which is the root of angular application.
1452+
*/
1453+
function getTestability(rootElement) {
1454+
return angular.element(rootElement).injector().get('$testability');
1455+
}
1456+
14441457
var SNAKE_CASE_REGEXP = /[A-Z]/g;
14451458
function snake_case(name, separator) {
14461459
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

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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 bindings = element.getElementsByClassName('ng-binding');
33+
var matches = [];
34+
for (var i = 0; i < bindings.length; ++i) {
35+
var dataBinding = angular.element(bindings[i]).data('$binding');
36+
if (dataBinding) {
37+
for (var j = 0; j < bindingNames.length; ++j) {
38+
if (opt_exactMatch) {
39+
var matcher = new RegExp('([^a-zA-Z\\d]|$)' + expression + '([^a-zA-Z\\d]|^)');
40+
if (matcher.test(dataBinding[j])) {
41+
matches.push(bindings[i]);
42+
}
43+
} else {
44+
if (dataBinding[j].indexOf(expression) != -1) {
45+
matches.push(bindings[i]);
46+
}
47+
}
48+
}
49+
}
50+
}
51+
return matches;
52+
};
53+
54+
/**
55+
* @ngdoc method
56+
* @name $testability#findModels
57+
*
58+
* @description
59+
* Returns an array of elements that are two-way found via ng-model to
60+
* expressions matching the input.
61+
*
62+
* @param {Element} element The element root to search from.
63+
* @param {string} expression The model expression to match.
64+
* @param {boolean} opt_exactMatch If true, only returns exact matches
65+
* for the expression.
66+
*/
67+
testability.findModels = function(element, expression, opt_exactMatch) {
68+
var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];
69+
for (var p = 0; p < prefixes.length; ++p) {
70+
var attributeEquals = opt_exactMatch ? '=' : '*=';
71+
var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
72+
var elements = element.querySelectorAll(selector);
73+
if (elements.length) {
74+
return elements;
75+
}
76+
}
77+
};
78+
79+
/**
80+
* @ngdoc method
81+
* @name $testability#getLocation
82+
*
83+
* @description
84+
* Shortcut for getting the location in a browser agnostic way.
85+
*/
86+
testability.getLocation = function() {
87+
return $location.absUrl();
88+
};
89+
90+
/**
91+
* @ngdoc method
92+
* @name $testability#setLocation
93+
*
94+
* @description
95+
* Shortcut for navigating to a location without doing a full page reload.
96+
*
97+
* @param {string} path The location path to go to.
98+
*/
99+
testability.setLocation = function(path) {
100+
if (path !== $location.path()) {
101+
$location.path(path);
102+
$rootScope.$digest();
103+
}
104+
};
105+
106+
/**
107+
* @ngdoc method
108+
* @name $testability#whenStable
109+
*
110+
* @description
111+
* Calls the callback when $timeout and $http requests are completed.
112+
*
113+
* @param {function} callback
114+
*/
115+
testability.whenStable = function(callback) {
116+
$browser.notifyWhenNoOutstandingRequests(callback);
117+
};
118+
119+
return testability;
120+
}];
121+
}

test/ng/testabilitySpec.js

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

0 commit comments

Comments
 (0)