commandsHooks.test.tsx250 lines · main
1import { act, render, screen, waitFor } from '@testing-library/react'
2import userEvent from '@testing-library/user-event'
3import { Fragment, useState } from 'react'
4import { beforeEach, describe, expect, it } from 'vitest'
5
6import { CommandProvider } from '../CommandProvider'
7import type { ICommand } from '../types'
8import { useCommands, useRegisterCommands } from './commandsHooks'
9
10const oneCommand = [
11 {
12 id: 'command-one',
13 name: 'Command one',
14 action: () => alert('One!'),
15 },
16]
17
18const twoCommands = [
19 ...oneCommand,
20 {
21 id: 'command-two',
22 name: 'Command two',
23 action: () => alert('Two!'),
24 },
25]
26
27let mockCommandMenuRenderCount = 0
28
29const MockCommandMenu = () => {
30 mockCommandMenuRenderCount++
31 const commandSections = useCommands()
32
33 return (
34 <div>
35 {commandSections.map((section) => (
36 <Fragment key={section.id}>
37 <h2>{section.name}</h2>
38 <ul>
39 {section.commands.map((cmd) => (
40 <li key={cmd.id}>{cmd.name}</li>
41 ))}
42 </ul>
43 </Fragment>
44 ))}
45 </div>
46 )
47}
48
49const SimpleComponent = ({ commands }: { commands: Array<ICommand> }) => {
50 useRegisterCommands('Section', commands)
51
52 return <span>Simple</span>
53}
54
55const ToggledComponent = ({ commands }: { commands: Array<ICommand> }) => {
56 const [visible, setVisible] = useState(true)
57
58 return (
59 <>
60 <button onClick={() => setVisible((vis) => !vis)}>Toggle</button>
61 {visible && <SimpleComponent commands={commands} />}
62 </>
63 )
64}
65
66const DisableableComponent = ({ commands }: { commands: Array<ICommand> }) => {
67 const [enabled, setIsEnabled] = useState(true)
68 useRegisterCommands('Section', commands, { enabled })
69
70 return (
71 <button onClick={() => setIsEnabled((enabled) => !enabled)}>
72 {enabled ? 'Disable commands' : 'Enable commands'}
73 </button>
74 )
75}
76
77const ChangeableCommands = () => {
78 const [commands, setCommands] = useState([{ ...twoCommands[0] }])
79 useRegisterCommands('Section', commands, { deps: [commands] })
80
81 return <button onClick={() => setCommands([{ ...twoCommands[1] }])}>Use second command</button>
82}
83
84const RerenderableComponent = () => {
85 const [renderFlag, setRerenderFlag] = useState(0)
86
87 // Define commands inline so the array identity changes each render
88 const commands = [...oneCommand]
89 useRegisterCommands('Section', commands) // Note no dependency array
90
91 return (
92 <>
93 <button onClick={() => setRerenderFlag((flag) => ++flag)}>Rerender</button>
94 <span>Render flag: {renderFlag}</span>
95 </>
96 )
97}
98
99describe('useRegisterCommand', () => {
100 beforeEach(() => {
101 mockCommandMenuRenderCount = 0
102 })
103
104 it('registers a command when component mounts', () => {
105 render(
106 <CommandProvider>
107 <SimpleComponent commands={oneCommand} />
108 <MockCommandMenu />
109 </CommandProvider>
110 )
111
112 // @ts-ignore
113 expect(screen.getByRole('heading')).toHaveTextContent('Section')
114 expect(screen.getByRole('listitem')).toHaveTextContent('Command one')
115 })
116
117 it('registers multiple commands when component mounts', () => {
118 render(
119 <CommandProvider>
120 <SimpleComponent commands={twoCommands} />
121 <MockCommandMenu />
122 </CommandProvider>
123 )
124
125 expect(screen.getByRole('heading')).toHaveTextContent('Section')
126
127 const items = screen.getAllByRole('listitem')
128 expect(items).toHaveLength(2)
129 expect(items[0]).toHaveTextContent('Command one')
130 expect(items[1]).toHaveTextContent('Command two')
131 })
132
133 it('unregisters and reregisters a command when component unmounts and remounts', async () => {
134 act(() => {
135 render(
136 <CommandProvider>
137 <ToggledComponent commands={oneCommand} />
138 <MockCommandMenu />
139 </CommandProvider>
140 )
141 })
142
143 expect(screen.getByRole('listitem')).toHaveTextContent('Command one')
144
145 await act(async () => {
146 await userEvent.click(screen.getByRole('button'))
147 })
148 await waitFor(() => {
149 expect(screen.queryByRole('listitem')).toBeNull()
150 })
151
152 await act(async () => {
153 await userEvent.click(screen.getByRole('button'))
154 })
155 await waitFor(() => {
156 expect(screen.getByRole('listitem')).toHaveTextContent('Command one')
157 })
158 })
159
160 it('unregisters multiple commands when component unmounts', async () => {
161 act(() => {
162 render(
163 <CommandProvider>
164 <ToggledComponent commands={twoCommands} />
165 <MockCommandMenu />
166 </CommandProvider>
167 )
168 })
169
170 expect(screen.getAllByRole('listitem')).toHaveLength(2)
171
172 await act(async () => {
173 await userEvent.click(screen.getByRole('button'))
174 })
175 await waitFor(() => {
176 expect(screen.queryAllByRole('listitem')).toHaveLength(0)
177 })
178 })
179
180 it('unregisters and reregisters a command when enabled changes', async () => {
181 act(() => {
182 render(
183 <CommandProvider>
184 <DisableableComponent commands={oneCommand} />
185 <MockCommandMenu />
186 </CommandProvider>
187 )
188 })
189
190 expect(screen.getByRole('listitem')).toHaveTextContent('Command one')
191
192 await act(async () => {
193 await userEvent.click(screen.getByRole('button'))
194 })
195 await waitFor(() => {
196 expect(screen.queryByRole('listitem')).toBeNull()
197 })
198
199 await act(async () => {
200 await userEvent.click(screen.getByRole('button'))
201 })
202 await waitFor(() => {
203 expect(screen.getByRole('listitem')).toHaveTextContent('Command one')
204 })
205 })
206
207 it('reregisters commands when dependencies change', async () => {
208 act(() => {
209 render(
210 <CommandProvider>
211 <ChangeableCommands />
212 <MockCommandMenu />
213 </CommandProvider>
214 )
215 })
216
217 expect(screen.getByText('Command one')).toBeVisible()
218
219 await act(async () => {
220 await userEvent.click(screen.getByRole('button'))
221 })
222 await waitFor(() => {
223 expect(screen.queryByText('Command one')).toBeNull()
224 })
225 expect(screen.getByRole('listitem')).toHaveTextContent('Command two')
226 })
227
228 it("doesn't trigger command menu rerender when dependencies are unchanged", async () => {
229 expect(mockCommandMenuRenderCount).toBe(0)
230
231 act(() => {
232 render(
233 <CommandProvider>
234 <RerenderableComponent />
235 <MockCommandMenu />
236 </CommandProvider>
237 )
238 })
239
240 // React renders twice in dev mode
241 expect(mockCommandMenuRenderCount).toBe(2)
242
243 await act(async () => {
244 await userEvent.click(screen.getByRole('button'))
245 })
246 await waitFor(() => screen.getByText('Render flag: 1'))
247
248 expect(mockCommandMenuRenderCount).toBe(2)
249 })
250})