simonihmig / ember-window-mock

Mock window global in tests with test-friendly replacements for location, alert/confirm/prompt/ and more.
MIT License
64 stars 17 forks source link
ember ember-addon

ember-window-mock

CI Ember Observer Score npm version

This Ember CLI addon provides the window global as an ES6 module import that you can use in any component or controller where you need window. But some of its properties and functions are prohibitive to be used in tests as they will break the test run:

So when running tests this import will be replaced with one that mocks these critical parts.

Compatibility

Installation

ember install ember-window-mock

Usage

How to use it in your app

Let's say you want to redirect to an external URL. A simple controller could look like this:

import Controller from "@ember/controller";
import { action } from "@ember/object";

export default class IndexController extends Controller {
  @action
  redirect(url) {
    window.location.href = url;
  }
}

With this addon, you can just import window instead of using the global:

import Controller from "@ember/controller";
import { action } from "@ember/object";
import window from "ember-window-mock";

export default class IndexController extends Controller {
  @action
  redirect(url) {
    window.location.href = url;
  }
}

Everything else works as you would expect, since the import is exactly the same as the global, when not running tests.

The window mock

When running in the test environment, the import will be replaced with a mock. It is a proxy to window, so all of the non-critical properties and functions just use the normal window global. But the critical parts are replaced suitable for tests:

Moreover it allows you to set any (nested) properties, even if they are defined as read only. This way you can pretend different environments in your tests. For example you can fake different devices by changing

See below for some examples.

Important:

Resetting the state in tests

It is possible to leak some state on the window mock between tests. For example when your app sets location.href in a test like this:

window.location.href = "http://www.example.com";

For the following test window.location.href will still be 'http://www.example.com', but instead it should have a fresh instance of the window mock. Therefore this addon exports a setupWindowMock function to kill all changed state on window:

import { setupWindowMock } from 'ember-window-mock/test-support';

module('SidebarController', function(hooks) {
  setupWindowMock(hooks);

  test(...);
});

If you want to reset the state within a test, you can explicitly call reset:

import window from 'ember-window-mock';
import { setupWindowMock, reset } from 'ember-window-mock/test-support';

module('SidebarController', function(hooks) {
  setupWindowMock(hooks);

  test('some test', function(assert) {
    window.location.href = 'https://example.com';
    assert.strictEqual(window.location.hostname, 'example.com');

    reset();

    assert.strictEqual(window.location.hostname, 'localhost');
  });
});

createMockedWindow()

When all you need is mocking the global window object, the guide above has you covered. But there can be cases where you want to create a new mocked window object from scratch, for example to mock window.parent with a different window instance. This you can use the createMockedWindow() test helper for:

import window from 'ember-window-mock/test-support';
import { createMockedWindow, setupWindowMock } from 'ember-window-mock/test-support';

module('SidebarController', function(hooks) {
  setupWindowMock(hooks);

  test('app is running in iframe', function(assert) {
    window.location.href = 'https://myapp.com';
    window.parent = createMockedWindow();
    window.parent.location.href = 'https://example.com';

    // ...
  });
});

setupWindowMock() will not reset the state of any explicitly created mocked windows, but in most cases this is not needed, since as soon as the reference to that mocked window is not used anymore, it will not have any effects and regular garbage collection will dispose the object. However, if you need to reset the state explicitly within a test, you can do so by passing the mocked window object to reset():

import { createMockedWindow, reset } from 'ember-window-mock/test-support';

const mockedWindow = createMockedWindow();
mockedWindow.localStorage.set('foo', 'bar');
// do something
reset(mockedWindow);
// now mockedWindow is back to its original state again

Test examples

Mocking window.location

Given a controller like the one above, that redirects to some URL when a button is clicked, an application test could like this:

import { module, test } from "qunit";
import { click, visit } from "@ember/test-helpers";
import { setupApplicationTest } from "ember-qunit";
import window from "ember-window-mock";
import { setupWindowMock } from "ember-window-mock/test-support";

module("Acceptance | redirect", function (hooks) {
  setupApplicationTest(hooks);
  setupWindowMock(hooks);

  test("it redirects when clicking the button", async function (assert) {
    await visit("/");
    await click("button");

    assert.equal(window.location.href, "http://www.example.com");
  });
});

Mocking confirm()

Here is an example that uses ember-sinon-qunit to replace confirm, so you can easily check if it has been called, and to return some defined value:

import { module, test } from "qunit";
import { click, visit } from "@ember/test-helpers";
import { setupApplicationTest } from "ember-qunit";
import window from "ember-window-mock";
import { setupWindowMock } from "ember-window-mock/test-support";
import sinon from "sinon";

module("Acceptance | redirect", function (hooks) {
  setupApplicationTest(hooks);
  setupWindowMock(hooks);

  test("it deletes an item", async function (assert) {
    let stub = sinon.stub(window, "confirm");
    stub.returns(true);

    await visit("/");
    await click("[data-test-delete]");

    assert.ok(stub.calledOnce);
    assert.ok(stub.calledWith("Are you sure?"));
  });
});

Mocking open()

Here is an example that assigns a new function to replace open. You can check if the function has been called as well as the value of its parameters.

import { module, test } from "qunit";
import { click, visit } from "@ember/test-helpers";
import { setupApplicationTest } from "ember-qunit";
import window from "ember-window-mock";
import { setupWindowMock } from "ember-window-mock/test-support";

module("Acceptance | new window", function (hooks) {
  setupApplicationTest(hooks);
  setupWindowMock(hooks);

  test("it opens a new window when clicking the button", async function (assert) {
    await visit("/");
    window.open = (urlToOpen) => {
      assert.equal(urlToOpen, "http://www.example.com/some-file.jpg");
    };
    await click("button");
  });
});

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.