Skip to content

Commit 0f79b0b

Browse files
authored
Add tests for custom utility hooks. Models for same w/ React-Testing-Library. (#84)
* Add tests for custom utility hooks. Models for same, RTL. * Reduce repetitive code.
1 parent 1d2bb29 commit 0f79b0b

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

test/util/useRoute.test.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// @vitest-environment jsdom
2+
3+
import { cleanup, render, screen } from '@testing-library/react'
4+
import userEvent from '@testing-library/user-event'
5+
import React, { FunctionComponent } from 'react'
6+
import { MemoryRouter } from 'react-router-dom'
7+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
8+
import useRoute, { Route } from '../../src/util/useRoute'
9+
10+
type ConsumerProps = {
11+
location: string
12+
navigationTarget?: string
13+
}
14+
15+
const InnerConsumer: FunctionComponent<ConsumerProps> = (props: ConsumerProps) => {
16+
const { navigationTarget } = props
17+
const { route, setRoute } = useRoute()
18+
const splitTarget = navigationTarget?.split('/') || ["", "", ""]
19+
const routeTarget = {
20+
page: splitTarget[1],
21+
runId: splitTarget[2]
22+
} as unknown as Route
23+
24+
return (
25+
<div>
26+
<div>page: {route.page}</div>
27+
<div>run id: {(route as unknown as {page: 'run', runId: string}).runId ?? '' }</div>
28+
<button onClick={() => setRoute(routeTarget)}></button>
29+
</div>
30+
)
31+
32+
}
33+
34+
const Consumer: FunctionComponent<ConsumerProps> = (props: ConsumerProps) => {
35+
const { location } = props
36+
return (
37+
<MemoryRouter initialEntries={[location]}>
38+
<InnerConsumer {...props} />
39+
</MemoryRouter>
40+
)
41+
}
42+
43+
describe("Navigation hook ", () => {
44+
afterEach(() => {
45+
cleanup()
46+
})
47+
test("Interprets route as home page if pathname does not begin with run", () => {
48+
render(<Consumer location="/not-run/" />)
49+
const pageInfo = screen.getByText(/page/).textContent ?? ''
50+
expect(pageInfo.includes('home')).toBeTruthy()
51+
})
52+
test("Interprets route as run page if pathname begins with run", () => {
53+
render(<Consumer location="/run/15" />)
54+
const pageTxt = screen.getByText(/page/).textContent ?? ''
55+
const runIdTxt = screen.getByText(/run id/).textContent ?? ''
56+
expect(pageTxt.includes('run')).toBeTruthy()
57+
expect(runIdTxt.includes('15')).toBeTruthy()
58+
})
59+
})
60+
61+
// Okay, in this case, with using hoisting to do this once for the whole file
62+
// (as we don't need to change anything about the mocking between tests)
63+
// Note the use of importActual, the way to use the 'real' underlying module in vi
64+
// equivalent to jest's requireActual
65+
const mockNavigate = vi.fn()
66+
vi.mock('react-router-dom', async () => {
67+
const router = await vi.importActual('react-router-dom') as unknown as object
68+
return { ...router, useNavigate: () => mockNavigate }
69+
})
70+
71+
describe("Navigation hook--route-setting callback", () => {
72+
let user
73+
afterEach(() => cleanup())
74+
beforeEach(() => {
75+
vi.resetAllMocks()
76+
user = userEvent.setup()
77+
})
78+
test("Navigates to empty path if route page is home", async () => {
79+
render(<Consumer location="/not-run/" navigationTarget='/home/123' />)
80+
await user.click(screen.getByRole('button'))
81+
expect(mockNavigate).toHaveBeenCalledOnce()
82+
const lastCall = mockNavigate.mock.lastCall[0]
83+
expect(lastCall.pathname).toEqual('')
84+
})
85+
test("Navigates to run page if route page is run", async () => {
86+
render(<Consumer location="/not-run/" navigationTarget='/run/123' />)
87+
await user.click(screen.getByRole('button'))
88+
expect(mockNavigate).toHaveBeenCalledOnce()
89+
const lastCall = mockNavigate.mock.lastCall[0]
90+
expect(lastCall.pathname).toEqual('/run/123')
91+
})
92+
test("Does nothing if route page is something else", async () => {
93+
render(<Consumer location="/not-run/" navigationTarget='/bad-address/123' />)
94+
await user.click(screen.getByRole('button'))
95+
expect(mockNavigate).toHaveBeenCalledTimes(0)
96+
})
97+
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// @vitest-environment jsdom
2+
3+
import { cleanup, render, screen } from '@testing-library/react'
4+
import React, { FunctionComponent } from 'react'
5+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
6+
import useWindowDimensions from '../../src/util/useWindowDimensions'
7+
8+
const Consumer: FunctionComponent = () => {
9+
const { width, height } = useWindowDimensions()
10+
return (
11+
<div>
12+
<div>Width: {width}</div>
13+
<div>Height: {height}</div>
14+
</div>
15+
)
16+
}
17+
18+
describe("Window dimensions hook", () => {
19+
const myWidth = 200
20+
const myHeight = 500
21+
const myNewWidth = 400
22+
const myNewHeight = 800
23+
24+
afterEach(() => cleanup())
25+
26+
beforeEach(() => {
27+
global.window.innerWidth = myWidth
28+
global.window.innerHeight = myHeight
29+
})
30+
31+
test("Begins with dimensions of surrounding window", () => {
32+
render(<Consumer />)
33+
const widthContext = screen.getByText(/Width/).textContent ?? ''
34+
const heightContext = screen.getByText(/Height/).textContent ?? ''
35+
expect(widthContext.includes(myWidth.toString())).toBeTruthy()
36+
expect(heightContext.includes(myHeight.toString())).toBeTruthy()
37+
})
38+
test("Updates dimensions when window resizes", async () => {
39+
render(<Consumer />)
40+
41+
global.window.innerWidth = myNewWidth
42+
global.window.innerHeight = myNewHeight
43+
let widthContext = (await screen.findByText(/Width/)).textContent ?? ''
44+
let heightContext = (await screen.findByText(/Height/)).textContent ?? ''
45+
expect(widthContext.includes(myWidth.toString())).toBeTruthy()
46+
expect(heightContext.includes(myHeight.toString())).toBeTruthy()
47+
48+
global.window.dispatchEvent(new Event('resize'))
49+
widthContext = (await screen.findByText(/Width/)).textContent ?? ''
50+
heightContext = (await screen.findByText(/Height/)).textContent ?? ''
51+
expect(widthContext.includes(myNewWidth.toString())).toBeTruthy()
52+
expect(heightContext.includes(myNewHeight.toString())).toBeTruthy()
53+
})
54+
test("Leaves no event listeners after component unmounts", async () => {
55+
global.window.addEventListener = vi.fn()
56+
global.window.removeEventListener = vi.fn()
57+
render(<Consumer />)
58+
expect(global.window.addEventListener).toHaveBeenCalledOnce()
59+
expect(global.window.removeEventListener).toBeCalledTimes(0)
60+
cleanup()
61+
expect(global.window.removeEventListener).toHaveBeenCalledOnce()
62+
})
63+
})

0 commit comments

Comments
 (0)