Skip to content

Commit cc8d143

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 2efe1c2 commit cc8d143

10 files changed

+291
-4
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',

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">

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
@@ -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,
@@ -1430,6 +1431,19 @@ function bootstrap(element, modules, config) {
14301431
};
14311432
}
14321433

1434+
/**
1435+
* @ngdoc function
1436+
* @name angular.getTestability
1437+
* @module ng
1438+
* @description
1439+
* Get the testability service for the instance of Angular on the given
1440+
* element.
1441+
* @param {DOMElement} element DOM element which is the root of angular application.
1442+
*/
1443+
function getTestability(rootElement) {
1444+
return angular.element(rootElement).injector().get('$$testability');
1445+
}
1446+
14331447
var SNAKE_CASE_REGEXP = /[A-Z]/g;
14341448
function snake_case(name, separator) {
14351449
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/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/testability.js

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.
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+
var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding;
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+
return matches;
49+
};
50+
51+
/**
52+
* @name $$testability#findModels
53+
*
54+
* @description
55+
* Returns an array of elements that are two-way found via ng-model to
56+
* expressions matching the input.
57+
*
58+
* @param {Element} element The element root to search from.
59+
* @param {string} expression The model expression to match.
60+
* @param {boolean} opt_exactMatch If true, only returns exact matches
61+
* for the expression.
62+
*/
63+
testability.findModels = function(element, expression, opt_exactMatch) {
64+
var prefixes = ['ng-', 'data-ng-', 'ng\\:'];
65+
for (var p = 0; p < prefixes.length; ++p) {
66+
var attributeEquals = opt_exactMatch ? '=' : '*=';
67+
var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
68+
var elements = element.querySelectorAll(selector);
69+
if (elements.length) {
70+
return elements;
71+
}
72+
}
73+
};
74+
75+
/**
76+
* @name $$testability#getLocation
77+
*
78+
* @description
79+
* Shortcut for getting the location in a browser agnostic way. Returns
80+
* the path, search, and hash. (e.g. /path?a=b#hash)
81+
*/
82+
testability.getLocation = function() {
83+
return $location.url();
84+
};
85+
86+
/**
87+
* @name $$testability#setLocation
88+
*
89+
* @description
90+
* Shortcut for navigating to a location without doing a full page reload.
91+
*
92+
* @param {string} url The location url (path, search and hash,
93+
* e.g. /path?a=b#hash) to go to.
94+
*/
95+
testability.setLocation = function(url) {
96+
if (url !== $location.url()) {
97+
$location.url(url);
98+
$rootScope.$digest();
99+
}
100+
};
101+
102+
/**
103+
* @name $$testability#whenStable
104+
*
105+
* @description
106+
* Calls the callback when $timeout and $http requests are completed.
107+
*
108+
* @param {function} callback
109+
*/
110+
testability.whenStable = function(callback) {
111+
$browser.notifyWhenNoOutstandingRequests(callback);
112+
};
113+
114+
return testability;
115+
}];
116+
}

test/ng/testabilitySpec.js

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

0 commit comments

Comments
 (0)