abhijit abhijit - 6 months ago 76
Javascript Question

Jasmine test For input type file and disabling/enabling a button on change event

I would like to know how Jasmine can test for the enabling/disabling of an input based on the change event. I have the following code in a function that I would like to be tested: -

$('#file-selector').change(function () {
var file = $('#file-selector').val();
if (file == '') {
$('#import').attr("disabled", true);
} else {
$('#import').attr("disabled", false);
}
});


Here's where I've got to so far, but would like to know the best way to achieve this.

beforeEach(function () {
setFixtures('<div id="file-selector-wrapper">' + '<input id="file-selector" name="xmlFile" type="file" /><span></span></div>' +
'<button id="import" type="submit" class="btn" disabled="disabled"></button>');
});

it('disables import button when file does not exist', function () {
var openSpy = jasmine.createSpy('open');
$("#file-selector").trigger('change');

});

Answer

A few things should happen to make your code more testable, but also cover all use-cases in your Jasmine tests. There should be multiple tests for this case, rather than just the one. Having more granular tests means that when one fails, it's easier to trace the failing code because the failing test is covering less lines.

To start off, I would recommend your JavaScript looks something like this: -

$('#file-selector').change(onFileChange);

function onFileChange() {
    var file = $('#file-selector').val();

    if (file == '') {
        $('#import').attr("disabled", true);
    } else {
        $('#import').attr("disabled", false);
    }
}

Then, in your Jasmine setup you can do something like this to test this functionality: -

// mocks
beforeEach(function () {
    var element = document.createElement('input');
    element.id = 'file-selector';
    var import = document.createElement('button');
    import.id = 'import';
    document.body.appendChild(element);
    document.body.appendChild(import);
});

it('calls onFileChange listener when a change has been detected/triggered', function () {
    var spy = spyOn(window, onFileChange); // assuming func is global here
    $('#file-selector').trigger('change');
    expect(spy).toHaveBeenCalled();
});

it('sets the attribute of the import button to false when there is a file', function () {
    document.getElementById('file-selector').value = 'mocked-file';

    onFileChange(); // calls to set attr

    var import = document.getElementById('import').getAttribute('disabled');
    expect(import).toBeFalsy(); // disabled to equal false as mocked file
});

it('sets the attribute of the import button to true when the file is empty', function () {
    document.getElementById('file-selector').value = '';

    onFileChange(); // calls to set attr

    var import = document.getElementById('import').getAttribute('disabled');
    expect(import).tobeTruthy(); // disabled to equal true as no file
});

// empties any previous DOM elements to make tests independent
afterEach(function () {
    var myNode = document.body;
    while (myNode.firstChild) {
        myNode.removeChild(myNode.firstChild);
    }
});

That afterEach is important when you are testing with things like this. It's important to clear the context of your tests to keep them independent and the way that you do this can change depending on what code is involved in your tests.

Do you see why I refactored the change function into its own? Now, you can prove the function is called when a change is detected, but test the function completely independently of that event in Jasmine, which means you're just testing the core logic that should happen when the function is called. Better yet, it would be even cooler to pass the value of the file-selector input as an argument to the onFileChange function, which makes it even easier to test.