138
votes

I need to test a function which opens a new tab in the browser

openStatementsReport(contactIds) {
  window.open(`a_url_${contactIds}`);
}

I would like to mock the window's open function so I can verify the correct URL is passed in to the open function.

Using Jest, I don't know how to mock the window. I tried to set window.open with a mock function but this way doesn't work. Below is the test case

it('correct url is called', () => {
  window.open = jest.fn();
  statementService.openStatementsReport(111);
  expect(window.open).toBeCalled();
});

But it gives me the error

expect(jest.fn())[.not].toBeCalled()

jest.fn() value must be a mock function or spy.
    Received:
      function: [Function anonymous]

What should I do to the test case?

11

11 Answers

101
votes

Instead of window use global

it('correct url is called', () => {
  global.open = jest.fn();
  statementService.openStatementsReport(111);
  expect(global.open).toBeCalled();
});

you could also try

const open = jest.fn()
Object.defineProperty(window, 'open', open);
98
votes

The following method worked for me. This approach allowed me to test some code that should work both in the browser and in Node.js, as it allowed me to set window to undefined.

This was with Jest 24.8 (I believe):

let windowSpy;

beforeEach(() => {
  windowSpy = jest.spyOn(window, "window", "get");
});

afterEach(() => {
  windowSpy.mockRestore();
});

it('should return https://example.com', () => {
  windowSpy.mockImplementation(() => ({
    location: {
      origin: "https://example.com"
    }
  }));

  expect(window.location.origin).toEqual("https://example.com");
});

it('should be undefined.', () => {
  windowSpy.mockImplementation(() => undefined);

  expect(window).toBeUndefined();
});
15
votes

There are a couple of ways to mock globals in Jest:

  1. Use the mockImplementation approach (the most Jest-like way), but it will work only for those variables which has some default implementation provided by jsdom. window.open is one of them:

    test('it works', () => {
      // Setup
      const mockedOpen = jest.fn();
      // Without making a copy, you will have a circular dependency problem
      const originalWindow = { ...window };
      const windowSpy = jest.spyOn(global, "window", "get");
      windowSpy.mockImplementation(() => ({
        ...originalWindow, // In case you need other window properties to be in place
        open: mockedOpen
      }));
    
      // Tests
      statementService.openStatementsReport(111)
      expect(mockedOpen).toBeCalled();
    
      // Cleanup
      windowSpy.mockRestore();
    });
    
  2. Assign the value directly to the global property. It is the most straightforward, but it may trigger error messages for some window variables, e.g. window.href.

    test('it works', () => {
      // Setup
      const mockedOpen = jest.fn();
      const originalOpen = window.open;
      window.open = mockedOpen;
    
      // Tests
      statementService.openStatementsReport(111)
      expect(mockedOpen).toBeCalled();
    
      // Cleanup
      window.open = originalOpen;
    });
    
  3. Don't use globals directly (requires a bit of refactoring)

    Instead of using the global value directly, it might be cleaner to import it from another file, so mocking will became trivial with Jest.

File ./test.js

jest.mock('./fileWithGlobalValueExported.js');
import { windowOpen } from './fileWithGlobalValueExported.js';
import { statementService } from './testedFile.js';

// Tests
test('it works', () => {
  statementService.openStatementsReport(111)
  expect(windowOpen).toBeCalled();
});

File ./fileWithGlobalValueExported.js

export const windowOpen = window.open;

File ./testedFile.js

import { windowOpen } from './fileWithGlobalValueExported.js';
export const statementService = {
  openStatementsReport(contactIds) {
    windowOpen(`a_url_${contactIds}`);
  }
}
12
votes

We can also define it using global in setupTests

// setupTests.js
global.open = jest.fn()

And call it using global in the actual test:

// yourtest.test.js
it('correct url is called', () => {
    statementService.openStatementsReport(111);
    expect(global.open).toBeCalled();
});
10
votes

In my component I need access to window.location.search. This is what I did in the Jest test:

Object.defineProperty(global, "window", {
  value: {
    location: {
      search: "test"
    }
  }
});

In case window properties must be different in different tests, we can put window mocking into a function, and make it writable in order to override for different tests:

function mockWindow(search, pathname) {
  Object.defineProperty(global, "window", {
    value: {
      location: {
        search,
        pathname
      }
    },
    writable: true
  });
}

And reset after each test:

afterEach(() => {
  delete global.window.location;
});
8
votes

I found an easy way to do it: delete and replace

describe('Test case', () => {
  const { open } = window;

  beforeAll(() => {
    // Delete the existing
    delete window.open;
    // Replace with the custom value
    window.open = jest.fn();
    // Works for `location` too, eg:
    // window.location = { origin: 'http://localhost:3100' };
  });

  afterAll(() => {
    // Restore original
    window.open = open;
  });

  it('correct url is called', () => {
    statementService.openStatementsReport(111);
    expect(window.open).toBeCalled(); // Happy happy, joy joy
  });
});
7
votes

I'm directly assigning jest.fn() to window.open.

window.open = jest.fn()
// ...code
expect(window.open).toHaveBeenCalledTimes(1)
expect(window.open).toHaveBeenCalledWith('/new-tab','__blank')
5
votes

You can try this:

import * as _Window from "jsdom/lib/jsdom/browser/Window";

window.open = jest.fn().mockImplementationOnce(() => {
    return new _Window({ parsingMode: "html" });
});

it("correct url is called", () => {
    statementService.openStatementsReport(111);
    expect(window.open).toHaveBeenCalled();
});
4
votes

If it's similar to the window location problem at window.location.href can't be changed in tests. #890, you could try (adjusted):

delete global.window.open;
global.window = Object.create(window);
global.window.open = jest.fn();
2
votes

In your Jest configuration, add setupFilesAfterEnv: ["./setupTests.js"], create that file, and add the code you want to run before the tests:

// setupTests.js
window.crypto = {
   .....
};

Reference: setupFilesAfterEnv [array]

1
votes

can test it:

describe('TableItem Components', () => {
    let open_url = ""
    const { open } = window;
    beforeAll(() => {
        delete window.open;
        window.open = (url) => { open_url = url };
    });
    afterAll(() => {
        window.open = open;
    });
    test('string type', async () => {
        wrapper.vm.openNewTab('http://example.com')
        expect(open_url).toBe('http://example.com')
    })
})