Shortcut.test.tsx197 lines · main
1import { fireEvent, render, screen } from '@testing-library/react'
2import { beforeEach, describe, expect, it, vi } from 'vitest'
3
4import { Shortcut } from './Shortcut'
5import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
6
7const { mockUseShortcut, mockShortcutTooltip } = vi.hoisted(() => ({
8 mockUseShortcut: vi.fn(),
9 mockShortcutTooltip: vi.fn((props: any) => (
10 <div data-testid="shortcut-tooltip">{props.children}</div>
11 )),
12}))
13
14vi.mock('@/state/shortcuts/useShortcut', () => ({
15 useShortcut: mockUseShortcut,
16}))
17
18vi.mock('./ShortcutTooltip', () => ({
19 ShortcutTooltip: mockShortcutTooltip,
20}))
21
22describe('Shortcut', () => {
23 beforeEach(() => {
24 vi.clearAllMocks()
25 })
26
27 describe('rendering', () => {
28 it('renders the wrapped child', () => {
29 render(
30 <Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={() => {}}>
31 <button>Open</button>
32 </Shortcut>
33 )
34 expect(screen.getByRole('button', { name: 'Open' })).toBeInTheDocument()
35 })
36
37 it('wraps the child in ShortcutTooltip', () => {
38 render(
39 <Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={() => {}}>
40 <button>Open</button>
41 </Shortcut>
42 )
43 const tooltip = screen.getByTestId('shortcut-tooltip')
44 expect(tooltip).toContainElement(screen.getByRole('button', { name: 'Open' }))
45 })
46 })
47
48 describe('useShortcut wiring', () => {
49 it('forwards id and onTrigger to useShortcut', () => {
50 const handler = vi.fn()
51 render(
52 <Shortcut id={SHORTCUT_IDS.ACTION_BAR_SAVE} onTrigger={handler}>
53 <button>Save</button>
54 </Shortcut>
55 )
56 expect(mockUseShortcut).toHaveBeenCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, undefined)
57 })
58
59 it('forwards options to useShortcut', () => {
60 const handler = vi.fn()
61 const options = { enabled: true, registerInCommandMenu: true }
62 render(
63 <Shortcut id={SHORTCUT_IDS.ACTION_BAR_SAVE} onTrigger={handler} options={options}>
64 <button>Save</button>
65 </Shortcut>
66 )
67 expect(mockUseShortcut).toHaveBeenCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, options)
68 })
69
70 it('passes updated options to useShortcut on rerender', () => {
71 const handler = vi.fn()
72 const { rerender } = render(
73 <Shortcut
74 id={SHORTCUT_IDS.ACTION_BAR_SAVE}
75 onTrigger={handler}
76 options={{ enabled: false }}
77 >
78 <button>Save</button>
79 </Shortcut>
80 )
81 expect(mockUseShortcut).toHaveBeenLastCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, {
82 enabled: false,
83 })
84
85 rerender(
86 <Shortcut id={SHORTCUT_IDS.ACTION_BAR_SAVE} onTrigger={handler} options={{ enabled: true }}>
87 <button>Save</button>
88 </Shortcut>
89 )
90 expect(mockUseShortcut).toHaveBeenLastCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, {
91 enabled: true,
92 })
93 })
94
95 it('passes updated id to useShortcut on rerender', () => {
96 const handler = vi.fn()
97 const { rerender } = render(
98 <Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={handler}>
99 <button>Go</button>
100 </Shortcut>
101 )
102 expect(mockUseShortcut).toHaveBeenLastCalledWith(SHORTCUT_IDS.NAV_HOME, handler, undefined)
103
104 rerender(
105 <Shortcut id={SHORTCUT_IDS.NAV_TABLE_EDITOR} onTrigger={handler}>
106 <button>Go</button>
107 </Shortcut>
108 )
109 expect(mockUseShortcut).toHaveBeenLastCalledWith(
110 SHORTCUT_IDS.NAV_TABLE_EDITOR,
111 handler,
112 undefined
113 )
114 })
115 })
116
117 describe('ShortcutTooltip wiring', () => {
118 it('forwards shortcutId to ShortcutTooltip', () => {
119 render(
120 <Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={() => {}}>
121 <button>Home</button>
122 </Shortcut>
123 )
124 expect(mockShortcutTooltip).toHaveBeenCalled()
125 expect(mockShortcutTooltip.mock.calls.at(-1)![0].shortcutId).toBe(SHORTCUT_IDS.NAV_HOME)
126 })
127
128 it('forwards tooltip positioning props', () => {
129 render(
130 <Shortcut
131 id={SHORTCUT_IDS.NAV_HOME}
132 onTrigger={() => {}}
133 side="right"
134 align="start"
135 sideOffset={8}
136 delayDuration={100}
137 >
138 <button>Home</button>
139 </Shortcut>
140 )
141 const props = mockShortcutTooltip.mock.calls.at(-1)![0]
142 expect(props.side).toBe('right')
143 expect(props.align).toBe('start')
144 expect(props.sideOffset).toBe(8)
145 expect(props.delayDuration).toBe(100)
146 })
147
148 it('forwards label override to ShortcutTooltip', () => {
149 render(
150 <Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={() => {}} label="Go home">
151 <button>Home</button>
152 </Shortcut>
153 )
154 expect(mockShortcutTooltip.mock.calls.at(-1)![0].label).toBe('Go home')
155 })
156
157 it('omits undefined positioning props rather than fabricating them', () => {
158 render(
159 <Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={() => {}}>
160 <button>Home</button>
161 </Shortcut>
162 )
163 const props = mockShortcutTooltip.mock.calls.at(-1)![0]
164 expect(props.side).toBeUndefined()
165 expect(props.align).toBeUndefined()
166 expect(props.sideOffset).toBeUndefined()
167 expect(props.delayDuration).toBeUndefined()
168 expect(props.label).toBeUndefined()
169 })
170 })
171
172 describe('child interactivity', () => {
173 it('does not intercept clicks on the wrapped child', () => {
174 const onClick = vi.fn()
175 const onTrigger = vi.fn()
176 render(
177 <Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={onTrigger}>
178 <button onClick={onClick}>Open</button>
179 </Shortcut>
180 )
181
182 fireEvent.click(screen.getByRole('button', { name: 'Open' }))
183 expect(onClick).toHaveBeenCalledTimes(1)
184 expect(onTrigger).not.toHaveBeenCalled()
185 })
186
187 it('does not invoke onTrigger during render — only the hotkey should trigger it', () => {
188 const onTrigger = vi.fn()
189 render(
190 <Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={onTrigger}>
191 <button>Open</button>
192 </Shortcut>
193 )
194 expect(onTrigger).not.toHaveBeenCalled()
195 })
196 })
197})