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

111 lines
3.7 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Verify focus modality ownership and document rebinding.
import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const { focusVisibility, captureMovedFocus, ResourceScope } = await loadModules({
focusVisibility: { file: 'packages/artplayer/src/accessibility/focus', name: 'focusVisibility' },
captureMovedFocus: { file: 'packages/artplayer/src/accessibility/moved-focus', name: 'captureMovedFocus' },
ResourceScope: 'packages/artplayer/src/lifecycle/scope',
})
test('DOM moves restore the same connected descendant and preserve externally moved focus', () => {
const body = {}
const outside = {}
const doc = { body, activeElement: null }
let count = 0
const child = { ownerDocument: doc, isConnected: true, focus(options) {
assert.deepEqual(options, { preventScroll: true })
count++
doc.activeElement = this
} }
const root = { ownerDocument: doc, contains: target => target === child }
doc.activeElement = child
const restore = captureMovedFocus(root)
doc.activeElement = body
restore()
assert.equal(doc.activeElement, child)
restore()
assert.equal(count, 1)
doc.activeElement = outside
restore()
assert.equal(doc.activeElement, outside)
doc.activeElement = body
child.isConnected = false
restore()
assert.equal(count, 1)
child.isConnected = true
doc.activeElement = outside
const unrelated = captureMovedFocus(root)
doc.activeElement = body
unrelated()
assert.equal(count, 1)
})
function fixture() {
const scope = new ResourceScope()
const doc = Object.assign(new EventTarget(), { activeElement: null })
const classes = new Set()
const child = {}
const root = Object.assign(new EventTarget(), {
ownerDocument: doc,
contains: value => value === child,
classList: {
contains: name => classes.has(name),
remove: name => classes.delete(name),
toggle: (name, active) => active ? classes.add(name) : classes.delete(name),
},
})
let shows = 0
const focused = focusVisibility(scope, root, () => shows++)
const focus = () => {
root.ownerDocument.activeElement = child
root.dispatchEvent(new Event('focusin'))
}
return { scope, doc, root, child, classes, focused, focus, shows: () => shows }
}
test('keyboard visibility follows actual focus without making pointer focus sticky', () => {
const f = fixture()
assert.equal(f.focused(), false)
f.focus()
assert.equal(f.focused(), true)
assert.equal(f.shows(), 1)
f.doc.dispatchEvent(new Event('mousedown'))
assert.equal(f.focused(), false)
assert.equal(f.classes.size, 0)
f.doc.dispatchEvent(new Event('keydown'))
assert.equal(f.focused(), true)
assert.equal(f.shows(), 2)
f.doc.dispatchEvent(new Event('keydown'))
assert.equal(f.shows(), 2)
f.doc.activeElement = null
f.root.dispatchEvent(new Event('focusout'))
assert.equal(f.focused(), false)
assert.equal(f.classes.size, 0)
f.scope.dispose()
})
test('moving a focused player rebinds modality listeners and disposal releases both documents', () => {
const f = fixture()
f.focus()
const next = Object.assign(new EventTarget(), { activeElement: null })
f.root.ownerDocument = next
f.focus()
f.doc.dispatchEvent(new Event('mousedown'))
assert.equal(f.focused(), true)
next.dispatchEvent(new Event('touchstart'))
assert.equal(f.focused(), false)
next.dispatchEvent(new Event('keydown'))
assert.equal(f.focused(), true)
const shows = f.shows()
f.scope.dispose()
assert.equal(f.focused(), false)
assert.equal(f.classes.size, 0)
for (const target of [f.doc, next, f.root])
target.dispatchEvent(new Event('keydown'))
f.focus()
assert.equal(f.shows(), shows)
assert.equal(f.classes.size, 0)
})