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