mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
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:
1
angularFiles.js
vendored
1
angularFiles.js
vendored
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
2
npm-shrinkwrap.json
generated
@@ -3230,7 +3230,7 @@
|
||||
}
|
||||
},
|
||||
"protractor": {
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0-beta1",
|
||||
"dependencies": {
|
||||
"request": {
|
||||
"version": "2.36.0",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"encodeUriQuery": false,
|
||||
"angularInit": false,
|
||||
"bootstrap": false,
|
||||
"getTestability": false,
|
||||
"snake_case": false,
|
||||
"bindJQuery": false,
|
||||
"assertArg": false,
|
||||
|
||||
@@ -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 || '_';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]');
|
||||
* });
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
117
src/ng/testability.js
Normal 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
172
test/ng/testabilitySpec.js
Normal 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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user