I need to test if specific methods are called when user scrolls the window to a certain point. In my source code I have windows listener attached, something like:
$(window).on("scroll.singleJob",function(e)
{
// code here, check if the window is scrolled past certain point etc. and then I need to call this method
LozengesPanel.makeFixed();
}
Now, in my Jasmine test I'm trying to confirm that the method is being called when the window is scrolled to a certain point. So I set up the test:
describe("SingleJob page", function() {
beforeEach(function() {
loadFixtures('my_fixture.html');
});
it("panel sticks to top when page scrolled down", function() {
spyOn(mycompany.singleJobTestable.LozengesPanel, "makeFixed");
window.scroll(0,1000);
expect(mycompany.singleJobTestable.LozengesPanel.makeFixed).toHaveBeenCalled();
});
});
But the test fails, all I get is Expected spy makeFixed to have been called.
How can I trigger window scroll so I can test methods inside of this callback?
EDIT:
Finally it all makes sense.. It seems that scroll event was put in a tasks queue only to be executed after the current thread finishes. Adding $(window).trigger("scroll"); did the trick. I posted short blog post about it that explains the issue http://spirytoos.blogspot.com.au/2014/02/testing-windowscroll-with-qunitjasmine.html
EDIT: This answer does not satisfy the question. See the comments for the reason.
Actually, it looks like you are triggering the scroll event from your Jasmine spec. I tried very similar code, which I include below. However, my expect still fails, like yours (I'm still getting familiar with Jasmine, so I can't explain with certainty why that is).
var fun = {
scrollEventCallback: function() {
console.log('scroll event triggered');
}
};
$(window).on('scroll', fun.scrollEventCallback);
describe("A test suite", function() {
it("should trigger f", function() {
spyOn(fun, "scrollEventCallback");
$(window).trigger('scroll'); // my callback function is executed because it logs to the console
expect(fun.scrollEventCallback).toHaveBeenCalled(); // this fails anyway
});
});
Maybe your window.scroll(0, 1000) is not actually pushing the viewport low enough to trigger your Lozenges.makeFixed() call. That would be the case if the page (your fixture, I think) wasn't long and it didn't actually have anywhere to scroll.
Also, I got code similar to your provided code to work. The expect(...) succeeds. It is pasted below.
var LozengesPanel = {
makeFixed: function() {
console.log('makeFixed was called with its original function definition');
}
};
$(window).on("scroll.singleJob",function(e) {
LozengesPanel.makeFixed();
});
describe("A test suite", function() {
it("should trigger callback", function() {
spyOn(LozengesPanel, "makeFixed");
$(window).trigger('scroll'); // nothing is logged to the console
expect(LozengesPanel.makeFixed).toHaveBeenCalled(); // succeeds
});
});
Related
I have a large project where one method attached to the scope has a console.log in it.
$scope.someFunctionAttachedToView = function() {
console.log("this method is being triggered");
return {
'width': '500px'
}
}
In this project, I have lots of listeners that listen for browser window size, and mouse clicks etc. and I noticed that sometimes, even if I don't think I'm making an action that would trigger a digest cycle, I see the console.log message above appearing in the browser console intermittently.
I am trying to track down what action could be possibly triggering the digest cycle to apply, as it is not intended behavior.
How would you track this? Is there something I can console.log in the Angular object that would tell me this?
If you're working with chrome/firefox you could put a debugger in your code like this:
$scope.someFunctionAttachedToView = function() {
debugger;
console.log("this method is being triggered");
return {
'width': '500px'
}
}
When this function triggers your javascript will be frozen and you can look in the stack trace which function called it. (In chrome you might have to enable async because digests often mess up the call stack)
I'm trying to get a javascript function to run only once. I've seen this question has been asked before, e.g. Function in javascript that can be called only once, but I can't get the solutions in here to work. I'm not sure if it's because I've got nested functions, or whether there's something I'm missing. Essentially, I'm trying to run a function which, when a webpage is scrolled, it:
- runs a little animation on a canvas in the header
- reduces the size of the header
- leaves it at that
But when there is any subsequent scrolling, the animation keeps re-running. Here's a summarised version of the non-working code:
$(document).on("scroll",function(){
var arrange_title = function(){
//some code
};
if($(document).scrollTop()>0){
arrange_title();
arrange_title = function(){};
setTimeout(function(){
$("header").removeClass("large").addClass("small");
},1000);
}
});
I've also tried declaring a global variable, setting it to "false" in a "window.onload" function, then set it to true in an if function that runs the animation (the if function running only if the variable is false), but that doesn't stop it either. Thoughts?
What you're looking for is something along the lines of listenToOnce where the listener fires the one time, but never again. This could be modified to a number of calls, but the logic is like so:
Register the listener.
Then once the listener fires, remove it.
See .off
$(document).on("scroll",function(){
var arrange_title = function(){
//some code
};
if($(document).scrollTop()>0){
arrange_title();
arrange_title = function(){};
setTimeout(function(){
$("header").removeClass("large").addClass("small");
// $(document).off('scroll'); // or here
},1000);
}
$(document).off('scroll'); // remove listener, you can place this in the setTimeout if you wish to make sure that the classes are added/removed
});
Don't use a time out. That is why you are getting in trouble. Declare a variable outside of your function using var, that will make it global. Your code should be inside of a check for that variable. Before executing your code the first time but inside of the check, change that variable so that the code will never run again.
Try avoid setTimeout. Almost all animation can be watched for end.
function doHeaderAnimation() {
return $('header').animate();
}
function makeHeaderSmall() {
$("header").removeClass("large").addClass("small");
}
function handleScroll(event) {
if ($(document).scrollTop() > 0) {
doHeaderAnimation().then(makeHeaderSmall);
$(document).off("scroll", handleScroll);
}
}
$(document).on("scroll", handleScroll);
I'm trying to do page automation with PhantomJS. My goal is to be able to go to a website, click an image, and continue with other code once the page has loaded from the click. To test this I'm trying to write a script that will go to the url of the quick start guide on the PhantomJS website and then click on the PhantomJS logo bringing the page to the PhantomJS homepage. Also to render a picture of the website before and after the click to make sure the click worked. This is my current code:
var page = require('webpage').create();
page.open('http://phantomjs.org/quick-start.html', function(status) {
console.log(status);
page.render('websiteBeforeClick.png');
console.log(page.frameUrl); //check url before click
var element = page.evaluate(function() {
return document.querySelector('img[alt="PhantomJS"]');
});
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl); //check url after click
}, 3000);
console.log('element is ' + element); //check that querySelector() is returning an element
page.render('websiteAfterClick.png');
phantom.exit();
});
Problem is my before and after pictures are the same. This is my output when I run it.
success
element is [object Object]
Im using their sendEvent method from here "http://phantomjs.org/api/webpage/method/send-event.html" but I'm not sure if its working.
Also why doesnt the console.log(page.frameUrl) in my window.setTimeout() get executed?
I was looking at their page automation examples on the PhantomJS website. Particularly this one "https://github.com/ariya/phantomjs/blob/master/examples/imagebin.js".
I noticed their examples used
document.querySelector('input[name=disclaimer_agree]').click()
But when I tried it with my code I got an error.
document.querySelector('img[alt="PhantomJS"]').click();
TypeError: 'undefined' is not a function
EDIT#1:
I changed the end section of my code to this:
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl);
page.render('websiteAfterClick.png');
phantom.exit();
}, 3000);
console.log('element is ' + element);
});
Now my after image is correct. But now my question is, If I want to continue on with my code i.e. click on another element on the site, will my new code have to be all nested inside of the timeout function?
There is an example function phantom.waitFor(callback) that I explain on the following post, it goes as follows:
phantom.waitFor = function(callback) {
do {
// Clear the event queue while waiting.
// This can be accomplished using page.sendEvent()
this.page.sendEvent('mousemove');
} while (!callback());
}
This can help streamline your code and avoid nested calls to window.setTimeout(), which are not very reliable anyway as you are waiting for a pre-set amount of time instead of waiting for the element to become visible. An example would be as follows:
// Step 1: Open and wait to finish loading
page.open('http://localhost/');
phantom.waitFor(function() {return !page.loading;});
// Step 2: Click on first panel and wait for it to show
page.evaluate(function() { $("#activate-panel1").click(); });
phantom.waitFor(function() {
return page.evaluate(function() {return $("#panel1").is(":visible");})
});
// Step 3: Click on second panel and wait for it to show
page.evaluate(function() { $("#activate-panel2").click(); });
phantom.waitFor(function() {
return page.evaluate(function() {return $("#panel2").is(":visible");})
});
console.log('READY!');
phantom.exit();
This will load each panel in succession (ie synchronously) while keeping your code simple and avoiding nested callbacks.
Hope it makes sense. You could also use CasperJS as an alternative, its aimed at making this stuff simpler.
Yes, your new code will be called from inside of the setTimeout callback. You can nest the code directly or write a function which capsules the code for you and call that function inside setTimeout.
function anotherClick(){
// something
}
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl);
page.render('websiteAfterClick.png');
anotherClick();
phantom.exit();
}, 3000);
There is another way. You can also write it completely with multiple setTimeout, but then you cannot react to sudden conditions in previous calls.
page.sendEvent('click', element.offsetLeft, element.offsetTop, 'left');
window.setTimeout(function () {
console.log(page.frameUrl);
page.render('websiteAfterClick.png');
}, 3000);
window.setTimeout(function () {
// some more actions
}, 6000); // you cannot know if this delay is sufficient
window.setTimeout(function () {
phantom.exit();
}, 9000); // you cannot know if this delay is sufficient
I would suggest using CasperJS, if you want to do many actions/navigation steps.
I'm using a solution explained in this answer to unit test events in my node application.
However, the setTimeout function never calls and so my tests pass when they should fail.
Here is an example:
suite('myTests', function() {
test('myFunction_whenCalled_emitsEvent', function() {
var myClass = new MyClass();
var eventTimeout = setTimeout(function() {
assert(false);
}, 1000);
myClass.on('something', function() {
clearTimeout(eventTimeout);
});
myClass.doSomething(); // this does not emit the 'something' event
});
});
I would expect this to fail, after 1 second as long as the 'something' event is not raised.
I put a breakpoint in the assert(false) line and it is never hit.
Could someone point me in the right direction ? Thanks.
You must use the done callback to show that your test is finished. Something like this:
suite('myTests', function() {
test('myFunction_whenCalled_emitsEvent', function(done) {
var myClass = new MyClass();
myClass.on('something', function() {
done();
});
myClass.doSomething();
});
});
It looks like you are only testing whether the event is emitted. If this is the case, then the whole setTimeout thing is unnecessary. Mocha will itself timeout if it does not get done without the default timeout (2000ms, if I recall correctly).
The way your code was set, Mocha would just schedule your event and then exit the test. Since scheduling the event was successful, Mocha would call the test successful.
I'm getting some odd behavior in a part of my js code.
I have some notifications which appear in a bar on top of the page and then disappear after a certain amount of time. I have used a simple setTimeout() to acheive this.
Sometimes, a notification will appear as a result of a particular url query string when the page loads but then a new one would need to be displayed when the user clicks on a button. I want the old one to disappear and the new one to appear. I'm using a variable to keep a reference to the setTimeout() in order to cancel it. However, when I try to do this I manage to create a loop that eventually crashes my chrome tab.
I have put together a jsfiddle illustrating my problem - http://jsfiddle.net/5Nm4c/
Clicking on show notification while another is visible will crash the browser tab. If you click on it when nothing is shown, it is fine.
Here is my js:
var Notification = {
// close main notification bar
close: function (callback) {
$('#notification-bar').fadeOut(250, function () {
// reset its position and fade it back in so it is ready to go again
$(this).css('top', -100).fadeIn(1);
// check if a callback function has been passed in
if (typeof callback === 'function') {
callback();
}
});
},
// open notification bar with the appropriate css class and message
open: function (message) {
// if the notification bar is already visisble
if (verge.inViewport($('#notification-bar'))) {
// hide and then show it with the new message
window.clearTimeout(Notification.timeout);
Notification.close(Notification.open(message));
return false;
}
$('#notification-bar').html(message);
$('#notification-bar').animate({
'top': 0
}, 250, function () {
Notification.timeout = window.setTimeout(function () { Notification.close() }, 1500);
});
},
timeout: null
}
Notification.open('hello');
$('#button').click(function(e){
e.preventDefault();
Notification.open('link clicked');
});
I'm using https://github.com/ryanve/verge/ as it has some nice methods to check if elements are visible in the viewport.
Could someone please tell me where my error is?
I think the error Uncaught RangeError: Maximum call stack size exceededcomes from jsfiddle itself, so I am not able to test it.
I see what you did there:
var Notification = {
open: function (message) {
Notification.close(Notification.open(message)); //Here you create the loop!!
}
}
Another problem I see in your code is, that when Notification.open is called while a animation is running Notification.timeout is not actuell. Try a $('#notification-bar').stop(true, true); to stop the actuell animation befor you call window.clearTimeout(Notification.timeout);. Maybe it would be even better to use $('#notification-bar').stop(true, false);, so the "old" setTimeout will not even be called.