ShortcutsReferenceSheet.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // @ts-nocheck
  2. import type { HotkeyRegistrationView, SequenceRegistrationView } from '@tanstack/react-hotkeys'
  3. import { screen } from '@testing-library/react'
  4. import userEvent from '@testing-library/user-event'
  5. import { beforeEach, describe, expect, it, vi } from 'vitest'
  6. import { ShortcutsReferenceSheet } from './ShortcutsReferenceSheet'
  7. import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS, type ShortcutId } from '@/state/shortcuts/registry'
  8. import type { ShortcutHotkeyMeta } from '@/state/shortcuts/useShortcut'
  9. import { customRender } from '@/tests/lib/custom-render'
  10. const { mockUseHotkeyRegistrations } = vi.hoisted(() => ({
  11. mockUseHotkeyRegistrations:
  12. vi.fn<() => { hotkeys: HotkeyRegistrationView[]; sequences: SequenceRegistrationView[] }>(),
  13. }))
  14. vi.mock('@tanstack/react-hotkeys', async () => {
  15. const actual =
  16. await vi.importActual<typeof import('@tanstack/react-hotkeys')>('@tanstack/react-hotkeys')
  17. return {
  18. ...actual,
  19. useHotkeyRegistrations: mockUseHotkeyRegistrations,
  20. }
  21. })
  22. const ACTIVE_SHORTCUT_IDS = [
  23. SHORTCUT_IDS.COMMAND_MENU_OPEN,
  24. SHORTCUT_IDS.NAV_HOME,
  25. ] satisfies ShortcutId[]
  26. const ACTIVE_DATABASE_SHORTCUT_IDS = [
  27. ...ACTIVE_SHORTCUT_IDS,
  28. SHORTCUT_IDS.NAV_DATABASE_TABLES,
  29. ] satisfies ShortcutId[]
  30. const ACTIVE_AUTH_SHORTCUT_IDS = [
  31. ...ACTIVE_SHORTCUT_IDS,
  32. SHORTCUT_IDS.NAV_AUTH_USERS,
  33. ] satisfies ShortcutId[]
  34. const ACTIVE_FUNCTION_DETAIL_NAV_SHORTCUT_IDS = [
  35. ...ACTIVE_SHORTCUT_IDS,
  36. SHORTCUT_IDS.NAV_FUNCTION_DETAIL_OVERVIEW,
  37. ] satisfies ShortcutId[]
  38. const ACTIVE_REALTIME_NAV_SHORTCUT_IDS = [
  39. ...ACTIVE_SHORTCUT_IDS,
  40. SHORTCUT_IDS.NAV_REALTIME_INSPECTOR,
  41. ] satisfies ShortcutId[]
  42. const ACTIVE_REALTIME_INSPECTOR_SHORTCUT_IDS = [
  43. ...ACTIVE_SHORTCUT_IDS,
  44. SHORTCUT_IDS.INSPECTOR_JOIN_CHANNEL,
  45. SHORTCUT_IDS.INSPECTOR_TOGGLE_LISTENING,
  46. SHORTCUT_IDS.INSPECTOR_BROADCAST,
  47. SHORTCUT_IDS.INSPECTOR_COPY_MESSAGE,
  48. ] satisfies ShortcutId[]
  49. const ACTIVE_SURFACE_SHORTCUT_IDS = [
  50. ...ACTIVE_SHORTCUT_IDS,
  51. SHORTCUT_IDS.AUTH_USERS_REFRESH,
  52. SHORTCUT_IDS.FUNCTION_DETAIL_OPEN_TEST,
  53. SHORTCUT_IDS.FUNCTION_OVERVIEW_INTERVAL_15MIN,
  54. SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH,
  55. SHORTCUT_IDS.LOGS_PREVIEW_REFRESH,
  56. SHORTCUT_IDS.SQL_EDITOR_FORMAT,
  57. SHORTCUT_IDS.STORAGE_BUCKETS_REFRESH,
  58. SHORTCUT_IDS.STORAGE_EXPLORER_REFRESH,
  59. ] satisfies ShortcutId[]
  60. let sequenceIdCounter = 0
  61. const buildSequenceRegistration = (id: ShortcutId): SequenceRegistrationView => {
  62. const definition = SHORTCUT_DEFINITIONS[id]
  63. const meta: ShortcutHotkeyMeta = {
  64. id: definition.id,
  65. name: definition.label,
  66. referenceGroup: definition.referenceGroup,
  67. }
  68. return {
  69. id: `sequence_${++sequenceIdCounter}`,
  70. sequence: definition.sequence,
  71. options: {
  72. enabled: true,
  73. meta,
  74. },
  75. target: document,
  76. triggerCount: 0,
  77. hasFired: false,
  78. matchedStepCount: 0,
  79. partialMatchLastKeyTime: 0,
  80. }
  81. }
  82. const seedRegistrations = (ids: ShortcutId[]) => {
  83. mockUseHotkeyRegistrations.mockReturnValue({
  84. hotkeys: [],
  85. sequences: ids.map(buildSequenceRegistration),
  86. })
  87. }
  88. const renderShortcutsReferenceSheet = (ids: ShortcutId[] = ACTIVE_SHORTCUT_IDS) => {
  89. seedRegistrations(ids)
  90. const onOpenChange = vi.fn()
  91. customRender(<ShortcutsReferenceSheet open onOpenChange={onOpenChange} />)
  92. return { onOpenChange }
  93. }
  94. describe('ShortcutsReferenceSheet', () => {
  95. beforeEach(() => {
  96. sequenceIdCounter = 0
  97. mockUseHotkeyRegistrations.mockReset()
  98. mockUseHotkeyRegistrations.mockReturnValue({ hotkeys: [], sequences: [] })
  99. })
  100. it('renders the grouped shortcut list by default', async () => {
  101. renderShortcutsReferenceSheet()
  102. expect(await screen.findByText('Keyboard shortcuts')).toBeInTheDocument()
  103. expect(screen.getByLabelText('Search shortcuts')).toBeInTheDocument()
  104. expect(screen.getByText('Command Menu')).toBeInTheDocument()
  105. expect(screen.getByText('Navigation')).toBeInTheDocument()
  106. expect(screen.queryByText('Global Navigation')).not.toBeInTheDocument()
  107. expect(screen.queryByText('Database Navigation')).not.toBeInTheDocument()
  108. expect(screen.getByText('Open command menu')).toBeInTheDocument()
  109. expect(screen.getByText('Go to Project Overview')).toBeInTheDocument()
  110. })
  111. it('shows only active shortcuts in a group when the group label matches the search', async () => {
  112. const user = userEvent.setup()
  113. renderShortcutsReferenceSheet()
  114. await user.type(screen.getByLabelText('Search shortcuts'), 'navigation')
  115. expect(screen.getByText('Navigation')).toBeInTheDocument()
  116. expect(screen.getByText('Go to Project Overview')).toBeInTheDocument()
  117. expect(screen.queryByText('Command Menu')).not.toBeInTheDocument()
  118. expect(screen.queryByText('Go to Database')).not.toBeInTheDocument()
  119. })
  120. it('keeps the parent group header when only an item label matches', async () => {
  121. const user = userEvent.setup()
  122. renderShortcutsReferenceSheet()
  123. await user.type(screen.getByLabelText('Search shortcuts'), 'Go to Project Overview')
  124. expect(screen.getByText('Navigation')).toBeInTheDocument()
  125. expect(screen.getByText('Go to Project Overview')).toBeInTheDocument()
  126. expect(screen.queryByText('Open command menu')).not.toBeInTheDocument()
  127. expect(screen.queryByText('Command Menu')).not.toBeInTheDocument()
  128. })
  129. it('shows the database navigation section when database shortcuts are active', async () => {
  130. renderShortcutsReferenceSheet(ACTIVE_DATABASE_SHORTCUT_IDS)
  131. expect(await screen.findByText('Global Navigation')).toBeInTheDocument()
  132. expect(screen.getByText('Database Navigation')).toBeInTheDocument()
  133. expect(screen.queryByText(/^Navigation$/)).not.toBeInTheDocument()
  134. expect(screen.getByText('Go to Tables')).toBeInTheDocument()
  135. })
  136. it('shows the auth navigation section when auth shortcuts are active', async () => {
  137. renderShortcutsReferenceSheet(ACTIVE_AUTH_SHORTCUT_IDS)
  138. expect(await screen.findByText('Global Navigation')).toBeInTheDocument()
  139. expect(screen.getByText('Auth Navigation')).toBeInTheDocument()
  140. expect(screen.queryByText(/^Navigation$/)).not.toBeInTheDocument()
  141. expect(screen.getByText('Go to Users')).toBeInTheDocument()
  142. })
  143. it('shows the edge function tabs section when function tab shortcuts are active', async () => {
  144. renderShortcutsReferenceSheet(ACTIVE_FUNCTION_DETAIL_NAV_SHORTCUT_IDS)
  145. expect(await screen.findByText('Global Navigation')).toBeInTheDocument()
  146. expect(screen.getByText('Edge Function Tabs')).toBeInTheDocument()
  147. expect(screen.queryByText('Edge Function Page Navigation')).not.toBeInTheDocument()
  148. expect(screen.getByText('Go to Overview')).toBeInTheDocument()
  149. })
  150. it('shows the realtime navigation section when realtime shortcuts are active', async () => {
  151. renderShortcutsReferenceSheet(ACTIVE_REALTIME_NAV_SHORTCUT_IDS)
  152. expect(await screen.findByText('Global Navigation')).toBeInTheDocument()
  153. expect(screen.getByText('Realtime Navigation')).toBeInTheDocument()
  154. expect(screen.getByText('Go to Inspector')).toBeInTheDocument()
  155. })
  156. it('uses human labels for realtime inspector shortcut groups', async () => {
  157. renderShortcutsReferenceSheet(ACTIVE_REALTIME_INSPECTOR_SHORTCUT_IDS)
  158. expect(await screen.findByText('Realtime Inspector')).toBeInTheDocument()
  159. expect(screen.getByText('Join a channel')).toBeInTheDocument()
  160. expect(screen.getByText('Start/Stop listening')).toBeInTheDocument()
  161. expect(screen.getByText('Broadcast a message')).toBeInTheDocument()
  162. expect(screen.getByText('Copy selected message')).toBeInTheDocument()
  163. expect(screen.queryByText('realtime-inspector')).not.toBeInTheDocument()
  164. })
  165. it('uses human labels for active surface shortcut groups', async () => {
  166. renderShortcutsReferenceSheet(ACTIVE_SURFACE_SHORTCUT_IDS)
  167. expect(await screen.findByText('Auth Users')).toBeInTheDocument()
  168. expect(screen.getByText('Edge Function Actions')).toBeInTheDocument()
  169. expect(screen.getByText('Edge Function Overview')).toBeInTheDocument()
  170. expect(screen.getByText('Edge Functions')).toBeInTheDocument()
  171. expect(screen.getByText('Logs Explorer')).toBeInTheDocument()
  172. expect(screen.getByText('SQL Editor')).toBeInTheDocument()
  173. expect(screen.getByText('Storage Buckets')).toBeInTheDocument()
  174. expect(screen.getByText('Storage File Explorer')).toBeInTheDocument()
  175. expect(screen.queryByText('auth-users')).not.toBeInTheDocument()
  176. expect(screen.queryByText('functions-detail')).not.toBeInTheDocument()
  177. expect(screen.queryByText('functions-list')).not.toBeInTheDocument()
  178. expect(screen.queryByText('functions-overview')).not.toBeInTheDocument()
  179. expect(screen.queryByText('logs-preview')).not.toBeInTheDocument()
  180. expect(screen.queryByText('sql-editor')).not.toBeInTheDocument()
  181. expect(screen.queryByText('storage-buckets')).not.toBeInTheDocument()
  182. expect(screen.queryByText('storage-explorer')).not.toBeInTheDocument()
  183. })
  184. it('does not show inactive database shortcuts in search results', async () => {
  185. const user = userEvent.setup()
  186. renderShortcutsReferenceSheet()
  187. await user.type(screen.getByLabelText('Search shortcuts'), 'Go to Tables')
  188. expect(screen.getByText('No matching shortcuts found')).toBeInTheDocument()
  189. expect(screen.queryByText('Database Navigation')).not.toBeInTheDocument()
  190. })
  191. it('hides shortcuts whose registration is soft-disabled', async () => {
  192. sequenceIdCounter = 0
  193. const enabled = buildSequenceRegistration(SHORTCUT_IDS.COMMAND_MENU_OPEN)
  194. const disabled = buildSequenceRegistration(SHORTCUT_IDS.NAV_HOME)
  195. disabled.options = { ...disabled.options, enabled: false }
  196. mockUseHotkeyRegistrations.mockReturnValue({
  197. hotkeys: [],
  198. sequences: [enabled, disabled],
  199. })
  200. customRender(<ShortcutsReferenceSheet open onOpenChange={vi.fn()} />)
  201. expect(await screen.findByText('Open command menu')).toBeInTheDocument()
  202. expect(screen.queryByText('Go to Project Overview')).not.toBeInTheDocument()
  203. })
  204. it('shows a clear button when searching and resets the list when clicked', async () => {
  205. const user = userEvent.setup()
  206. renderShortcutsReferenceSheet()
  207. await user.type(screen.getByLabelText('Search shortcuts'), 'navigation')
  208. expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument()
  209. await user.click(screen.getByRole('button', { name: 'Clear search' }))
  210. expect(screen.getByLabelText('Search shortcuts')).toHaveValue('')
  211. expect(screen.getByText('Command Menu')).toBeInTheDocument()
  212. expect(screen.getByText('Navigation')).toBeInTheDocument()
  213. })
  214. it.each(['⌘Esc', 'Mod+/'])('does not search shortcut values like %s', async (query) => {
  215. const user = userEvent.setup()
  216. renderShortcutsReferenceSheet()
  217. await user.type(screen.getByLabelText('Search shortcuts'), query)
  218. expect(screen.getByText('No matching shortcuts found')).toBeInTheDocument()
  219. expect(screen.queryByText('Navigation')).not.toBeInTheDocument()
  220. })
  221. it('shows an empty state when nothing matches', async () => {
  222. const user = userEvent.setup()
  223. renderShortcutsReferenceSheet()
  224. await user.type(screen.getByLabelText('Search shortcuts'), 'totally missing')
  225. expect(screen.getByText('No matching shortcuts found')).toBeInTheDocument()
  226. })
  227. })