Hey there 👋
It's been a long time since I wrote a post to this blog and recently came across this problem, which will surely come across again so I deemed it blog-worthy.
This example, I needed to test a custom hook which tracks the window
's innerWidth
and innerHeight
and I will be referring to it as useWindowSize
in this post, heres what it looks like:
// useWindowSize.ts
import { useLayoutEffect, useState } from "react"
interface WindowSize {
width: number
height: number
}
export const useWindowSize = () => {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: 0,
height: 0,
})
useLayoutEffect(() => {
const updateSize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight })
}
window.addEventListener("resize", updateSize)
updateSize()
return () => window.removeEventListener("resize", updateSize)
}, [])
return windowSize
}
Hopefully, you'll walk away from reading this article knowing how to test similar components or hooks, you can also blindly copy and paste the code, both are cool!
Create a customGlobal object
Firstly we use the global
object in Jest if we want to mock window
properties. We don't have access to a window
object as we normally would in the browser.
If you're using plain old JavaScript you can skip this part, but when using TypeScript we'll need to type the window properties we wish to mock!
This first solution is a bit of an ugly workaround, but hey it works 🤷♂️and we're still getting value from testing. We can do this by giving customGlobal
an any
type.
// useWindowSize.test.ts
const customGlobal: any = global
If you really wanted to add better types to customGlobal
we can achieve this by extending NodeJS.Global
itself.
// useWindowSize.test.ts
import Global = NodeJS.Global
interface CustomGlobal extends Global {
innerWidth?: number
innerHeight?: number
}
const customGlobal: CustomGlobal = global
Mocking window properties
Now TypeScript is pleased with our customGlobal
object, we can simply add the properties we wish to mock directly to it.
This is so we can test that useWindowSize
is correctly reading them from the window
object.
// useWindowSize.test.ts
// ...
describe("hooks/useWindowSize", () => {
customGlobal.innerWidth = 500
customGlobal.innerHeight = 900
//...
})
Firing window events
As you can see in the useWindowSize
hook above, it listens for resize
events. We're going to want to test this too.
// useWindowSize.test.ts
// ...
describe("hooks/useWindowSize", () => {
// ..
it("updates innerWidth and innerHeight values when window resizes", () => {
// ..
act(() => {
customGlobal.innerWidth = 1000
customGlobal.innerHeight = 1000
fireEvent(customGlobal, new Event("resize"))
})
})
})
Putting this all together
Here is the final test suite testing that useWindowSize
correctly reads from window
and updates when a resize
event happens on window
.
// useWindowSize.test.ts
import { fireEvent } from "@testing-library/react"
import { act, renderHook } from "@testing-library/react-hooks"
import { useWindowSize } from "./useWindowSize"
const customGlobal = global as any
describe("hooks/useWindowSize", () => {
customGlobal.innerWidth = 500
customGlobal.innerHeight = 800
it("reads initial innerWidth and innerHeight values from window", () => {
const { result } = renderHook(() => useWindowSize())
expect(result.current.width).toBe(500)
expect(result.current.height).toBe(800)
})
it("updates innerWidth and innerHeight values when window resizes", () => {
const { result } = renderHook(() => useWindowSize())
expect(result.current.width).toBe(500)
expect(result.current.height).toBe(800)
act(() => {
customGlobal.innerWidth = 1000
customGlobal.innerHeight = 1000
fireEvent(customGlobal, new Event("resize"))
})
expect(result.current.width).toBe(1000)
expect(result.current.height).toBe(1000)
})
})