How to Mock the Window Location with Vitest

When writing unit tests for business logic that uses the Window object, we need to mock it in order to fake certain behaviors. Since our use of the window object corresponds frequently to manipulating the browser.

In this tutorial, we will learn how to mock the Window object, and develop a few examples in order to understand this better.

This tutorial is Framework agnostic. Let’s get started, shall we?

Prerequisites

Set the Vitest environment as browser-based. Use jsdom or happy-dom. you can do that inside vite.config.js (create it if it doesn’t exist):

import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    environment: 'happy-dom',
  },
})

This will provide us with a window object in our test environment.

How to Mock Location Reload

We usually access the window location to reload or redirect. Let’s imagine we have two functions that do these operations:

export function reloadWindow() {
  window.location.reload()
}

export function changeWindowLocation() {
  window.location = 'https://www.google.com'
}

Let’s then implement their unit tests. First, write the spec for the reload function:

import { describe } from "vitest";

describe('Window Functions', () => {
  describe('reloadWindow', () => {
    test('reloads the window', () => {})
  })
})

Then, implement the test by calling the function and expecting that window.reload was called:

import { describe, expect, test } from 'vitest'
import { reloadWindow } from './window-functions'

describe('Window Functions', () => {
  describe('reloadWindow', () => {
    test('reloads the window', () => {
      reloadWindow()

      expect(window.location.reload).toHaveBeenCalled()
    })
  })
})

At this point, the test will fail because the reload method is not mocked, hence, we can’t use it with toHaveBeenCalled.

vitest mock window location

To fix this, spy on it at the beginning of the outer describe block:

import { describe, expect, test, vi } from 'vitest'
import { reloadWindow } from './window-functions'

describe('Window Functions', () => {
  vi.spyOn(window.location, 'reload')

  describe('reloadWindow', () => {
    test('reloads the window', () => {
      reloadWindow()

      expect(window.location.reload).toHaveBeenCalled()
    })
  })
})

Since we are not altering the implementation of the method, we can just use vi.spyOn instead of vi.fn. so spying instead of mocking…

How to Assert the Current Location

Let’s do the same thing for changeWindowLocation function, First, we start with the spec:

import { describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'

describe('Window Functions', () => {
  vi.spyOn(window.location, 'reload')

  describe('reloadWindow', () => {
    test('reloads the window', () => {
      reloadWindow()

      expect(window.location.reload).toHaveBeenCalled()
    })
  })

  describe('changeWindowLocation', () => {
    test('changes location to https://www.google.com', () => {})
  })
})

Then we assert that after calling the function the window.location value changes to the expected one:

import { describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'

describe('Window Functions', () => {
  vi.spyOn(window.location, 'reload')

  describe('reloadWindow', () => {
    test('reloads the window', () => {
      reloadWindow()

      expect(window.location.reload).toHaveBeenCalled()
    })
  })

  describe('changeWindowLocation', () => {
    test('changes location to https://www.google.com', () => {
      changeWindowLocation()

      expect(window.location).toBe('https://www.google.com')
    })
  })
})

If we run the tests, they should work, as you can see in the figure below.

vitest mock window location

We didn’t have to mock anything this time, life is beautiful this way isn’t it? well, I am afraid to let you know that we have an issue actually, allow me to demonstrate!

If we add a new test that checks the window location, what do you think the result would be? let’s find out:

import { describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'

describe('Window Functions', () => {
  vi.spyOn(window.location, 'reload')

  describe('reloadWindow', () => {
    test('reloads the window', () => {
      reloadWindow()

      expect(window.location.reload).toHaveBeenCalled()
    })
  })

  describe('changeWindowLocation', () => {
    test('changes location to https://www.google.com', () => {
      changeWindowLocation()

      expect(window.location).toBe('https://www.google.com')
    })
  })

  test('window location should not be google.com here?', () => {
    expect(window.location).not.toBe('https://www.google.com')
  })
})

and the result is…

vitest mock window location

The problem we have is the window location is still carrying the same state between different tests, that’s a big no-no. Each test should have a fresh location object so that we don’t fall into false positive conditions.

To fix this, store the original “fresh” window.location in a constant, then before each test reset the current window.location to that “fresh” version:

import { beforeEach, describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'

describe('Window Functions', () => {
  const originalWindowLocation = window.location

  vi.spyOn(window.location, 'reload')

  beforeEach(() => {
    window.location = originalWindowLocation
  })

  describe('reloadWindow', () => {
    test('reloads the window', () => {
      reloadWindow()

      expect(window.location.reload).toHaveBeenCalled()
    })
  })

  describe('changeWindowLocation', () => {
    test('changes location to https://www.google.com', () => {
      changeWindowLocation()

      expect(window.location).toBe('https://www.google.com')
    })
  })

  test('window location should not be google.com here?', () => {
    expect(window.location).not.toBe('https://www.google.com')
  })
})

Now life is beautiful again:

vitest mock window location - tests passing again!

Closing Thoughts

You have now mastered the art of testing the window’s location object.

You can find the code on GitHub. Also, this tutorial is part of a series that goes through mocking the commonly used Web APIs and Packages, you can find the others here. Cheers!

Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19