Skip to content

WIP - add testability on top of binding info changes #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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 @@ -34,6 +34,7 @@ var angularFiles = {
'src/ng/sanitizeUri.js',
'src/ng/sce.js',
'src/ng/sniffer.js',
'src/ng/testability.js',
'src/ng/timeout.js',
'src/ng/urlUtils.js',
'src/ng/window.js',
Expand Down
13 changes: 13 additions & 0 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,19 @@ function bootstrap(element, modules, config) {
};
}

/**
* @ngdoc function
* @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 @@ -78,6 +78,7 @@
$SceDelegateProvider,
$SnifferProvider,
$TemplateCacheProvider,
$TestabilityProvider,
$TimeoutProvider,
$$RAFProvider,
$$AsyncCallbackProvider,
Expand Down Expand Up @@ -135,6 +136,7 @@ function publishExternalAPI(angular){
'lowercase': lowercase,
'uppercase': uppercase,
'callbacks': {counter: 0},
'getTestability': getTestability,
'$$minErr': minErr,
'$$csp': csp
});
Expand Down Expand Up @@ -227,6 +229,7 @@ function publishExternalAPI(angular){
$sceDelegate: $SceDelegateProvider,
$sniffer: $SnifferProvider,
$templateCache: $TemplateCacheProvider,
$testability: $TestabilityProvider,
$timeout: $TimeoutProvider,
$window: $WindowProvider,
$$rAF: $$RAFProvider,
Expand Down
121 changes: 121 additions & 0 deletions src/ng/testability.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use strict';


function $TestabilityProvider() {
this.$get = ['$rootScope', '$browser', '$location',
function($rootScope, $browser, $location) {

/**
* @ngdoc service
* @name $testability
*
* @description
* The $testability service provides a collection of methods for use when debugging
* or by automated test and debugging tools.
*/
var testability = {};

/**
* @ngdoc method
* @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.
*/
testability.findBindings = function(element, expression, opt_exactMatch) {
var bindings = element.getElementsByClassName('ng-binding');
var matches = [];
for (var i = 0; i < bindings.length; ++i) {
var dataBinding = angular.element(bindings[i]).data('$binding');
if (dataBinding) {
for (var j = 0; j < bindingNames.length; ++j) {
if (opt_exactMatch) {
var matcher = new RegExp('([^a-zA-Z\\d]|$)' + expression + '([^a-zA-Z\\d]|^)');
if (matcher.test(dataBinding[j])) {
matches.push(bindings[i]);
}
} else {
if (dataBinding[j].indexOf(expression) != -1) {
matches.push(bindings[i]);
}
}
}
}
}
return matches;
};

/**
* @ngdoc method
* @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-', 'ng_', 'data-ng-', 'x-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;
}
}
};

/**
* @ngdoc method
* @name $testability#getLocation
*
* @description
* Shortcut for getting the location in a browser agnostic way.
*/
testability.getLocation = function() {
return $location.absUrl();
};

/**
* @ngdoc method
* @name $testability#setLocation
*
* @description
* Shortcut for navigating to a location without doing a full page reload.
*
* @param {string} path The location path to go to.
*/
testability.setLocation = function(path) {
if (path !== $location.path()) {
$location.path(path);
$rootScope.$digest();
}
};

/**
* @ngdoc method
* @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;
}];
}
152 changes: 152 additions & 0 deletions test/ng/testabilitySpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
'use strict';

ddescribe('$testability', function() {
describe('finding elements', function() {
var $testability, $compile, scope, element;

// beforeEach(module(function($compileProvider) {
// $compileProvider.enableDebugInfo(true);
// }));

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