How to test a function which has a setTimeout with jasmine?

JavascriptJqueryUnit TestingSettimeoutJasmine

Javascript Problem Overview


I need to write a test for a function that has a setTimeout() call inside, but i can't find how i should do.

This is the function

// Disables all submit buttons after a submit button is pressed.
var block_all_submit_and_ajax = function( el ) {
	// Clone the clicked button, we need to know what button has been clicked so that we can react accordingly
	var $clone = $( el ).clone();
	// Change the type to hidden
	$clone.attr( 'type', 'hidden' );
	// Put the hidden button in the DOM
	$( el ).after( $clone );
	// Disable all submit button. I use setTimeout otherwise this doesn't work in chrome.
	setTimeout(function() {
		 $( '#facebook input[type=submit]' ).prop( 'disabled', true );
	 }, 10);
	// unbind all click handler from ajax
	$( '#facebook a.btn' ).unbind( "click" );
	// Disable all AJAX buttons.
	$( '#facebook a.btn' ).click( function( e ) {
		e.preventDefault();
		e.stopImmediatePropagation();
	} );
};

And this is my test

it( "Disable all submit buttons", function() {
	// Get a button
	var $button = $( '#ai1ec_subscribe_users' );
	// Call the function
	utility_functions.block_all_submit_and_ajax( $button.get(0) );
	// check that all submit are disabled
	$( '#facebook input[type=submit]' ).each( function( i, el ) {
		console.log( 'f' );
		expect( el ).toHaveProp( 'disabled', true );
	} );
} );

I've tried using jasmine.Clock.useMock(); and jasmine.Clock.tick(11); but i couldn't get things to work, the test never pass

Javascript Solutions


Solution 1 - Javascript

The overall approach varies based on your Jasmine version.

Jasmine 1.3

You can use waitsFor:

it( "Disable all submit buttons", function() {
    // Get a button
    var $button = $( '#ai1ec_subscribe_users' );
    // Call the function
    utility_functions.block_all_submit_and_ajax( $button.get(0) );

    // Wait 100ms for all elements to be disabled.
    waitsFor('button to be disabled', function(){
        var found = true;
        // check that all submit are disabled
        $( '#facebook input[type=submit]' ).each( function( i, el ) {
            if (!el.prop('disabled')) found = false;
        });
        return found;
    }, 100);
});

You could also use waits if you know exactly how long it will take:

it( "Disable all submit buttons", function() {
    // Get a button
    var $button = $( '#ai1ec_subscribe_users' );
    // Call the function
    utility_functions.block_all_submit_and_ajax( $button.get(0) );

    // Wait 20ms before running 'runs' section.
    waits(20);

    runs(function(){
        // check that all submit are disabled
        $( '#facebook input[type=submit]' ).each( function( i, el ) {
            expect( el ).toHaveProp( 'disabled', true );
        });
    });
});

There is also a third way of doing this, without the need for waits, waitsFor, and runs.

it( "Disable all submit buttons", function() {
    jasmine.Clock.useMock();

    // Get a button
    var $button = $( '#ai1ec_subscribe_users' );
    // Call the function
    utility_functions.block_all_submit_and_ajax( $button.get(0) );

    jasmine.Clock.tick(10);
  
    // check that all submit are disabled
    $( '#facebook input[type=submit]' ).each( function( i, el ) {
        expect( el ).toHaveProp( 'disabled', true );
    });
});
Jasmine 2.0

You can use done, the test callback:

it( "Disable all submit buttons", function(done) {
    // Get a button
    var $button = $( '#ai1ec_subscribe_users' );

    utility_functions.block_all_submit_and_ajax( $button.get(0) );

    setTimeout(function(){
        // check that all submit are disabled
        $( '#facebook input[type=submit]' ).each( function( i, el ) {
            expect( el ).toHaveProp( 'disabled', true );
        });

        // Let Jasmine know the test is done.
        done();
    }, 20);
});

you can mock out the timer behavior:

it( "Disable all submit buttons", function() {
    jasmine.clock().install();

    // Get a button
    var $button = $( '#ai1ec_subscribe_users' );
    // Call the function
    utility_functions.block_all_submit_and_ajax( $button.get(0) );

    jasmine.clock().tick(10);
  
    // check that all submit are disabled
    $( '#facebook input[type=submit]' ).each( function( i, el ) {
        expect( el ).toHaveProp( 'disabled', true );
    });
    
    jasmine.clock().uninstall()
});

Solution 2 - Javascript

Since Jasmine 2 the syntax has changed: http://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support

You now can simply pass a done callback to beforeEach, it, and afterEach:

it('tests something async', function(done) {
    setTimeout(function() {
        expect(somethingSlow).toBe(true);
        done();
    }, 400);
});

Update: Since writing this it's now also possible to use async/await which would be my preferred approach.

Solution 3 - Javascript

For anyone googling this, a better answer can be found timer testing

import { fakeAsync, tick, discardPeriodicTasks } from '@angular/core/testing';

it('polls statusStore.refreshStatus on an interval', fakeAsync(() => {
  spyOn(mockStatusStore, 'refreshStatus').and.callThrough();
  component.ngOnInit();
  expect(mockStatusStore.refreshStatus).not.toHaveBeenCalled();
  tick(3001);
  expect(mockStatusStore.refreshStatus).toHaveBeenCalled();
  tick(3001);
  expect(mockStatusStore.refreshStatus).toHaveBeenCalledTimes(2);
  discardPeriodicTasks();
 }));

Solution 4 - Javascript

I've never done any testing with jasmine, but I think I understand your problem. I would restructure the code a little to allow for you to wrap the function being called in a proxy function like this:

Modify your code that is being test to extract the setTimeout code into another function:

Original Code:

// Disables all submit buttons after a submit button is pressed. 
var block_all_submit_and_ajax = function( el ) { 
    // Clone the clicked button, we need to know what button has been clicked so that we can react accordingly 
    var $clone = $( el ).clone(); 
    // Change the type to hidden 
    $clone.attr( 'type', 'hidden' ); 
    // Put the hidden button in the DOM 
    $( el ).after( $clone ); 
    // Disable all submit button. I use setTimeout otherwise this doesn't work in chrome. 
    setTimeout(function() { 
        $( '#facebook input[type=submit]' ).prop( 'disabled', true ); 
    }, 10); 
    // unbind all click handler from ajax 
    $( '#facebook a.btn' ).unbind( "click" ); 
    // Disable all AJAX buttons. 
    $( '#facebook a.btn' ).click( function( e ) { 
        e.preventDefault(); 
        e.stopImmediatePropagation(); 
    } ); 
};

Modified Code:

// Disables all submit buttons after a submit button is pressed. 
var block_all_submit_and_ajax = function( el ) { 
    // Clone the clicked button, we need to know what button has been clicked so that we can react accordingly 
    var $clone = $( el ).clone(); 
    // Change the type to hidden 
    $clone.attr( 'type', 'hidden' ); 
    // Put the hidden button in the DOM 
    $( el ).after( $clone ); 
    // Disable all submit button. I use setTimeout otherwise this doesn't work in chrome. 
    setTimeout(disableSubmitButtons, 10); 
    // unbind all click handler from ajax 
    $( '#facebook a.btn' ).unbind( "click" ); 
    // Disable all AJAX buttons. 
    $( '#facebook a.btn' ).click( function( e ) { 
        e.preventDefault(); 
        e.stopImmediatePropagation(); 
    } ); 
};

var utilityFunctions =
{
  disableSubmitButtons : function()
  {
    $( '#facebook input[type=submit]' ).prop( 'disabled', true ); 

  }
}

Next I would modify the testing code like this:

it( "Disable all submit buttons", function() { 
    // Get a button 
    var $button = $( '#ai1ec_subscribe_users' ); 

    var originalFunction = utilityFunctions.disableSubmitButtons;
    utilityFunctions.disableSubmitButtons = function()
    {
        // call the original code, and follow it up with the test
        originalFunction();

        // check that all submit are disabled 
        $( '#facebook input[type=submit]' ).each( function( i, el ) { 
            console.log( 'f' ); 
            expect( el ).toHaveProp( 'disabled', true ); 
        }); 

        // set things back the way they were
        utilityFunctions.disableSubmitButtons = originalFunction;
    }

    // Call the function 
    utility_functions.block_all_submit_and_ajax( $button.get(0) ); 
}); 

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNicola PeluchettiView Question on Stackoverflow
Solution 1 - JavascriptloganfsmythView Answer on Stackoverflow
Solution 2 - JavascriptDominicView Answer on Stackoverflow
Solution 3 - JavascriptE. MagginiView Answer on Stackoverflow
Solution 4 - JavascriptMark At Ramp51View Answer on Stackoverflow