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
https://github.com/angular/angular.dart/pull/1191
This commit is contained in:
Julie
2014-06-09 22:20:47 -07:00
committed by Julie Ralph
parent 46343c603d
commit 85880a6490
15 changed files with 327 additions and 16 deletions

1
angularFiles.js vendored
View File

@@ -35,6 +35,7 @@ var angularFiles = {
'src/ng/sce.js',
'src/ng/sniffer.js',
'src/ng/templateRequest.js',
'src/ng/testability.js',
'src/ng/timeout.js',
'src/ng/urlUtils.js',
'src/ng/window.js',

View File

@@ -38,7 +38,9 @@ the method from your view. If you want to `eval()` an Angular expression yoursel
## Example
<example>
<file name="index.html">
1+2={{1+2}}
<span>
1+2={{1+2}}
</span>
</file>
<file name="protractor.js" type="protractor">

View File

@@ -50,7 +50,7 @@ I'm in a hurry. How do I get a Hello World module working?
<file name="protractor.js" type="protractor">
it('should add Hello to the name', function() {
expect(element(by.binding(" 'World' | greet ")).getText()).toEqual('Hello, World!');
expect(element(by.binding("'World' | greet")).getText()).toEqual('Hello, World!');
});
</file>
</example>
@@ -128,7 +128,7 @@ The above is a suggestion. Tailor it to your needs.
<file name="protractor.js" type="protractor">
it('should add Hello to the name', function() {
expect(element(by.binding(" greeting ")).getText()).toEqual('Bonjour World!');
expect(element(by.binding("greeting")).getText()).toEqual('Bonjour World!');
});
</file>

2
npm-shrinkwrap.json generated
View File

@@ -3230,7 +3230,7 @@
}
},
"protractor": {
"version": "1.1.1",
"version": "1.2.0-beta1",
"dependencies": {
"request": {
"version": "2.36.0",

View File

@@ -33,7 +33,7 @@
"karma-sauce-launcher": "0.2.0",
"karma-script-launcher": "0.1.0",
"karma-browserstack-launcher": "0.0.7",
"protractor": "1.0.0",
"protractor": "1.2.0-beta1",
"yaml-js": "~0.0.8",
"rewire": "1.1.3",
"promises-aplus-tests": "~2.0.4",

View File

@@ -79,6 +79,7 @@
"encodeUriQuery": false,
"angularInit": false,
"bootstrap": false,
"getTestability": false,
"snake_case": false,
"bindJQuery": false,
"assertArg": false,

View File

@@ -74,6 +74,7 @@
encodeUriQuery: true,
angularInit: true,
bootstrap: true,
getTestability: true,
snake_case: true,
bindJQuery: true,
assertArg: true,
@@ -1459,6 +1460,18 @@ function reloadWithDebugInfo() {
window.location.reload();
}
/*
* @name angular.getTestability
* @module ng
* @description
* Get the testability service for the instance of Angular on the given
* element.
* @param {DOMElement} element DOM element which is the root of angular application.
*/
function getTestability(rootElement) {
return angular.element(rootElement).injector().get('$$testability');
}
var SNAKE_CASE_REGEXP = /[A-Z]/g;
function snake_case(name, separator) {
separator = separator || '_';

View File

@@ -79,6 +79,7 @@
$SnifferProvider,
$TemplateCacheProvider,
$TemplateRequestProvider,
$$TestabilityProvider,
$TimeoutProvider,
$$RAFProvider,
$$AsyncCallbackProvider,
@@ -136,6 +137,7 @@ function publishExternalAPI(angular){
'lowercase': lowercase,
'uppercase': uppercase,
'callbacks': {counter: 0},
'getTestability': getTestability,
'$$minErr': minErr,
'$$csp': csp,
'reloadWithDebugInfo': reloadWithDebugInfo
@@ -230,6 +232,7 @@ function publishExternalAPI(angular){
$sniffer: $SnifferProvider,
$templateCache: $TemplateCacheProvider,
$templateRequest: $TemplateRequestProvider,
$$testability: $$TestabilityProvider,
$timeout: $TimeoutProvider,
$window: $WindowProvider,
$$rAF: $$RAFProvider,

View File

@@ -814,7 +814,7 @@ var inputType = {
</file>
<file name="protractor.js" type="protractor">
it('should change state', function() {
var color = element(by.binding('color | json'));
var color = element(by.binding('color'));
expect(color.getText()).toContain('blue');
@@ -1313,7 +1313,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
</div>
</file>
<file name="protractor.js" type="protractor">
var user = element(by.binding('user'));
var user = element(by.exactBinding('user'));
var userNameValid = element(by.binding('myForm.userName.$valid'));
var lastNameValid = element(by.binding('myForm.lastName.$valid'));
var lastNameError = element(by.binding('myForm.lastName.$error'));
@@ -2542,7 +2542,7 @@ var minlengthDirective = function() {
* </file>
* <file name="protractor.js" type="protractor">
* var listInput = element(by.model('names'));
* var names = element(by.binding('names'));
* var names = element(by.exactBinding('names'));
* var valid = element(by.binding('myForm.namesInput.$valid'));
* var error = element(by.css('span.error'));
*
@@ -2572,7 +2572,7 @@ var minlengthDirective = function() {
* <file name="protractor.js" type="protractor">
* it("should split the text by newlines", function() {
* var listInput = element(by.model('list'));
* var output = element(by.binding(' list | json '));
* var output = element(by.binding('list | json'));
* listInput.sendKeys('abc\ndef\nghi');
* expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
* });

View File

@@ -19,7 +19,9 @@
<button ng-click="count = count + 1" ng-init="count=0">
Increment
</button>
count: {{count}}
<span>
count: {{count}}
</span>
</file>
<file name="protractor.js" type="protractor">
it('should check ng-click', function() {

View File

@@ -123,13 +123,13 @@ var ngOptionsMinErr = minErr('ngOptions');
</file>
<file name="protractor.js" type="protractor">
it('should check ng-options', function() {
expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('red');
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
element.all(by.model('myColor')).first().click();
element.all(by.css('select[ng-model="myColor"] option')).first().click();
expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('black');
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
element(by.css('.nullable select[ng-model="myColor"]')).click();
element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
expect(element(by.binding(' {selected_color:myColor} ')).getText()).toMatch('null');
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
});
</file>
</example>

View File

@@ -490,7 +490,7 @@ function dateFilter($locale) {
</file>
<file name="protractor.js" type="protractor">
it('should jsonify filtered objects', function() {
expect(element(by.binding(" {'name':'value'} | json ")).getText()).toMatch(/\{\n "name": ?"value"\n}/);
expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/);
});
</file>
</example>

View File

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

117
src/ng/testability.js Normal file
View File

@@ -0,0 +1,117 @@
'use strict';
function $$TestabilityProvider() {
this.$get = ['$rootScope', '$browser', '$location',
function($rootScope, $browser, $location) {
/**
* @name $testability
*
* @description
* The private $$testability service provides a collection of methods for use when debugging
* or by automated test and debugging tools.
*/
var testability = {};
/**
* @name $$testability#findBindings
*
* @description
* Returns an array of elements that are bound (via ng-bind or {{}})
* to expressions matching the input.
*
* @param {Element} element The element root to search from.
* @param {string} expression The binding expression to match.
* @param {boolean} opt_exactMatch If true, only returns exact matches
* for the expression. Filters and whitespace are ignored.
*/
testability.findBindings = function(element, expression, opt_exactMatch) {
var bindings = element.getElementsByClassName('ng-binding');
var matches = [];
forEach(bindings, function(binding) {
var dataBinding = angular.element(binding).data('$binding');
if (dataBinding) {
forEach(dataBinding, function(bindingName) {
if (opt_exactMatch) {
var matcher = new RegExp('(^|\\s)' + expression + '(\\s|\\||$)');
if (matcher.test(bindingName)) {
matches.push(binding);
}
} else {
if (bindingName.indexOf(expression) != -1) {
matches.push(binding);
}
}
});
}
});
return matches;
};
/**
* @name $$testability#findModels
*
* @description
* Returns an array of elements that are two-way found via ng-model to
* expressions matching the input.
*
* @param {Element} element The element root to search from.
* @param {string} expression The model expression to match.
* @param {boolean} opt_exactMatch If true, only returns exact matches
* for the expression.
*/
testability.findModels = function(element, expression, opt_exactMatch) {
var prefixes = ['ng-', 'data-ng-', 'ng\\:'];
for (var p = 0; p < prefixes.length; ++p) {
var attributeEquals = opt_exactMatch ? '=' : '*=';
var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
var elements = element.querySelectorAll(selector);
if (elements.length) {
return elements;
}
}
};
/**
* @name $$testability#getLocation
*
* @description
* Shortcut for getting the location in a browser agnostic way. Returns
* the path, search, and hash. (e.g. /path?a=b#hash)
*/
testability.getLocation = function() {
return $location.url();
};
/**
* @name $$testability#setLocation
*
* @description
* Shortcut for navigating to a location without doing a full page reload.
*
* @param {string} url The location url (path, search and hash,
* e.g. /path?a=b#hash) to go to.
*/
testability.setLocation = function(url) {
if (url !== $location.url()) {
$location.url(url);
$rootScope.$digest();
}
};
/**
* @name $$testability#whenStable
*
* @description
* Calls the callback when $timeout and $http requests are completed.
*
* @param {function} callback
*/
testability.whenStable = function(callback) {
$browser.notifyWhenNoOutstandingRequests(callback);
};
return testability;
}];
}

172
test/ng/testabilitySpec.js Normal file
View File

@@ -0,0 +1,172 @@
'use strict';
describe('$$testability', function() {
describe('finding elements', function() {
var $$testability, $compile, scope, element;
beforeEach(inject(function(_$$testability_, _$compile_, $rootScope) {
$$testability = _$$testability_;
$compile = _$compile_;
scope = $rootScope.$new();
}));
afterEach(function() {
dealoc(element);
});
it('should find partial bindings', function() {
element =
'<div>' +
' <span>{{name}}</span>' +
' <span>{{username}}</span>' +
'</div>';
element = $compile(element)(scope);
var names = $$testability.findBindings(element[0], 'name');
expect(names.length).toBe(2);
expect(names[0]).toBe(element.find('span')[0]);
expect(names[1]).toBe(element.find('span')[1]);
});
it('should find exact bindings', function() {
element =
'<div>' +
' <span>{{name}}</span>' +
' <span>{{username}}</span>' +
'</div>';
element = $compile(element)(scope);
var users = $$testability.findBindings(element[0], 'name', true);
expect(users.length).toBe(1);
expect(users[0]).toBe(element.find('span')[0]);
});
it('should ignore filters for exact bindings', function() {
element =
'<div>' +
' <span>{{name | uppercase}}</span>' +
' <span>{{username}}</span>' +
'</div>';
element = $compile(element)(scope);
var users = $$testability.findBindings(element[0], 'name', true);
expect(users.length).toBe(1);
expect(users[0]).toBe(element.find('span')[0]);
});
it('should ignore whitespace for exact bindings', function() {
element =
'<div>' +
' <span>{{ name }}</span>' +
' <span>{{username}}</span>' +
'</div>';
element = $compile(element)(scope);
var users = $$testability.findBindings(element[0], 'name', true);
expect(users.length).toBe(1);
expect(users[0]).toBe(element.find('span')[0]);
});
it('should find bindings by class', function() {
element =
'<div>' +
' <span ng-bind="name"></span>' +
' <span>{{username}}</span>' +
'</div>';
element = $compile(element)(scope);
var names = $$testability.findBindings(element[0], 'name');
expect(names.length).toBe(2);
expect(names[0]).toBe(element.find('span')[0]);
expect(names[1]).toBe(element.find('span')[1]);
});
it('should only search within the context element', function() {
element =
'<div>' +
' <ul><li>{{name}}</li></ul>' +
' <ul><li>{{name}}</li></ul>' +
'</div>';
element = $compile(element)(scope);
var names = $$testability.findBindings(element.find('ul')[0], 'name');
expect(names.length).toBe(1);
expect(names[0]).toBe(element.find('li')[0]);
});
it('should find partial models', function() {
element =
'<div>' +
' <input type="text" ng-model="name"/>' +
' <input type="text" ng-model="username"/>' +
'</div>';
element = $compile(element)(scope);
var names = $$testability.findModels(element[0], 'name');
expect(names.length).toBe(2);
expect(names[0]).toBe(element.find('input')[0]);
expect(names[1]).toBe(element.find('input')[1]);
});
it('should find exact models', function() {
element =
'<div>' +
' <input type="text" ng-model="name"/>' +
' <input type="text" ng-model="username"/>' +
'</div>';
element = $compile(element)(scope);
var users = $$testability.findModels(element[0], 'name', true);
expect(users.length).toBe(1);
expect(users[0]).toBe(element.find('input')[0]);
});
it('should find models in different input types', function() {
element =
'<div>' +
' <input type="text" ng-model="name"/>' +
' <textarea ng-model="username"/>' +
'</div>';
element = $compile(element)(scope);
var names = $$testability.findModels(element[0], 'name');
expect(names.length).toBe(2);
expect(names[0]).toBe(element.find('input')[0]);
expect(names[1]).toBe(element.find('textarea')[0]);
});
it('should only search for models within the context element', function() {
element =
'<div>' +
' <ul><li><input type="text" ng-model="name"/></li></ul>' +
' <ul><li><input type="text" ng-model="name"/></li></ul>' +
'</div>';
element = $compile(element)(scope);
var names = $$testability.findModels(element.find('ul')[0], 'name');
expect(names.length).toBe(1);
expect(names[0]).toBe(angular.element(element.find('li')[0]).find('input')[0]);
});
});
describe('location', function() {
beforeEach(module(function() {
return function($httpBackend) {
$httpBackend.when('GET', 'foo.html').respond('foo');
$httpBackend.when('GET', 'baz.html').respond('baz');
$httpBackend.when('GET', 'bar.html').respond('bar');
$httpBackend.when('GET', '404.html').respond('not found');
};
}));
it('should return the current URL', inject(function($location, $$testability) {
$location.path('/bar.html');
expect($$testability.getLocation()).toMatch(/bar.html$/);
}));
it('should change the URL', inject(function($location, $$testability) {
$location.path('/bar.html');
$$testability.setLocation('foo.html');
expect($location.path()).toEqual('/foo.html');
}));
});
describe('waiting for stability', function() {
it('should process callbacks immediately with no outstanding requests',
inject(function($$testability) {
var callback = jasmine.createSpy('callback');
$$testability.whenStable(callback);
expect(callback).toHaveBeenCalled();
}));
});
});