I have a few spots where things happen in the UI on a delay using $timeout or $interval. Here's a simplified example:
Controller code:
$timeout(function() {
$scope.showElement = true;
}, 10000);
HTML:
<div id="myElement" ng-show="showElement"></div>
I want to be able to create an end-to-end Protractor test that tests whether #myElement gets displayed after a 10 second wait. The only way I have found to do this is to call browser.sleep(10000), which results in an actual 10-second delay in my test. This works, but these pauses add up add up and significantly increase the duration of my tests. Imagine a situation where you wanted to test whether a modal pops up after 30 minutes of inactivity.
Is there a way to simulate the passage of a specific amount of time, similar to $timeout.flush() in a jasmine test?
You can decorate $timeout and $interval to override the delay supplied to them:
lower-wait-time.js
exports.module = function() {
angular.module('lowerWaitTimeDecorator', [])
.config(function($provide) {
$provide.decorator('$timeout', function($delegate) {
return function() {
// The second argument is the delay in ms
arguments[1] = arguments[1] / 10;
return $delegate.apply(this, arguments);
};
});
})
};
Usage
beforeAll(function() {
var lowerWaitTime = require('lower-wait-time');
browser.addMockModule('lowerWaitTimeDecorator', lowerWaitTime.module);
});
afterAll(function() {
browser.removeMockModule('lowerWaitTimeDecorator');
});
it('My-sped-up-test', function() {
});
You could do this potentially using async.whilst. The idea is keep on looking for the element until the timeout is reached. If the element is found BEFORE timeout reaches or if element is NOT found within the timeout, test fails otherwise it passes. I haven't tested this but you get the idea. For example,
var driver = browser.driver,
wd = browser.wd,
async = require('async'),
start = Date.now(),
found = false,
diff;
async.whilst(
function() {
var diff = Date.now() - start;
return diff <= 10000 && !found;
},
function(callback) {
driver.findElement(wd.By.id('myElement')).then(function() {
found = true;
callback();
},function(err) {
found = false;
callback();
});
},
function (err) {
var isTesrPassed = !err && found && diff>=10000;
assertTrue(isTestPassed, 'element visibility test failed');
}
);
Related
I've done extensive research on this subject, but no matter what I do, I find it extremely difficult to achieve this objective.
I want to execute code when all elements have been fully rendered in AngularJS web application. I think I found solution suggesting to use routers and views, but I could not make that work on my case, as it seems it requires certain configuration.
When you have ng-repeat and a lot of nested directives that will generate HTML/Content based on various conditions using ng-if, I noticed that HTML rendering continues even after document ready event is fired or view content have been loaded ie $viewContentLoaded event is triggered.
The closest idea I have is to use $watch over the length of the children of the element of a given directive. Every time the $watch is executed, increment counter renderCount. Then, in another timer event, check if the counter renderCount didn't change over the past say 3-5 seconds, then we can make an assumption that rendering is done.
The code to watch for the children, and check if no more rendering is taking place, could be as follows:
app.directive('whenRenderingDone', function($interval, $parse){
return {
link: function (scope, el, attrs) {
var renderingCount = 0;
function watchForChildren() {
scope.$watch(function(){
return $(':input', el).length;
}, function(newVal, oldVal){
if (newVal) {
renderingCount++;
}
})
}
watchForChildren();
//Check counter every 3 seconds, if no change since last time, this means rendering is done.
var checkRenderingDone = $interval(function(){
var lastCount = lastCount || -1;
if (lastCount === renderingCount) {
var func = $parse(attrs.whenRenderingDone);
$interval.cancel(checkRenderingDone);
func(scope);
}
lastCount = renderingCount || -1;
}, 3000);
}
}
});
I will try to implement the above approach, and if you have feedback please let me know.
Tarek
I developed the following directive which is working well under Chrome and IE11:
app.directive('whenRenderingDone', function($timeout, $parse){
return {
link: function (scope, el, attrs) {
var lastCount;
var lastTimer = 5000; // Initial timeout
//Check counter every few seconds, if no change since last time, this means rendering is done.
var checkRenderingDone = function (){
var mainPromiseResolved = scope.mainPromiseResolved;
lastCount = lastCount || -1;
if (lastCount === el.find('*').length && mainPromiseResolved) {
console.log('Rendering done, lastCount = %i', lastCount);
var func = $parse(attrs.whenRenderingDone);
func(scope);
} else {
lastCount = el.find('*').length;
console.log('mainPromiseResolved = %s, lastCount %i', mainPromiseResolved, lastCount)
console.log('Rendering not yet done. Check again after %i seconds.', lastTimer/1000.00);
stopCheckRendering = $timeout(checkRenderingDone, lastTimer);
lastTimer = lastTimer - 1000;
if (lastTimer <= 0) {
lastTimer = 1000;
}
return stopCheckRendering;
}
}
var stopCheckRendering;
stopCheckRendering = checkRenderingDone();
el.on('$destroy', function() {
if (stopCheckRendering) {
$timeout.cancel(stopCheckRendering);
}
});
}
}
});
I hope this will be of help to you, and if you have any comment to improve, please let me know. See this to give you an idea about how it is working.
Tarek
You can use $$postDigest to run code after the digest cycle completes. You can read more about the scope lifecycle here
// Some $apply action here or simply entering the digest cycle
scope.$apply(function () { ... });
...
scope.$$postDigest(function () {
// Run any code in here that will run after all the watches complete
// in the digest cycle. Which means it runs once after all the
// watches manipulate the DOM and before the browser renders
});
It seem $watch not work with cookies expires. Here is the code I tried
$scope.$watch(function(){
return $cookies.get('cookies')
},function(newVal,oldVal){
if(!newVal){
console.log('ok')
}
})
I can check it with $interval. But I dont want call digest cycle every 2s , or 5s.
Unfortunately, it is not possible to read the cookie expiration time in JS so my first idea of iterating over them and executing action on their expiration dates is not possible.
If you don't want to start a digest on every "cookie check", you can disable invoking it with the fourth parameter for the $interval set to false. See the $interval docs.
Example code:
.run(function($cookies, $interval) {
var previouslyAvailableCookies = Object.keys($cookies.getAll());
var watchCookieChanges = function() {
var availableCookies = Object.keys($cookies.getAll());
var expiredCookies = previouslyAvailableCookies.filter(function(cookieName) { return availableCookies.indexOf(cookieName) < 0; });
var newCookies = availableCookies.filter(function(cookieName) { return previouslyAvailableCookies.indexOf(cookieName) < 0; });
if (expiredCookies.length || newCookies.length) {
document.writeln("<p>New cookies: " + newCookies
+ "<br>Expired cookies: " + expiredCookies + '</p>');
}
previouslyAvailableCookies = availableCookies;
};
$interval(watchCookieChanges, 3000, 0, false);
})
And a codepen.
Before asking my real question, I have a different one... Does it make sense to unit test DOM manipulation in Angular directives?
For instance, here's my complete linking function:
function linkFn(scope, element) {
var ribbon = element[0];
var nav = ribbon.children[0];
scope.ctrl.ribbonItemClick = function (index) {
var itemOffsetLeft;
var itemOffsetRight;
var item;
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
itemOffsetLeft = item.offsetLeft - ribbon.offsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if (itemOffsetLeft < nav.scrollLeft) {
nav.scrollLeft = itemOffsetLeft - MAGIC_PADDING;
}
if(itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
nav.scrollLeft = itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
}
this.itemClick({
item: scope.ctrl.model.items[index],
index: index
});
$location.path(scope.ctrl.model.items[index].href);
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
nav.scrollLeft = item.offsetLeft - ribbon.offsetLeft - MAGIC_PADDING;
});
}
This is for a scrollable tabbed component and I have no idea how to test the 3 instances of nav.scrollLeft = x.
The first two if statements happen when an item - which is only partially visible - is clicked. The left/right (each if) item will be snapped to the left/right border of the component.
The third one, is to place the selected item in view if it's not visible when the component is loaded.
How do I unit test this with Karma/Jasmine. does it even make sense to do it or should I do functional tests with Protractor instead?
When testing directives, look for things that set or return explicit values. These are generally easy to assert and it makes sense to unit test them with Jasmine and Karma.
Take a look at Angular's tests for ng-src. Here, they test that the directive works by asserting that the src attribute on the element gets set to the right values. It is explicit: either the src attribute has a specific value or it does not.
it('should not result empty string in img src', inject(function($rootScope, $compile) {
$rootScope.image = {};
element = $compile('<img ng-src="{{image.url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('src')).not.toBe('');
expect(element.attr('src')).toBe(undefined);
}));
The same with ng-bind. Here, they pass a string of HTML with to the $compiler and then assert that the return value has had its HTML populated with actual scope values. Again, it is explicit.
it('should set text', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind="a"></div>')($rootScope);
expect(element.text()).toEqual('');
$rootScope.a = 'misko';
$rootScope.$digest();
expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual('misko');
}));
When you get into more complicated scenarios like testing against viewport visibility or testing whether specific elements are positioned in the right places on the page, you could try to test that CSS and style attributes get set properly, but that gets fiddly real quick and is not recommended. At this point you should be looking at Protractor or a similar e2e testing tool.
I would 100% want to test all the paths of your directive even if it isn't the easiest thing. But there are approaches you can take to make this process simpler.
Break Complicated Logic into Service
The first thing that stands out to me is the complicated piece of logic about setting the nav scrollLeft. Why not break this into a separate service that can be unit tested on its own?
app.factory('AutoNavScroller', function() {
var MAGIC_PADDING;
MAGIC_PADDING = 25;
return function(extraOffsetLeft) {
this.getScrollPosition = function(item, nav) {
var itemOffsetLeft, itemOffsetRight;
itemOffsetLeft = item.offsetLeft - extraOffsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if ( !!nav && itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
return itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
} else {
return itemOffsetLeft - MAGIC_PADDING;
}
};
}
});
This makes it much easier to test all the paths and refactor (which you can see I was able to do above. The tests can be seen below:
describe('AutoNavScroller', function() {
var AutoNavScroller;
beforeEach(module('app'));
beforeEach(inject(function(_AutoNavScroller_) {
AutoNavScroller = _AutoNavScroller_;
}));
describe('#getScrollPosition', function() {
var scroller, item;
function getScrollPosition(nav) {
return scroller.getScrollPosition(item, nav);
}
beforeEach(function() {
scroller = new AutoNavScroller(50);
item = {
offsetLeft: 100
};
})
describe('with setting initial position', function() {
it('gets the initial scroll position', function() {
expect(getScrollPosition()).toEqual(25);
});
});
describe('with item offset left of the nav scroll left', function() {
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 100
})).toEqual(25);
});
});
describe('with item offset right of the nav width and scroll left', function() {
beforeEach(function() {
item.clientWidth = 300;
});
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 25,
clientWidth: 50
})).toEqual(325);
});
});
});
});
Test That Directive is Calling Service
Now that we've broken up our directive, we can just inject the service and make sure it is called correctly.
app.directive('ribbonNav', function(AutoNavScroller, $timeout) {
return {
link: function(scope, element) {
var navScroller;
var ribbon = element[0];
var nav = ribbon.children[0];
// Assuming ribbon offsetLeft remains the same
navScroller = new AutoNavScroller(ribbon.offsetLeft);
scope.ctrl.ribbonItemClick = function (index) {
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
nav.scrollLeft = navScroller.getScrollLeft(item, nav);
// ...rest of directive
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
// Sets initial nav scroll left
nav.scrollLeft = navScroller.getScrollLeft(item);
});
}
}
});
The easiest way to make sure our directive keeps using the service, is to just spy on the methods that it will call and make sure they are receiving the correct parameters:
describe('ribbonNav', function() {
var $compile, $el, $scope, AutoNavScroller;
function createRibbonNav() {
$el = $compile($el)($scope);
angular.element(document)
$scope.$digest();
document.body.appendChild($el[0]);
}
beforeEach(module('app'));
beforeEach(module(function ($provide) {
AutoNavScroller = jasmine.createSpy();
AutoNavScroller.prototype.getScrollLeft = function(item, nav) {
return !nav ? 50 : 100;
};
spyOn(AutoNavScroller.prototype, 'getScrollLeft').and.callThrough();
$provide.provider('AutoNavScroller', function () {
this.$get = function () {
return AutoNavScroller;
}
});
}));
beforeEach(inject(function(_$compile_, $rootScope) {
$compile = _$compile_;
$el = "<div id='ribbon_nav' ribbon-nav><div style='width:50px;overflow:scroll;float:left;'><div class='item selected' style='height:100px;width:200px;float:left;'>An Item</div><div class='item' style='height:100px;width:200px;float:left;'>An Item</div></div></div>";
$scope = $rootScope.$new()
$scope.ctrl = {
model: {
selectedIndex: 0
}
};
createRibbonNav();
}));
afterEach(function() {
document.getElementById('ribbon_nav').remove();
});
describe('on link', function() {
it('calls AutoNavScroller with selected item', inject(function($timeout) {
expect(AutoNavScroller).toHaveBeenCalledWith(0);
}));
it('calls AutoNavScroller with selected item', inject(function($timeout) {
$timeout.flush();
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[0]);
}));
it('sets the initial nav scrollLeft', inject(function($timeout) {
$timeout.flush();
expect($el[0].children[0].scrollLeft).toEqual(50);
}));
});
describe('ribbonItemClick', function() {
beforeEach(function() {
$scope.ctrl.ribbonItemClick(1);
});
it('calls AutoNavScroller with item', inject(function($timeout) {
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[1], $el[0].children[0]);
}));
it('sets the nav scrollLeft', function() {
expect($el[0].children[0].scrollLeft).toEqual(100);
});
});
});
Now, obviously these specs can be refactored a 100 ways but you can see that a higher coverage is much easier to achieve once we started breaking out the complicated logic. There are some risks around mocking objects too much because it can make your tests brittle but I believe the tradeoff is worth it here. Plus I can definitely see that AutoNavScroller being generalized and reused elsewhere. That would not have been possible if the code existed in the directive before.
Conclusion
Anyways, the reason why I believe Angular is great is the ability to test these directives and how they interact with the DOM. These jasmine specs can be run in any browser and will quickly surface inconsistencies or regressions.
Also, here is a plunkr so you can see all the moving pieces and experiment: http://plnkr.co/edit/wvj4TmmJtxTG0KW7v9rn?p=preview
i am trying to make an animation on an html table.
i use $interval to display each row one by one.
var loadList = function() {
Obj.query(function(obj){
$scope.objs = [];
$interval(function() {$scope.objs.push(obj.shift())}, 200, obj.length);
});
}
then there is a function to remove each row one by one,
and finally it's looping and reload the table again :
var cleanList = function() {
var delay = 200;
var n = $scope.objs.length;
if (n!==0) {
$interval(function() {$scope.objs.shift()}, delay, n);
}
$interval(function() { loadList() }, delay*n, 1);
}
loadList();
$interval(cleanList, 7000);
The code is working here (here is the plunker), but i guess there is a way to do something nicer with a kind of "callback" or "promise" to trigger when the cleanList function is completed ?
How can i do that ?
$interval returns a promise so you can simply call then() on it.
For example:
$interval(function() {$scope.objs.push(obj.shift())}, 200, obj.length).then(cleanList);
Here's a working plunkr example:
http://plnkr.co/edit/rYzXjM?p=preview
I am in a spot where I need to poll my server for data every so often. I have looked around at how people are handling this in angularjs and I am pretty confused.
Some examples are of just simple counters that increment up/down. Other are using the $timeout service. I need the ability to turn this on/off with a button click. I.E. click to start poll, poll every 30 seconds, click button to stop polling.
I am not claiming to be great at javascript nor angular so please go easy. I did write my own service that uses setInterval and clearInterval:
angular.module('myModule', [])
.factory('TimerService',
function () {
var timers = {};
var startTimer = function(name, interval, callback) {
// Stop the timer if its already running, no-op if not running
stopTimer(name);
timers[name] = setInterval(function() {
callback();
}, interval);
// Fire right away, interval will fire again in specified interval
callback();
}
var stopTimer = function(name) {
var timer = timers[name];
if (timer) {
clearInterval(timer);
delete timers[name];
}
}
return {
start: startTimer,
stop: stopTimer
};
});
Then in my controller I do this:
var timerARunning = false;
$scope.onClickA = function() {
var timerName = 'timerA';
timerARunning = !timerARunning;
if (timerARunning) {
TimerService.start(timerName, 5000, function() {
alert("Timer A just fired");
});
} else {
TimerService.stop(timerName);
}
}