Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 97ddb35

Browse files
committedAug 25, 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 fdf9989 commit 97ddb35

8 files changed

+309
-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

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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-', 'ng_', 'data-ng-', 'x-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.
91+
*/
92+
testability.getLocation = function() {
93+
return $location.absUrl();
94+
};
95+
96+
/**
97+
* @ngdoc method
98+
* @name $testability#setLocation
99+
*
100+
* @description
101+
* Shortcut for navigating to a location without doing a full page reload.
102+
*
103+
* @param {string} path The location path to go to.
104+
*/
105+
testability.setLocation = function(path) {
106+
if (path !== $location.path()) {
107+
$location.path(path);
108+
$rootScope.$digest();
109+
}
110+
};
111+
112+
/**
113+
* @ngdoc method
114+
* @name $testability#whenStable
115+
*
116+
* @description
117+
* Calls the callback when $timeout and $http requests are completed.
118+
*
119+
* @param {function} callback
120+
*/
121+
testability.whenStable = function(callback) {
122+
$browser.notifyWhenNoOutstandingRequests(callback);
123+
};
124+
125+
return testability;
126+
}];
127+
}

‎test/ng/testabilitySpec.js

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

0 commit comments

Comments
 (0)
Please sign in to comment.