Files
ArtPlayer/test/accessibility-button.test.js

130 lines
4.7 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Exercise owned keyboard interactions with the repository runner.
import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const { keyboardButton, isClaimedKey, acceptsHotkey, ResourceScope } = await loadModules({
keyboardButton: { file: 'packages/artplayer/src/accessibility/button', name: 'keyboardButton' },
isClaimedKey: { file: 'packages/artplayer/src/accessibility/keyboard', name: 'isClaimedKey' },
acceptsHotkey: { file: 'packages/artplayer/src/input/keyboard-focus', name: 'acceptsHotkey' },
ResourceScope: 'packages/artplayer/src/lifecycle/scope',
})
class Button extends EventTarget {
attributes = new Map()
ownerDocument = { activeElement: this }
nodeType = 1
tagName = 'DIV'
isContentEditable = false
hits = 0
getAttribute(key) { return this.attributes.get(key) ?? null }
hasAttribute(key) { return this.attributes.has(key) }
setAttribute(key, value) { this.attributes.set(key, String(value)) }
click() { this.hits++ }
}
function fixture() {
const scope = new ResourceScope()
const button = new Button()
keyboardButton(scope, button)
const key = (type, value, options = {}) => {
const event = Object.assign(new Event(type, { cancelable: true }), { key: value, ...options })
button.dispatchEvent(event)
return event
}
return { scope, button, key }
}
test('buttons activate Enter on press and Space on release without key-repeat duplicates', () => {
const { scope, button, key } = fixture()
assert.equal(button.getAttribute('role'), 'button')
assert.equal(button.tabIndex, 0)
assert.equal(key('keydown', 'Enter').defaultPrevented, true)
key('keydown', 'Enter', { repeat: true })
key('keyup', 'Enter')
assert.equal(button.hits, 1)
key('keydown', ' ')
key('keydown', ' ', { repeat: true })
assert.equal(button.hits, 1)
key('keyup', ' ')
key('keyup', ' ')
assert.equal(button.hits, 2)
scope.dispose()
})
test('owned button keys avoid hotkeys while unrelated prevented events keep the legacy policy', () => {
const { scope, button, key } = fixture()
const owned = key('keydown', ' ')
assert.equal(isClaimedKey(owned), true)
assert.equal(acceptsHotkey(owned, button.ownerDocument), false)
const external = new Event('keydown', { cancelable: true })
external.preventDefault()
assert.equal(isClaimedKey(external), false)
assert.equal(acceptsHotkey(external, button.ownerDocument), true)
scope.dispose()
})
test('blur, modifiers, composition and disabled state cancel button activation', () => {
const { scope, button, key } = fixture()
key('keydown', ' ')
button.dispatchEvent(new Event('blur'))
key('keyup', ' ')
for (const options of [{ ctrlKey: true }, { altKey: true }, { metaKey: true }, { isComposing: true }]) {
key('keydown', 'Enter', options)
key('keydown', ' ', options)
key('keyup', ' ', options)
}
button.setAttribute('aria-disabled', 'true')
assert.equal(isClaimedKey(key('keydown', 'Enter')), true)
key('keydown', ' ')
key('keyup', ' ')
assert.equal(button.hits, 0)
scope.dispose()
})
test('disposal removes button listeners and invalidates an armed Space action', () => {
const { scope, button, key } = fixture()
key('keydown', ' ')
scope.dispose()
assert.equal(key('keyup', ' ').defaultPrevented, false)
assert.equal(key('keydown', 'Enter').defaultPrevented, false)
assert.equal(button.hits, 0)
})
test('suspended button scopes cannot activate or arm Space until resumed', () => {
const scope = new ResourceScope()
const button = new Button()
let active = false
keyboardButton(scope, button, () => button.click(), () => active)
const key = (type, value) => button.dispatchEvent(Object.assign(new Event(type, { cancelable: true }), { key: value }))
key('keydown', 'Enter')
key('keydown', ' ')
active = true
key('keyup', ' ')
assert.equal(button.hits, 0)
key('keydown', ' ')
active = false
key('keyup', ' ')
active = true
key('keyup', ' ')
assert.equal(button.hits, 0)
key('keydown', 'Enter')
assert.equal(button.hits, 1)
scope.dispose()
})
test('button setup preserves caller role and tab order and ignores descendant key events', () => {
const scope = new ResourceScope()
const button = new Button()
button.setAttribute('role', 'switch')
button.setAttribute('tabindex', '-1')
keyboardButton(scope, button)
assert.equal(button.getAttribute('role'), 'switch')
assert.equal(button.getAttribute('tabindex'), '-1')
const event = Object.assign(new Event('keydown', { cancelable: true }), { key: 'Enter', composedPath: () => [new Button(), button] })
button.dispatchEvent(event)
assert.equal(event.defaultPrevented, false)
assert.equal(button.hits, 0)
scope.dispose()
})