Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

feat(testability): add $testability service #8767

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion docs/content/guide/expression.ngdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
4 changes: 2 additions & 2 deletions docs/content/guide/module.ngdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>

Expand Down
2 changes: 1 addition & 1 deletion npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"encodeUriQuery": false,
"angularInit": false,
"bootstrap": false,
"getTestability": false,
"snake_case": false,
"bindJQuery": false,
"assertArg": false,
Expand Down
13 changes: 13 additions & 0 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
encodeUriQuery: true,
angularInit: true,
bootstrap: true,
getTestability: true,
snake_case: true,
bindJQuery: true,
assertArg: true,
Expand Down Expand Up @@ -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 || '_';
Expand Down
3 changes: 3 additions & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
$SnifferProvider,
$TemplateCacheProvider,
$TemplateRequestProvider,
$$TestabilityProvider,
$TimeoutProvider,
$$RAFProvider,
$$AsyncCallbackProvider,
Expand Down Expand Up @@ -136,6 +137,7 @@ function publishExternalAPI(angular){
'lowercase': lowercase,
'uppercase': uppercase,
'callbacks': {counter: 0},
'getTestability': getTestability,
'$$minErr': minErr,
'$$csp': csp,
'reloadWithDebugInfo': reloadWithDebugInfo
Expand Down Expand Up @@ -230,6 +232,7 @@ function publishExternalAPI(angular){
$sniffer: $SnifferProvider,
$templateCache: $TemplateCacheProvider,
$templateRequest: $TemplateRequestProvider,
$$testability: $$TestabilityProvider,
$timeout: $TimeoutProvider,
$window: $WindowProvider,
$$rAF: $$RAFProvider,
Expand Down
8 changes: 4 additions & 4 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -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'));
*
Expand Down Expand Up @@ -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]');
* });
Expand Down
4 changes: 3 additions & 1 deletion src/ng/directive/ngEventDirs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
6 changes: 3 additions & 3 deletions src/ng/directive/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion src/ng/filter/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions src/ng/filter/limitTo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
117 changes: 117 additions & 0 deletions src/ng/testability.js
Original file line number Diff line number Diff line change
@@ -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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setLocation works with path while get location returns absolute url. that is inconsistent and surprising.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. Both work on url now (everything past the authority, e.g. /path?a=b#hash)

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;
}];
}
Loading