I've found the only way to navigate to different URLs to do view and router behavior tests is to use Backbone.history.loadUrl(). Backbone.history.navigate('#something', true) and router.navigate('#something, {trigger: true, replace: true} and any combination thereof do not work within the test. My application does NOT use pushstate.
This works correctly within the context of a single test.
describe('that can navigate to something as expected', function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
//helper method does my responds to fetches, etc. My router in my app is what starts Backbone.history
this.router = initializeBackboneRouter(this.server, this.fixtures, false);
});
afterEach(function(){
this.server.restore();
Backbone.history.stop();
Backbone.history.loadUrl('#');
});
it('should create the view object', function(){
Backbone.history.loadUrl('#something');
expect(this.router.myView).toBeDefined();
});
});
During testing you can see that backbone is appending hashes as expected to the URL: localhost:8888/#something Depending on the test.
Unfortunately, loadUrl seems to be introducing a lot of inconsistencies in the way the tests behave. During one of my tests that involves some asynchronous JS where I need to wait for an AJAX call to complete, the fails about 50% of the time with a timeout or sometimes Expected undefined to be defined. If I console out the data I expect to be there it is, so I know it's not a BS test.
it('should add the rendered html to the body', function(){
runs(function(){
Backbone.history.loadUrl('#something');
});
waitsFor(function(){
var testEl = $('#title');
if(testEl.length > 0){ return true; }
}, 1000, 'UI to be set up');
runs(function(){
var testEl = $('#title');
expect(testEl.text()).toEqual(this.router.model.get(0).title);
});
});
The important note here is that it only fails when all tests are run; run by itself it passes 100% of the time.
My question then is: is Backbone.history.loadUrl a bad way to do programatic navigation around a backbone app in jasmine? I feel like I've tried everything to get this to simulate a user going to a specific URL. Is my teardown incorrect? I've tried it without the Backbone.history.loadUrl('#'); and got different behavior but not passing tests.
The core problem seems to be that in the context of several, hundreds, or even a few jasmine tests, Backbone.history is not clearing itself out and is sticking around as one instance of itself instead of being completely re-initialized at each test.
This sucked.
The solution was to edit my code a bit to add a loading complete flag that was set to true when i was sure that the DOM was 100% finished loading.
Then I wrote a helper function that waited for that flag to be true in my beforeEach function in the root of each test.
var waitForLoadingComplete = function(view){
waitsFor(function(){
if(view.loadingComplete == true){return true;}
}, 100, 'UI Setup Finished');
}
After that I refactored my setup into a helper function:
var setupViewTestEnvironment = function(options) {
var temp = {};
temp.server = sinon.fakeServer.create();
temp.router = initializeBackboneRouter(temp.server, options.fixture);
waitForLoadingComplete(temp.router.initialview);
runs(function(){
Backbone.history.loadUrl(options.url);
temp.view = temp.router[options.view];
temp.model = temp.router[options.model];
waitForLoadingComplete(temp.view);
});
return temp;
}
Example use:
beforeEach(function() {
this.testEnv = setupViewTestEnvironment({
url: '#profile',
view: 'profileIndex',
model: 'myModel',
fixture: this.fixtures
});
});
After which I had a view that i had loaded which I could be assured was finished loading so I could test stuff on the DOM or anything else I wanted to do.
Related
I added this code to my component controller to focus an input and it worked great in the browser but it broke all of my template tests. I thought I could just flush the $timeout and all would be well but it's not.
vm.$onInit = init;
function init(){
focusInput();
}
function focusInput(){
$timeout(function(){
$document[0]
.querySelector('md-autocomplete-wrap')
.querySelector('input')
.focus();
}, 0);
}
However, in my unit test Jasmine is reporting that .querySelector is not available because the result of the first querySelector is null in the test environment.
it('should render', function(){
var wrap, searchBarDirective, $scope;
$scope = $rootScope.$new();
searchBarDirective = $compile(angular.element(template))($scope);
$scope.$digest();
$timeout.flush();
wrap = searchBarDirective.find('md-autocomplete-wrap')[0];
expect(wrap).toBeDefined();
});
It's obvious to me that $document doesn't contain the rendered directive and thus the second querySelector fails. But why doesn't $document contain the directive?
I tried mocking querySelector with spyOn($document[0], "querySelector").and.returnValues($document[0],$document[0]) but that doesn't get me past the focus. Thinking I have lot my way here.
* Revised *
I think that it is important to continue to use $document but I decided to drop the querySelector for the jqLite find method.
function focusInput(){
$timeout(function(){
var input;
try {
// can throw an error if the first find fails
input = $document.find('md-autocomplete').find('input');
}
catch (e) {
angular.noop(e);
}
if(input && angular.isFunction(input.focus)) {
input.focus();
}
}, 0);
}
The test I changed per comments to below. I do have Karma load jquery to make testing easier which allows me to search for :focus
beforeEach(function(){
element = angular.element(template);
$document[0].body.appendChild(element[0]);
$scope = $rootScope.$new();
});
afterEach(function(){
element[0].remove();
});
it('should be focused', function(){
var input, searchBarDirective;
searchBarDirective = $compile(element)($scope);
$scope.$digest();
$timeout.flush();
input = searchBarDirective.find(':focus')[0];
expect(input).toBeDefined();
});
The reason why your querySelector call works in the browser, but not in tests is that you are creating a DOM element with angular.element, but you are never attaching it to the document. There are two ways to address this:
First, you could simply do this. Instead of:
searchBarDirective = $compile(angular.element(template))($scope);
Do:
let element; // declare this in the describe block so it is available later
element = angular.element(template);
document.body.appendChild(element[0]);
searchBarDirective = $compile(element)($scope);
And then do this:
afterEach(() => element[0].remove());
But, that's a bit messy. You should not be manipulating global scope (ie- the document) in your unit tests unless you have to. It would be better in your non-test code to avoid accessing the document and instead access a scope element, or some other DOM element that you can also mock in your tests. This will be a bit harder to do since it may require re-architecting your code a bit. In general though, in order to make modular and testable code, you want to avoid accessing the document object as much as possible.
I'm rather new to angular and I'm trying to integrate np-autocomplete in my application (https://github.com/ng-pros/np-autocomplete). However I can only get it to work when I'm passing a html string as a template inside the $scope.options and it doesn't work when I want to load it from a separate html.
the Code for my app looks as follows:
var eventsApp = angular.module('eventsApp',['ng-pros.directive.autocomplete'])
eventsApp.run(function($templateCache, $http) {
$http.get('test.html', {
cache: $templateCache
});
console.log($templateCache.get('test.html')) // --> returns undefined
setTimeout(function() {
console.log($templateCache.get('test.html')) // --> works fine
}, 1000);
//$templateCache.put('test.html', 'html string') //Would solve my issue in the controller,
//but I would rather prefer to load it from a separate html as I'm trying above
Inside my controller I am setting the options for autocomplete as follows:
controllers.createNewEventController = function ($scope) {
$scope.options = {
url: 'https://api.github.com/search/repositories',
delay: 300,
dataHolder: 'items',
searchParam: 'q',
itemTemplateUrl: 'test.html', // <-- Does not work
};
//other stuff...
}
however, it seems that test.html is undefined by the time np-autocomplete wants to use it (as it is also in first console.log above).
So my intuition tells me that the test.html is probably accessed in the controller before it is loaded in eventsApp.run(...). However I am not sure how to solve that?
Any help would be highly appreciated.
You are most likely correct in your assumption.
The call by $http is asynchronous, but the run block will not wait for it to finish. It will continue to execute and the execution will hit the controller etc before the template has been retrieved and cached.
One solution is to first retrieve all templates that you need then manually bootstrap your application.
Another way that should work is to defer the execution of the np-autocomplete directive until the template has been retrieved.
To prevent np-autocomplete from running too early you can use ng-if:
<div np-autocomplete="options" ng-if="viewModel.isReady"></div>
When the template has been retrieved you can fire an event:
$http.get('test.html', {
cache: $templateCache
}).success(function() {
$rootScope.$broadcast('templateIsReady');
});
In your controller listen for the event and react:
$scope.$on('templateIsReady', function () {
$scope.viewModel.isReady = true;
});
If you want you can stop listening immediately since the event should only fire once anyway:
var stopListening = $scope.$on('templateIsReady', function() {
$scope.viewModel.isReady = true;
stopListening();
});
I'm attempting to unit test some angular js controllers that I have written within the jasmine testing framework. I've got everything set up so that I am able to create instances of my controller, and pass in mock services.
However, I've got a few lines of code that run when the page loads.
$scope.tags = [];
$scope.noData = false;
$scope.generateSearchResults = function(input){
searchAPI.executeSearch(input).then(function(res){
$scope.tags = res.data;
});
};
//does some post processing on tags
$scope.checkNumberOfResults = function(){
if($scope.tags.length < 1){
$scope.noData = true;
}
}
//this is the code that runs when the page loads.. normally I want this behavior,
//but for my jasmine unit tests, I don't want the controller running any code on
//instantiation
$scope.$watch('$viewContentLoaded', function(){
$scope.searchQuery = $routeParams.query; //grabs from the url
$scope.generateSearchResults($scope.searchQuery){
.then(function(res){
$scope.checkNumberOfResults();
});
});
So if you can tell, when the page loads I want to grab a query string from the url and then display search results. The thing is, I don't want this code to be run while testing, at least not for my unit tests. Maybe when I do some integration tests I will want to be able to simulate a page load, but for now, I want to unit test some of my other functions in the controller without necessarily requiring a call to the search API service..
Does that make sense? Does anyone have any advice for where to go on this?
Instead of watching for the $viewContentLoaded event you could inject the searchQuery into your controller by using resolve of ngRoute or uiRouter. This allows you to mock the searchQuery in your tests and be independent of page loading and $routeParams
// code for ngRouter
...
controller: 'MyCtrl',
resolve: {
searchQuery: function ($route) {
// grabs search query from URL
return $route.current.params.query;
}
}
Then inject searchQuery into your controller:
module.controller('MyCtrl', function ($scope, searchQuery) {
$scope.$watch('searchQuery', function(newValue){
// ignore undefined etc.
if (!newValue) { return; }
$scope.generateSearchResults($scope.searchQuery){
.then(function(res){
$scope.checkNumberOfResults();
});
});
});
I'm running E2E tests against an AngularJS site, using Karma and angular-scenario.
I'm executing some login code in a beforeEach function before every it block.
My login function has a timeout delay in it to ensure that the login completes correctly. This is time-consuming and inefficient (not to mention inelegant). In addition, the user would only login once during a session, so this would more accurately model my scenario.
What I'm looking for is a before function that executes the login only once for a collection of it blocks contained within a describe block, but this facility doesn't seem to exist (I've checked the docs and the source code).
Seems like an obvious requirement for a testing library! Has anybody solved this problem?
could you use a variable flag? for example:
var bdone = false;
describe('Search POC', function() {
beforeEach(function() {
if (!bdone) {
browser().navigateTo('login');
console.log('navigated once');
bdone = true;
}
});
it ('should have an img link on the login results', function() {
expect(element('a:last').html()).toMatch(/jpg/);
});
it ('should redirect to user details when clicked', function() {
element('#UserThumbImage:first').click();
expect(browser().window().href()).toMatch(/user/);
});
});
I'm trying to access $scope's within an E2E test without success...
As a test I tried this: (My site does not use JQuery..)
The runner has my site in a nested iframe, so I'm accessing it directly, then getting all ng-scopes and trying .scope() on them as in this post and code below...
var frameDocument = document.getElementById('test-frames').children[0].contentDocument;
var scopeElements = frameDocument.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return angular.element(e).scope();
});
The above code finds the proper elements, but calling scope() on them returns undefined for each....
Can someone confirm or deny that we can access the scope in E2E? I'd assume there is a way?
Thank-you
Here is my trick based on previous answer.
You can extend it to dynamic scopes. The main part is getting the reference to appWindow from addFutureAction.
//HTML CODE
<body id="main-controller" ng-controller="mainCtrl" ng-init="__init__()">
//Scenario helper.
/*
Run `callback` with scope from `selector`.
*/
angular.scenario.dsl('scope', function() {
return function(selector, callback) {
return this.addFutureAction(
'Executing scope at ' + selector,
function(appWindow, $document, done) {
var body = appWindow.document.getElementById(selector)
var scope = appWindow.angular.element(body).scope()
callback(scope)
scope.$apply()
done(null, 'OK');
})
}
})
//Actual test.
it(
'When alerts are defined, they are displayed.',
function() {
scope('main-controller', function(scope) {
scope.alerts.push({'type': 'error', 'msg': 'Some error.'})
})
expect(element('#alerts').css('display')).toEqual('block')
})
In E2E tests, accessing scope that way is not good option. Instead You can use helper functions like element() to select elements in page, and use expect() to check model data.
What you might need is unit testing. You can access $scope in unit tests easily.
There is a very good guide here: http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-testacular.html
Also it might be a timing issue, i can reach scopes in testacular runner like this. It runs tests in iframe. To make it work you need to add sleep(3) to your test. But this is very fragile.
setTimeout(function () {
console.log('try to reach frames');
var frame = window.frames[0].window.frames['senario_frame'];
if (!frame) {
console.log('too late');
} else {
var scopeElements = frame.document.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return frame.angular.element(e).scope();
});
console.log(scopes);
}
}, 2000);