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 =
+ '' +
+ ' ' +
+ ' ' +
+ '
';
+ 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 =
+ '';
+ 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();
+ }));
+ });
+});