Tony Lâmpada
Tony Lâmpada

Reputation: 5459

How to unit test javascript code that dynamically loads javascript files?

Suppose file1.js has a function called doSomething() that dynamically loads file2.js

file2.js sets a global variable window.FILE2_LOADED == true

How can I write a test like below?

describe("A Unit Test", function() {
  it("can I load scripts dynamically here?", function() {
    loadScript('base/file2.js');
    waitForDynamicallyLoadedScriptsToLoad_IWishThisFunctionExisted(function(){
        expect(window.FILE2_LOADED).equal(true);
    });
  });

});

UPDATE: I found some solutions (and no-solutions) that I still don't like, but think it's worth documenting.

It's on this Github repo (it uses Mocha, not Jasmine) -> https://github.com/tonylampada/AngularJS-Testing-Article

You can run it with grunt test:unit

The relevant code is in dynamic_load_test.js

describe("Unit: dynamic loading scripts", function() {
  it("method 1 works, but using setTimeout and done is still ugly"+
    "also, it may interfere with other tests", function(done) {
    console.log('----------- method 1 -------------');
    window.ACOUNTER = undefined;
    loadScript('base/app/scripts/file2.js');
    setTimeout(function(){
        console.log('m1_2: '+window.ACOUNTER);
        expect(window.ACOUNTER).equal(1);
        done()
    }, 1000)
    console.log('m1_1: '+window.ACOUNTER);
  });

  it("method 2 works too, but I don't wanna have acces to the callback", function() {
    window.ACOUNTER = undefined;
    console.log('----------- method 2 -------------');
    loadScript('base/app/scripts/file2.js', function(){
        console.log('m2_2: '+window.ACOUNTER);
        expect(window.ACOUNTER).equal(1);
    });
    console.log('m2_1: '+window.ACOUNTER);
  });

  it("method 3, doesn't work", function() {
    window.ACOUNTER = undefined;
    console.log('----------- method 3 -------------');
    loadScript('base/app/scripts/file2.js');
    describe("wait for all async to finish", function(){
        it("now it will be loaded", function(){
            console.log('m3_2: '+window.ACOUNTER);
            expect(window.ACOUNTER).equal(1);
        })
    })
    console.log('m3_1: '+window.ACOUNTER);
  });

  it("method 4, use jquery. Callback is never invoked.", function() {
    window.ACOUNTER = undefined;
    console.log('----------- method 4 -------------');
    loadScript('base/app/scripts/file2.js');

    $(document).ajaxStop(function () {
        console.log('m4_2: '+window.ACOUNTER);
        expect(window.ACOUNTER).equal(1);
    });

    console.log('m4_1: '+window.ACOUNTER);
  });

});

Upvotes: 2

Views: 3191

Answers (2)

Ben Reeves
Ben Reeves

Reputation: 51

As Mchl stated, using an async function as your test and awaiting the call to add the script will work.

//jasmine setup
let scriptContainer = null;
beforeEach(function() {
  scriptContainer = document.createElement("div");
  document.body.appendChild(scriptContainer);
});
afterEach(function() {
  document.body.removeChild(scriptContainer);
});
it("resolves a promise if the script loads", async function() {
  //setup
  const sourceUrl = "myTestFile.js";
  const successCallback = jasmine.createSpy();
  const errorCallback = jasmine.createSpy();
  //test
  await loadScript(sourceUrl, scriptContainer).then(successCallback).catch(errorCallback);

  //assert
  expect(successCallback).toHaveBeenCalled();
});
//function for loading script
function loadScript(url, targetElement) {
        return new Promise(function (resolve, reject) {
            var scriptElement = document.createElement("script");
            scriptElement.onload = function () {
                resolve();
            };
            scriptElement.onerror = function (error) {
                reject(error);
            };
            scriptElement.src = url;
            targetElement.appendChild(scriptElement);
        });
    }

Be aware, if you're looking to test loading a script from your own directory, the way you reference you javascript file may be different depending on how you run your unit tests. If you use the SpecRunner.html file, you can just reference the script relative to the html file. If you use the command line headless browser, you will need to include any scripts you want to load in the karma.conf.js AND the path to the file will be different. I jump back and forth between the two and wrote a helper function to alter the source URL. This worked for my environment:

function getScriptBase() {
  //varies depending on whether tests are run from command line or specRunner in browser
  //for command line, scripts must also be included in karma.conf.js or they will not exist for the test runner
  return window.location.href.indexOf("localhost") > -1 ? "/base/" : "";
}

Upvotes: 0

Mchl
Mchl

Reputation: 62395

Jasmine supports asynchronous tests: https://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support

I would also have the path to the file to be loaded set through an argument to a function, so that you could load some simple mock file instead of an entire (supposedly requiring some other dependencies) script.

Upvotes: 1

Related Questions