import { describe, expect, it } from 'vitest'; import { DEFAULT_VIEWER_SHORTCUTS, describeViewerShortcut, formatShortcutChord, groupViewerShortcuts, isEditableEventTarget, isInteractiveEventTarget, matchViewerShortcut, shouldIgnoreViewerKeyboardEvent, } from './viewerKeyboardShortcuts'; import type { KeyboardEventLike, ViewerShortcutDefinition, } from './viewerKeyboardShortcuts'; function keyEvent(overrides: Partial): KeyboardEventLike { return { altKey: false, code: '', ctrlKey: false, defaultPrevented: false, key: '', metaKey: false, repeat: false, shiftKey: false, target: null, ...overrides, }; } describe('viewerKeyboardShortcuts', () => { it('matches unmodified default viewer commands', () => { expect(matchViewerShortcut(keyEvent({ key: 'R' }))?.command).toBe('resetCamera'); expect(matchViewerShortcut(keyEvent({ key: 'f' }))?.command).toBe('fitCamera'); expect(matchViewerShortcut(keyEvent({ key: ' ' }))?.command).toBe('togglePlayback'); }); it('does not steal browser or assistive modifier chords by default', () => { expect(matchViewerShortcut(keyEvent({ key: 'r', ctrlKey: true }))).toBeNull(); expect(matchViewerShortcut(keyEvent({ key: 'r', metaKey: true }))).toBeNull(); expect(matchViewerShortcut(keyEvent({ key: 'f', altKey: true }))).toBeNull(); }); it('supports repeat only for shortcuts that explicitly opt in', () => { expect(matchViewerShortcut(keyEvent({ key: 'w', repeat: true }))).toBeNull(); expect(matchViewerShortcut(keyEvent({ key: ']', repeat: true }))?.command).toBe( 'increaseExplosion', ); expect(matchViewerShortcut(keyEvent({ key: '[', repeat: true }))?.command).toBe( 'decreaseExplosion', ); }); it('matches platform modifier shortcuts when requested by a custom definition', () => { const shortcuts: readonly ViewerShortcutDefinition[] = [ { command: 'fitCamera', group: 'navigation', label: 'Fit model', chords: [{ key: 'f', modKey: true }], }, ]; expect(matchViewerShortcut(keyEvent({ key: 'f', ctrlKey: true }), shortcuts)?.command).toBe( 'fitCamera', ); expect(matchViewerShortcut(keyEvent({ key: 'f', metaKey: true }), shortcuts)?.command).toBe( 'fitCamera', ); expect(matchViewerShortcut(keyEvent({ key: 'f' }), shortcuts)).toBeNull(); }); it('formats shortcut chords for help UI text', () => { expect(formatShortcutChord({ key: 's', modKey: true }, 'mac')).toBe('⌘ + S'); expect(formatShortcutChord({ key: 's', modKey: true }, 'windows')).toBe('Ctrl + S'); expect(formatShortcutChord({ key: '?', shiftKey: true }, 'windows')).toBe('Shift + ?'); expect( describeViewerShortcut( { command: 'showKeyboardHelp', group: 'help', label: 'Help', chords: [{ key: '?', shiftKey: true }, { key: 'F1' }], }, 'windows', ), ).toBe('Shift + ? / F1'); }); it('groups shortcuts in a stable product order', () => { const grouped = groupViewerShortcuts([ { command: 'showKeyboardHelp', group: 'help', label: 'Help', chords: [{ key: '?' }], }, { command: 'resetCamera', group: 'navigation', label: 'Reset', chords: [{ key: 'r' }], }, ]); expect(grouped.map((group) => group.id)).toEqual(['navigation', 'help']); }); it('keeps the shipped default shortcut catalogue free of duplicate command ids', () => { const commandIds = DEFAULT_VIEWER_SHORTCUTS.map((shortcut) => shortcut.command); expect(new Set(commandIds).size).toBe(commandIds.length); }); const domIt = typeof document === 'undefined' ? it.skip : it; domIt('ignores editable fields and native interactive controls', () => { const input = document.createElement('input'); const button = document.createElement('button'); expect(isEditableEventTarget(input)).toBe(true); expect(isInteractiveEventTarget(input)).toBe(true); expect(isInteractiveEventTarget(button)).toBe(true); expect(matchViewerShortcut(keyEvent({ key: ' ', target: input }))).toBeNull(); expect(matchViewerShortcut(keyEvent({ key: 'r', target: button }))).toBeNull(); }); domIt('honors explicit shortcut allow and ignore attributes', () => { const ignoredPanel = document.createElement('div'); const ignoredChild = document.createElement('span'); ignoredPanel.setAttribute('data-viewer-shortcuts', 'ignore'); ignoredPanel.append(ignoredChild); expect(shouldIgnoreViewerKeyboardEvent(keyEvent({ key: 'r', target: ignoredChild }))).toBe( true, ); const button = document.createElement('button'); button.setAttribute('data-viewer-shortcuts', 'allow'); expect(matchViewerShortcut(keyEvent({ key: 'r', target: button }))?.command).toBe( 'resetCamera', ); }); });