diff --git a/angularFiles.js b/angularFiles.js index 924fcd487fb2..facb607af961 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -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', diff --git a/docs/content/guide/expression.ngdoc b/docs/content/guide/expression.ngdoc index 1650a8286021..9c4069355709 100644 --- a/docs/content/guide/expression.ngdoc +++ b/docs/content/guide/expression.ngdoc @@ -38,7 +38,9 @@ the method from your view. If you want to `eval()` an Angular expression yoursel ## Example - 1+2={{1+2}} + + 1+2={{1+2}} + diff --git a/docs/content/guide/module.ngdoc b/docs/content/guide/module.ngdoc index 1ef968347a05..0b0297904bc6 100644 --- a/docs/content/guide/module.ngdoc +++ b/docs/content/guide/module.ngdoc @@ -50,7 +50,7 @@ I'm in a hurry. How do I get a Hello World module working? 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!'); }); @@ -128,7 +128,7 @@ The above is a suggestion. Tailor it to your needs. 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!'); }); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cc3cf7043cd4..c215feba8bb6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -3230,7 +3230,7 @@ } }, "protractor": { - "version": "1.1.1", + "version": "1.2.0-beta1", "dependencies": { "request": { "version": "2.36.0", diff --git a/package.json b/package.json index e86ec044616d..75caf15aa889 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/.jshintrc b/src/.jshintrc index 6b7190b12ff8..e26593342df5 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -79,6 +79,7 @@ "encodeUriQuery": false, "angularInit": false, "bootstrap": false, + "getTestability": false, "snake_case": false, "bindJQuery": false, "assertArg": false, diff --git a/src/Angular.js b/src/Angular.js index d3c80e1cc841..a90d7ee7b35b 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -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 || '_'; diff --git a/src/AngularPublic.js b/src/AngularPublic.js index f81b613f644c..c263e1a9c8cf 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -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, diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 068079fb1e51..f5abfee8be56 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -814,7 +814,7 @@ var inputType = { 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 - 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() { * * * 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() { * * 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]'); * }); diff --git a/src/ng/directive/ngEventDirs.js b/src/ng/directive/ngEventDirs.js index d228c76077c0..426ff234472e 100644 --- a/src/ng/directive/ngEventDirs.js +++ b/src/ng/directive/ngEventDirs.js @@ -19,7 +19,9 @@ - count: {{count}} + + count: {{count}} + it('should check ng-click', function() { diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index 285791e64727..f8f2f0b81297 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -123,13 +123,13 @@ var ngOptionsMinErr = minErr('ngOptions'); 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'); }); diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js index 0b69d7b4ba07..65ea0e1ab0f4 100644 --- a/src/ng/filter/filters.js +++ b/src/ng/filter/filters.js @@ -490,7 +490,7 @@ function dateFilter($locale) { 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}/); }); diff --git a/src/ng/filter/limitTo.js b/src/ng/filter/limitTo.js index 6f01aa9c1f86..52abd260b9a2 100644 --- a/src/ng/filter/limitTo.js +++ b/src/ng/filter/limitTo.js @@ -40,8 +40,8 @@ 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'); diff --git a/src/ng/testability.js b/src/ng/testability.js new file mode 100644 index 000000000000..299ce2f95b95 --- /dev/null +++ b/src/ng/testability.js @@ -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; + }]; +} diff --git a/test/ng/testabilitySpec.js b/test/ng/testabilitySpec.js new file mode 100644 index 000000000000..6454248531b2 --- /dev/null +++ b/test/ng/testabilitySpec.js @@ -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 = + '
' + + ' {{name}}' + + ' {{username}}' + + '
'; + 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 = + '
' + + ' {{name}}' + + ' {{username}}' + + '
'; + 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 = + '
' + + ' {{name | uppercase}}' + + ' {{username}}' + + '
'; + 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 = + '
' + + ' {{ name }}' + + ' {{username}}' + + '
'; + 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 = + '
' + + ' ' + + ' {{username}}' + + '
'; + 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 = + '
' + + ' ' + + ' ' + + '
'; + 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 = + '
' + + ' ' + + ' ' + + '
'; + 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 = + '
' + + ' ' + + ' ' + + '
'; + 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 = + '
' + + ' ' + + '