test(quick-261022-k9k): keyboard shortcut coverage

- hotkey core: combos, AltGr and layouts, labels, aria-keyshortcuts, editable
  targets, modal root, refcounted install, disabled and repeat
- smoke: record, create, Create and new (new session key), Esc, modal scope,
  settings Save and close, My account, list search and the help dialog
- ShortcutHelpDialog rows, keys and close button
This commit is contained in:
Jakub Zych
2026-10-22 21:07:40 +02:00
parent 66bb96eebd
commit 058738efdf
4 changed files with 760 additions and 12 deletions

View File

@@ -0,0 +1,307 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
ariaKeyshortcuts,
availableShortcuts,
closeShortcutHelp,
comboOf,
hotkeyRoot,
installHotkeys,
isEditableTarget,
isMacPlatform,
shortcutHelp,
shortcutHint,
shortcutLabel,
shortcutParts,
} from '../../src/app/hotkeys'
import { setBundle } from '../../src/app/i18n'
import lang from '../fixtures/lang.json'
import { press } from '../helpers'
const key = (init: KeyboardEventInit) => new KeyboardEvent('keydown', init)
let uninstalls: Array<() => void> = []
function install(): () => void {
const uninstall = installHotkeys()
uninstalls.push(uninstall)
return uninstall
}
function button(hotkey: string, parent: ParentNode = document.body): HTMLButtonElement {
const element = document.createElement('button')
element.setAttribute('data-hotkey', hotkey)
parent.appendChild(element)
return element
}
function dialog(attrs: Record<string, string>): HTMLDivElement {
const element = document.createElement('div')
for (const [name, value] of Object.entries(attrs)) {
element.setAttribute(name, value)
}
document.body.appendChild(element)
return element
}
beforeEach(() => {
setBundle(lang.data, 'pl')
})
afterEach(() => {
for (const uninstall of uninstalls) {
uninstall()
}
uninstalls = []
closeShortcutHelp()
document.body.innerHTML = ''
})
describe('comboOf', () => {
it('maps the modifier combos with Ctrl or Cmd', () => {
expect(comboOf(key({ key: 's', ctrlKey: true }))).toBe('mod+s')
expect(comboOf(key({ key: 's', metaKey: true }))).toBe('mod+s')
expect(comboOf(key({ key: 'S', ctrlKey: true, shiftKey: true }))).toBe('mod+shift+s')
expect(comboOf(key({ key: 'Enter', ctrlKey: true }))).toBe('mod+enter')
expect(comboOf(key({ key: 'Enter', metaKey: true }))).toBe('mod+enter')
})
it('maps the plain keys whatever the shift state', () => {
expect(comboOf(key({ key: 'Escape' }))).toBe('escape')
expect(comboOf(key({ key: '/' }))).toBe('/')
expect(comboOf(key({ key: '?', shiftKey: true }))).toBe('?')
})
it('ignores AltGr, Alt, plain letters, Ctrl+Shift+Enter and composition', () => {
expect(comboOf(key({ key: 's', ctrlKey: true, altKey: true }))).toBeNull()
expect(comboOf(key({ key: 's', altKey: true }))).toBeNull()
expect(comboOf(key({ key: 's' }))).toBeNull()
expect(comboOf(key({ key: 'Enter', ctrlKey: true, shiftKey: true }))).toBeNull()
expect(comboOf(key({ key: 's', ctrlKey: true, isComposing: true }))).toBeNull()
})
it('reads the physical key of a non-Latin layout', () => {
expect(comboOf(key({ key: 'ы', code: 'KeyS', ctrlKey: true }))).toBe('mod+s')
expect(comboOf(key({ key: 'ы', code: 'KeyD', ctrlKey: true }))).toBeNull()
})
})
describe('labels', () => {
it('detects Apple platforms', () => {
expect(isMacPlatform('MacIntel')).toBe(true)
expect(isMacPlatform('iPhone')).toBe(true)
expect(isMacPlatform('iPad')).toBe(true)
expect(isMacPlatform('Linux x86_64')).toBe(false)
expect(isMacPlatform('Win32')).toBe(false)
})
it('reads navigator.platform by default', () => {
vi.spyOn(navigator, 'platform', 'get').mockReturnValue('MacIntel')
expect(isMacPlatform()).toBe(true)
expect(shortcutLabel('mod+s')).toBe('⌘S')
})
it('names the keys with words on Linux and glyphs on macOS', () => {
expect(shortcutLabel('mod+s', false)).toBe('Ctrl+S')
expect(shortcutLabel('mod+enter', false)).toBe('Ctrl+Enter')
expect(shortcutLabel('mod+shift+s', false)).toBe('Ctrl+Shift+S')
expect(shortcutLabel('escape', false)).toBe('Esc')
expect(shortcutLabel('mod+s', true)).toBe('⌘S')
expect(shortcutLabel('mod+enter', true)).toBe('⌘↩')
expect(shortcutLabel('mod+shift+s', true)).toBe('⇧⌘S')
expect(shortcutParts('mod+shift+s', true)).toEqual(['⇧', '⌘', 'S'])
expect(shortcutParts('/', false)).toEqual(['/'])
expect(shortcutParts('?', true)).toEqual(['?'])
})
it('builds the tooltip hint from the phrasebook string', () => {
expect(shortcutHint('Zapisz', 'mod+s', false)).toBe('Zapisz (Ctrl+S)')
expect(shortcutHint('Zapisz', 'mod+s', true)).toBe('Zapisz (⌘S)')
})
it('lists the Control and Meta forms for aria-keyshortcuts', () => {
expect(ariaKeyshortcuts('mod+s mod+enter')).toBe('Control+S Meta+S Control+Enter Meta+Enter')
expect(ariaKeyshortcuts('mod+shift+s')).toBe('Control+Shift+S Meta+Shift+S')
expect(ariaKeyshortcuts('escape')).toBe('Escape')
expect(ariaKeyshortcuts('/')).toBe('/')
})
})
describe('isEditableTarget', () => {
it('is true inside inputs, textareas, selects and contenteditable', () => {
document.body.innerHTML = `
<input id="i"><textarea id="t"></textarea><select id="s"></select>
<div contenteditable="true"><span id="c">x</span></div>
<div contenteditable="false"><span id="n">x</span></div>
<button id="b"></button>`
for (const id of ['i', 't', 's', 'c']) {
expect(isEditableTarget(document.getElementById(id))).toBe(true)
}
expect(isEditableTarget(document.getElementById('b'))).toBe(false)
expect(isEditableTarget(document.getElementById('n'))).toBe(false)
expect(isEditableTarget(document.body)).toBe(false)
expect(isEditableTarget(null)).toBe(false)
})
})
describe('hotkeyRoot', () => {
it('is the document without a modal', () => {
expect(hotkeyRoot(document)).toBe(document)
})
it('is the last open aria-modal dialog or alert dialog', () => {
dialog({ role: 'dialog', 'aria-modal': 'true', 'data-state': 'open' })
const second = dialog({ role: 'dialog', 'aria-modal': 'true', 'data-state': 'open' })
expect(hotkeyRoot(document)).toBe(second)
const alert = dialog({ role: 'alertdialog', 'data-state': 'open' })
expect(hotkeyRoot(document)).toBe(alert)
})
it('ignores popovers (no aria-modal) and closing dialogs', () => {
const modal = dialog({ role: 'dialog', 'aria-modal': 'true' })
dialog({ role: 'dialog' })
dialog({ role: 'dialog', 'aria-modal': 'true', 'data-state': 'closed' })
expect(hotkeyRoot(document)).toBe(modal)
})
})
describe('availableShortcuts', () => {
it('lists the targets of the current root plus ?', () => {
button('mod+s')
button('escape')
expect(availableShortcuts().map((entry) => entry.combo)).toEqual(['mod+s', 'escape', '?'])
const modal = dialog({ role: 'dialog', 'aria-modal': 'true' })
button('mod+s mod+enter', modal)
expect(availableShortcuts().map((entry) => entry.combo)).toEqual(['mod+s', 'mod+enter', '?'])
})
})
describe('installHotkeys', () => {
it('clicks the target once per keydown however often it is installed, and stops after every uninstall', () => {
const save = button('mod+s')
const click = vi.spyOn(save, 'click')
const first = install()
const second = install()
press(document.body, 's', { ctrlKey: true })
expect(click).toHaveBeenCalledTimes(1)
first()
first()
press(document.body, 's', { ctrlKey: true })
expect(click).toHaveBeenCalledTimes(2)
second()
const event = press(document.body, 's', { ctrlKey: true })
expect(click).toHaveBeenCalledTimes(2)
expect(event.defaultPrevented).toBe(false)
})
it('consumes the key without clicking a disabled target or on auto-repeat', () => {
install()
const save = button('mod+s')
const click = vi.spyOn(save, 'click')
expect(press(document.body, 's', { ctrlKey: true, repeat: true }).defaultPrevented).toBe(true)
expect(click).not.toHaveBeenCalled()
save.disabled = true
expect(press(document.body, 's', { ctrlKey: true }).defaultPrevented).toBe(true)
expect(click).not.toHaveBeenCalled()
save.disabled = false
save.setAttribute('aria-disabled', 'true')
press(document.body, 's', { ctrlKey: true })
expect(click).not.toHaveBeenCalled()
})
it('prevents the browser Save page without a target, and lets other modifier keys through', () => {
install()
expect(press(document.body, 's', { ctrlKey: true }).defaultPrevented).toBe(true)
expect(press(document.body, 'Enter', { ctrlKey: true }).defaultPrevented).toBe(false)
expect(press(document.body, 'S', { ctrlKey: true, shiftKey: true }).defaultPrevented).toBe(false)
})
it('stops a handled modifier key before an editor sees it, also inside an editable control', () => {
install()
const save = button('mod+enter')
const click = vi.spyOn(save, 'click')
const editor = document.createElement('div')
editor.setAttribute('contenteditable', 'true')
document.body.appendChild(editor)
const editorKeys = vi.fn()
editor.addEventListener('keydown', editorKeys)
const event = press(editor, 'Enter', { ctrlKey: true })
expect(event.defaultPrevented).toBe(true)
expect(click).toHaveBeenCalledTimes(1)
expect(editorKeys).not.toHaveBeenCalled()
})
it('searches only the innermost modal', () => {
install()
const page = button('mod+s')
const pageClick = vi.spyOn(page, 'click')
const modal = dialog({ role: 'dialog', 'aria-modal': 'true', 'data-state': 'open' })
const submit = button('mod+s mod+enter', modal)
const submitClick = vi.spyOn(submit, 'click')
press(document.body, 's', { ctrlKey: true })
press(document.body, 'Enter', { metaKey: true })
expect(submitClick).toHaveBeenCalledTimes(2)
expect(pageClick).not.toHaveBeenCalled()
dialog({ role: 'alertdialog', 'data-state': 'open' })
expect(press(document.body, 's', { ctrlKey: true }).defaultPrevented).toBe(true)
expect(submitClick).toHaveBeenCalledTimes(2)
expect(pageClick).not.toHaveBeenCalled()
})
it('runs plain keys outside editable controls and open layers only', () => {
install()
const cancel = button('escape')
const click = vi.spyOn(cancel, 'click')
const input = document.createElement('input')
document.body.appendChild(input)
expect(press(input, 'Escape').defaultPrevented).toBe(false)
expect(click).not.toHaveBeenCalled()
expect(press(document.body, 'Escape').defaultPrevented).toBe(true)
expect(click).toHaveBeenCalledTimes(1)
const handled = document.createElement('div')
document.body.appendChild(handled)
handled.addEventListener('keydown', (event) => event.preventDefault())
press(handled, 'Escape')
expect(click).toHaveBeenCalledTimes(1)
const layer = dialog({ 'data-dismissable-layer': '' })
press(document.body, 'Escape')
expect(click).toHaveBeenCalledTimes(1)
layer.remove()
cancel.disabled = true
expect(press(document.body, 'Escape').defaultPrevented).toBe(false)
})
it('focuses an input target and opens the help on ?', () => {
install()
const search = document.createElement('input')
search.setAttribute('data-hotkey', '/')
search.value = 'abc'
document.body.appendChild(search)
button('mod+s')
expect(press(document.body, '/').defaultPrevented).toBe(true)
expect(document.activeElement).toBe(search)
expect(press(search, '?', { shiftKey: true }).defaultPrevented).toBe(false)
expect(shortcutHelp.value).toBeNull()
search.blur()
expect(press(document.body, '?', { shiftKey: true }).defaultPrevented).toBe(true)
expect(shortcutHelp.value?.map((entry) => entry.combo)).toEqual(['mod+s', '/', '?'])
closeShortcutHelp()
expect(shortcutHelp.value).toBeNull()
})
})