How can I mock the JavaScript 'window' object using Jest?
Asked Answered
U

17

244

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 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 window. I tried to set window.open with a mock function, but this way doesn't work. Below is the test case:

it('the 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?

Undertrump answered 27/1, 2017 at 1:31 Comment(1)
See also #46275389Pathan
A
221

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();
});
Allergy answered 12/7, 2019 at 2:46 Comment(7)
That's much better than Object.defineProperty since this allows not to affect other tests when mocking.Sexivalent
This should be the accepted answer since it mocks/spies instead of changing the actual global propertyValdovinos
I just used x = jest.spyOn(window, 'open') and x.mockImplementation(() => {}), fyi, but all i needed to mock was window.open.Poignant
@Poignant I was looking for a way to mock window.open and you saved my day, thanks!Roadrunner
I'm getting the The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string. Consider using the "jsdom" test environment. error.Leckie
Unfortunately not a compatible approach with typescript.Germanous
@BrianC You can cast the correct type. In this case windowSpy = jest.spyOn(window, "window", "get") as jest.Mock;Alius
A
134

Instead of window, use global:

it('the 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);
Aguilera answered 27/1, 2017 at 7:19 Comment(1)
These approaches throw errors these days, but stackoverflow.com/a/56999581 still works.Abidjan
S
40

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}`);
  }
}
Shrovetide answered 12/1, 2020 at 14:17 Comment(0)
R
26

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')
Ryals answered 4/9, 2020 at 7:22 Comment(0)
B
20

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;
});
Bagehot answered 29/6, 2020 at 17:33 Comment(0)
O
14

We can also define it using global in setupTests:

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

And call it using global in the actual test:

// File 'yourtest.test.js'
it('the correct URL is called', () => {
    statementService.openStatementsReport(111);
    expect(global.open).toBeCalled();
});
Obsolescent answered 31/5, 2018 at 5:36 Comment(0)
A
9

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
  });
});
Airscrew answered 13/3, 2020 at 5:15 Comment(0)
C
8

The window object in Jest is self-mocking

One of the things unaddressed in other answers is a comment by the OP:

Using Jest, I don't know how to mock the window.

The window object is already mocked and can be referenced out of the box.

From the documentation:

Jest ships with jsdom which simulates a DOM environment as if you were in the browser. This means that every DOM API that we call can be observed in the same way it would be observed in a browser!

Example:

describe('i am a window', () => {
    it('has a window object', () => {
      expect(window).toBeTruthy(); // test will pass
    });
});
Carranza answered 8/1, 2022 at 2:39 Comment(0)
M
5

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();
});
Maletta answered 1/3, 2018 at 12:0 Comment(0)
D
5

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();
Dutton answered 22/8, 2019 at 15:1 Comment(0)
M
4

Try simply:

let windowOpenSpy: jest.SpyInstance;
beforeEach(() => {
    windowOpenSpy = jest.spyOn(window, 'open');
});

it('should open window with dashboard url', () => {
    expect(windowOpenSpy).toBeCalledWith('your URL', '_blank');
});
Masaccio answered 25/8, 2021 at 8:3 Comment(2)
An explanation would be in order.Coincide
this is how I imagined the answer looking, and I can say this approach worked for me.Carny
O
2

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]

Olympian answered 8/5, 2019 at 10:52 Comment(0)
L
1

You 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')
    })
})
Lacerta answered 31/1, 2021 at 6:3 Comment(1)
An explanation would be in order.Coincide
M
1

I have a utility function which allows me to mock any method on the window like so:

  function givenMockWindowMethods(methods: Partial<{ [key in keyof Window]: jest.Mock<any, any> }>): () => void {
    const mocks = Object.values(methods);

    Object.entries(methods).forEach(([key, value]) => {
      Object.defineProperty(window, key, { value });
    });

    return (): void => mocks.forEach((mock) => mock?.mockClear());
  }

So if I need to mock the open method (or anything really) on the window, I can do:

      const cleanupMocks = givenMockWindowMethods({ open: jest.fn() });
      // expect(...).toBe(...)

      //at the end of the test, clean it up
      cleanupMocks()

Marou answered 14/9, 2021 at 0:14 Comment(0)
P
0
const windowSpy = jest.spyOn(iFrame, "contentWindow", "get");
windowSpy.mockImplementation(() => ({
  location: {
    origin: "https://test.com",
    href: "href",
    hash: "hash"
  }
}));
Phebe answered 2/9, 2021 at 15:56 Comment(1)
An explanation would be in order.Coincide
P
0

I tried a similar test, and it worked with me...

My code:

export const Blah = () => {
        const BLAH = 'https://www.google.com/'
        const handleBlah = () => {
            window.open(BLAH, '_blank')
        }

    return (
        <button onClick={handleBlah}> BLAHBLAH </button>
    )
}

My test using Jest:

it('should be able to render "BLAHBLAH " button ', () => {
    window.open = jest.fn();
    const BLAH = 'https://www.google.com/'
    const { getByText } = render(<Blah/>) // Get text by my page Blah
    const buttonGoToBlah = getByText('BLAHBLAH') // Get button by text
    fireEvent.click(buttonGoToBlah) // Simulate the click event

    expect(window.open).toHaveBeenCalledTimes(1) // Expect the window.open have to been called at least once.
    expect(window.open).toHaveBeenCalledWith(BLAH, '_blank'); // And the page should be the same called in my BLAH page
})
Passion answered 28/9, 2021 at 14:40 Comment(0)
B
0

Date: July 2023

Jest version: 28.1.3

This version spys on window.open (via global.open) then restores window.open after all tests are completed.

const realOpen = global.open; // store real window.open

describe('Given [thing you are testing]', () => {
  beforeAll(() => {
    jest.spyOn(global, 'open'); // create spy in beforeAll hook
  });

  afterAll(() => {
    global.open = realOpen; // after all the tests are completed restore window.open in afterAll hook
  });

  it('the correct URL is called', () => {
    jest.clearAllMocks(); // clear all mocks before running statementService.openStatementsReport
    statementService.openStatementsReport(111);
    expect(global.open).toBeCalled();
  });
});
Bother answered 5/7, 2023 at 10:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.