Shortcut.test.tsx 6.1 KB

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