I'm trying to test some "style set" functions, and I'm facing issues because I need to wait until the element if fully rendered, then read its property and check if it was sucesfully changed. I know that this is pretty obvious, but I was wondering if there is a way to do the same but without waiting.
For example, I want to test this function:
function changeWidth() {
document.getElementById('test').style.width = '200px';
}
And I use this test in Mocha:
it('Width should be 200px', () => {
changeWidth();
assert.equal(document.getElementById('test').style.width, '200px');
});
That assertion will always returns false. The following test will work:
it('Width should be 200px', () => {
changeWidth();
window.setTimeout( () => {
assert.equal(document.getElementById('test').style.width, '200px');
}, 1000);
});
It has to be a better way to accomplish the same without using timeouts. Can someone guide me? Thanks!
There are two things I noted:
No, you cannot force a render to happen synchronously. The browser decides, so the test will be async and a bit messy. That being said, there are (a bit) more elegant answers than setTimeout.
Your test is synchronous, while the logic is asynchronous. That means your test will always pass, as the assertion is called after the test has finished (see this). You need to pass a callback to the test that can be called when the test has finished.
You could do the tests somewhat cleaner by using requestAnimationFrame. Just create a helper that will run each function passed to it in a seperate animation frame and you will be guaranteed separate render phases.
function runSteps(fns) {
if(!fns.length) return;
var current = fns[0];
var rest = fns.slice(1);
requestAnimationFrame(function() {
current();
runSteps(rest);
})
}
// note the `done` argument - it is what makes Mocha able to know
// when the test is finished. See https://stackoverflow.com/questions/20748918/cannot-run-mocha-js-in-synchronous-mode
it('should run dom render steps chronologically', function(done) {
function assertWidthChanged(){
assert.equal(
document.getElementById('test').style.width,
'200px'
);
}
runSteps([
changeWidth,
assertWidthChanged,
done
]);
}
Related
I have an asynchronous function running in my web application that enables a chat input. In a different component I need to set a variable to the input and then focus() on it when certain conditionals are met. Unfortunately the chat input DOM element isn't always available when I try to declare it based on the asynchronous nature of the function that enables it. Being familiar with how setTimeoout() works with the call stack I wrapped my declaration in a setTimeout and everything (seemingly) works as expected now.
So my question is if this is a good practice or not? I'm using React/Redux and will have to do a lot of prop threading and extra logic to get a seemingly easy task accomplished without the setTimeout.
It is an alright practice ;)
It gets the job done, but it is usually preferable to work with callbacks or promises instead of polling to see if the dom is ready. The main failing with a "setTimeout" approach is that you are setting a timer and what if the resource (chat plugin) takes longer to load than the timer you set.
// Run it
main();
// Supporting code
function main() {
let attempts = 0;
const maxAttempts = 10;
tryUpdate();
function tryUpdate() {
// Call it once
attempts++;
const success = updateAndFocus();
console.log(attempts);
// Keep calling it every 100ms
if (!success && attempts < maxAttempts) {
setTimeout(() => tryUpdate(), 100);
}
}
}
function updateAndFocus() {
const el = document.getElementById('findme');
if (!el) return false;
// do work
el.focus;
return true;
}
I'm struggling a bit to find a solution on how to test this exported function with jest.
export const scrollToError = () => {
setTimeout(() => {
const hasErrorElement = jQuery('.has-error');
if (!hasErrorElement.length) return;
jQuery('html,body').animate({
scrollTop: hasErrorElement.offset().top - 50,
}, 'slow');
}, 400);
};
I imported it in my test file and tried to start it:
import { scrollToError } from './utils';
describe('Utils', () => {
it('should scroll to error', () => {
const result = scrollToError();
expect(result).toBe(true); //added this just to force an error and got result as undefined
});
});
Could anyone give me any tips on how to test code with these dependencies?
scrollToError() is asynchronous function and you can't invoke it and expect the result to be there immediately. You need to wait that amount of ms (400 in your case), before testing for it.
Asynchronous code is tested a bit differently in Jest: Testing Asynchronous Code. You can also take control over the timers or combine it all with the manual mocks and override jQuery itself.
How are you using jQuery?
I mean, did you get it using npm or yarn? to mock node_modules you can follow this link: https://jestjs.io/docs/en/manual-mocks#mocking-node-modules
Otherwise, you will have to create a manual mock. You can see how to do it here: https://jestjs.io/docs/en/manual-mocks
Updated:
the simplest way is to override it, is while settting up your test at beforeXXX method.
You can simply put something like window.JQuery = jest.fn();
this is the simplest mock ever but you will have to create the methods like animate and other jquery related methods.
Having second thoughts here and looking to your function, if you mock jQuery what else left to be tested?
If you mock, you will be testing if your fn are doing the steps you defined here. Like check if the jQuery fn was called with .has-error class or if animate received the correct parameters.
This kind of test doesn't help you at all, it's just checking if it's following line by line your algorithm. The problem here, that you could do some refactorings like changing the .has-error class name or the animate method by other improved one.
What you really need to change, if it's doing at the end what should be doing. Displaying the div or whatever that should be displayed. If you test that, regardless the way you refactor your code the test will check if the final solution still works and that what matters.
Was I clear? English is not my first language so, it may be a little bit confusing
I finally managed to find a proper solution.
I wrote three test cases for it:
jest.useFakeTimers();
describe('utils', () => {
afterEach(() => {
document.body.innerHTML = '';
});
it('ScrollToError - should run the settimeout for 400 ms', () => {
scrollToError();
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 400);
});
it('ScrollToError - should scroll to error', () => {
document.body.innerHTML = formStep1ErrorMock;
window.setTimeout = fn => fn();
const result = scrollToError();
expect(result).toBe(true);
});
it('ScrollToError - should do nothing as has no errors', () => {
document.body.innerHTML = formStep1Mock;
window.setTimeout = fn => fn();
const result = scrollToError();
expect(result).toBe(true);
});
});
So basically, first I check if the setTimeout was called with the proper amount of seconds (not that it's important).
Then I mock the setTimeout by doing this window.setTimeout = fn => fn(); so it runs without waiting for the delay. I also mock the html with the proper details I need it to have.
And finally, I just cover another scenario.
PS: I added a return true statement to the scrollToError method to make it simpler to have an expected result.
This way I achieved 100% coverage for this method.
I have a following function:
function prompt_mandatory_field_completion(){
$("#mandatory_fail").show(150, function() {
setTimeout(function() {
$("#mandatory_fail").fadeOut(500)
}, 2000);
});
window.scrollTo(0,0)
}
That I would like to test with jasmine but regardless to what I put in my spec file the test seems to pass.
The spec file contains the following code :
it(' NEED TO FIX THAT FADE OUT Should prompt user to fill in mandatory questions via prompt_mandatory_field_completion function', function() {
prompt_mandatory_field_completion();
setTimeout(2000, function(){
expect($('#mandatory_fail').css('display').toEqual('random thing'));
});
In my SpecRunner.html I am using the following function that I run in before each test in this description block:
function setupFixtures(){
setFixtures('<div id="mandatory_fail" style="display:none;"></div>');
prompt_mandatory_field_completion();
};
Any idea how to make this into a meaningful test? I guess I have been staring at it way too long and poking it from all the directions.
Best,
Adam
You're trying to write a functional test of asynchronous behavior. You might have a lot better experience trying to use protractor for this sort of test. It's tuned more toward asserting things that will eventually be true.
However, jasmine does have an asynchronous facility since about 2.0, known as done(), that will insist that all of the asynchronous code has run before the test passes or fails.
You have to pass the done function to get asynchronous tests :
it(' NEED TO FIX THAT FADE OUT Should prompt user to fill in mandatory questions via prompt_mandatory_field_completion function', function(done) {
prompt_mandatory_field_completion();
setTimeout(2000, function(){
expect($('#mandatory_fail').css('display').toEqual('random thing'));
done();
});
}, 3000);
You can also pass a timeout as a last parameter, depending on what you've set in your jasmine's settings.
Otherwise, Jasmine will consider this test to fail if its execution exceed its timeout.
Pretty new to meteor, velocity and jasmine so not sure if I am doing something wrong, using Jasmine for something it's not designed for, or this is just the way it works.
I am finding I need to set timeouts for pretty much all of my tests in order to get them to pass. Should this be the case or am I doing something incorrectly?
For example some tests I am running to check validation messages:
describe("add quote validation", function() {
beforeEach(function (done) {
Router.go('addQuote');
Tracker.afterFlush(function(){
done();
});
});
beforeEach(waitForRouter);
it("should show validation when Quote is missing", function(done) {
$('#quote').val('');
$('#author').val('Some author');
Meteor.setTimeout(function(){
$('#addQuoteBtn').click();
}, 500);
Meteor.setTimeout(function(){
expect($('.parsley-custom-error-message').text()).toEqual("Quote can't be empty.");
done();
}, 500);
});
}
OK, we've had this exact same problem and devised a pretty elegant solution to it, that doesn't require timeouts and is the fastest way to run your tests. Basically, we use one of two strategies, depending on what screen elements you're waiting for.
All code goes into tests/mocha/client/lib.coffee, not 100% what the Jasmine equivalent is, but it should be available to all client test code. I've left it in Coffeescript, but you can compile it on coffeescript.org into Javascript, it should work fine as well.
If whatever you do (routing or something else like changing a reactive variable) causes a Template to (re)render, you can use the Template.<your_template>.rendered hook to detect when it is finished rendering. So, we've added the following function in lib.coffee:
#afterRendered = (template,f)->
cb = template.rendered
template.rendered = ->
cb?()
template.rendered = cb
f?()
return
return
What does it do? It basically "remembers" the original rendered callback and temporarily replaces it with one that calls an extra function after the template is rendered and the original callback is called. It needs to do this sort of housekeeping to avoid breaking any code that may have depended on the rendered callback, as you're basically messing with the Meteor code directly.
In your test, you can then do something like this:
it.only "should check stuff after routing", (done)->
try
Router.go "<somewhere>"
afterRendered Template.<expected_template>, ->
<your tests here>
done()
catch e
done(e)
I'd recommend the try-catch as well, as I've noticed asynchronous errors don't always make it into the velocity system, merely giving you a timeout failure.
OK, then there are things that don't actually re-render, but are generated with JS or by some kind of "show/hide" mechanism. For that, you do need some kind of timeout, but you can reduce the "time cost" of the timeout by using a polling mechanism.
# evaluates if a JQuery element is visible or not
$.fn.visible = -> this.length > 0 and this.css('display') isnt 'none'
# This superduper JQuery helper function will trigger a function when an element becomes visible (display != none). If the element is already visible, it triggers immediately.
$.fn.onVisible = (fn,it)->
sel = this.selector
if this.visible()
console.log "Found immediately"
fn?(this)
else
counter = 0
timer = setInterval ->
counter++
el = $(sel)
if el.visible()
fn?(el)
clearInterval timer
console.log "Found on iteration #{counter}"
else
it?(el)
, 50
You can remove the console logging and secondary it iterator function if you like, they're not important. This allows you to do something like this in your test:
$('#modalId').onVisible (el)->
<tests here>
done()
, (el)->
console.log "Waiting for #{el.selector}"
You can remove the second function if you want, it is the it iterator function mentioned above. However, do note that this particular code works with "display: hidden" as the marker for invisibility (Bootstrap does this). Change it if your code uses another mechanism to hide/show parts.
Works like a charm for us!
I'm having a problem with QUnit stop() basically it doesn't seem to be working at all. Here's an example of something I'm trying to do: http://jsfiddle.net/7hZMM/1/
If you remove the line:
testIframe.remove();
It works fine and runs the test and passes.
I presumed that calling stop() would halt test execution (as the docs say it should) but that doesn't seem to be the case as the remove() is being called before the load callback is completed.
What can I do to make sure the test is run before the iframe is removed from the page?
I don't want to put the iframe remove in a callback as I have a few instances of this and will cause callback hell if I have to make each equal run off each other.
This is not clear from the docs, but stop() just ensures that teardown for the test will not be started until start() has been called. It does not in any way postpone the statements in the current test case following the call to stop().
The correct answer to your question is the one provided by jhorback; put the removal in the teardown.
You could try putting the removal in the teardown of the module. This would wait until the start() method is called and the text execution is complete.
QUnit.module("new test", {
setup: function () {
var testIframe = $('<iframe id="testiframe" src="/" />').appendTo("body");
testIframe.load(function() {
console.log('done loading iframe');
});
},
teardown: function () {
testIframe.remove();
}
});
QUnit.stop only tells QUnit to wait so many miliseconds before checking the results. Also, the iframe load event may not be triggered if the iframe does not have a valid src.