Files
ArtPlayer/test/browser/accessibility-setting.spec.js
T

270 lines
12 KiB
JavaScript

import { expect, test } from './fixtures.js'
async function setup(page) {
await page.goto('/test/player.html?core=candidate&chapter=published')
await page.evaluate(() => {
window.settingCalls = []
window.settingHotkeys = []
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
muted: true,
hotkey: true,
setting: true,
fullscreenWeb: true,
playbackRate: false,
aspectRatio: false,
flip: false,
subtitleOffset: false,
settings: [
{ name: 'quality', html: 'Quality', tooltip: 'Auto', selector: [
{ name: 'auto', html: 'Auto', default: true },
{ name: 'hd', html: 'HD' },
{ name: 'more', html: 'More', selector: [{ name: 'ultra', html: 'Ultra' }, { name: 'cinema', html: 'Cinema' }] },
], onSelect(item, element, event) {
window.settingCalls.push(['select', item.name, this === window.art, element === item.$item, event.type])
return item.html
} },
{ name: 'loop', html: 'Loop', switch: false, onSwitch(item, element, event) {
window.settingCalls.push(['switch', this === window.art, element === item.$item, event.type])
return !item.switch
} },
{ name: 'offset', html: 'Offset', range: [0, -10, 10, 1], onChange(item, element, event) {
window.settingCalls.push(['input', item.range[0], this === window.art, element === item.$item, event.type])
return `${item.range[0]}s`
}, onRange(item, element, event) {
window.settingCalls.push(['change', item.range[0], this === window.art, element === item.$item, event.type])
return `${item.range[0]}s`
} },
{ name: 'action', html: 'Action', onClick(item, element, event) {
window.settingCalls.push(['action', this === window.art, element === item.$item, event.type])
return 'Clicked'
} },
],
})
window.art.on('hotkey', event => window.settingHotkeys.push(event.code))
})
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.locator('.art-control-setting').focus()
}
const row = (page, name) => page.locator(`.art-setting-item[data-name="${name}"]`)
const button = page => page.locator('.art-control-setting')
for (const unavailable of ['removed', 'hidden', 'disabled']) {
test(`candidate: setting exit falls back to the player when its button is ${unavailable}`, async ({ page }) => {
await setup(page)
await page.keyboard.press('Enter')
await expect(row(page, 'quality')).toBeFocused()
await page.evaluate((unavailable) => {
if (unavailable === 'removed')
window.art.controls.remove('setting')
else if (unavailable === 'hidden')
window.art.controls.setting.style.visibility = 'hidden'
else
window.art.controls.setting.setAttribute('aria-disabled', 'true')
}, unavailable)
await page.keyboard.press('Escape')
await expect(page.locator('.art-settings')).toHaveCount(1)
await expect(page.locator('.art-settings')).toBeHidden()
await expect(page.locator('.art-video-player')).toBeFocused()
})
}
test('candidate: setting navigation ignores hidden rows and inert descendants', async ({ page }) => {
await setup(page)
await page.keyboard.press('Enter')
await page.evaluate(() => {
window.art.setting.find('loop').$item.style.visibility = 'hidden'
window.art.setting.find('offset').$item.setAttribute('inert', '')
})
await page.keyboard.press('ArrowDown')
await expect(row(page, 'action')).toBeFocused()
await page.keyboard.press('ArrowUp')
await expect(row(page, 'quality')).toBeFocused()
})
test('candidate: setting keyboard enters nested panels and returns to the originating item', async ({ page }, testInfo) => {
await setup(page)
await page.evaluate(() => {
window.art.fullscreenWeb = true
})
await expect(button(page)).toBeFocused()
await page.keyboard.press('Enter')
await expect(row(page, 'quality')).toBeFocused()
await expect(button(page)).toHaveAttribute('aria-expanded', 'true')
await expect(page.locator('.art-setting-panel.art-current')).toHaveAttribute('aria-label', 'Settings')
await page.keyboard.press('ArrowRight')
await expect(row(page, 'auto')).toBeFocused()
await expect(row(page, 'auto')).toHaveAttribute('aria-current', 'true')
await page.keyboard.press('ArrowDown')
await expect(row(page, 'hd')).toBeFocused()
expect(await page.evaluate(() => window.settingCalls)).toEqual([])
await page.keyboard.press('Enter')
await expect(row(page, 'quality')).toBeFocused()
expect(await page.evaluate(() => window.settingCalls)).toEqual([['select', 'hd', true, true, 'click']])
await expect(row(page, 'quality')).toContainText('HD')
await page.keyboard.press('ArrowRight')
await expect(row(page, 'hd')).toBeFocused()
await expect(row(page, 'hd')).toHaveAttribute('aria-current', 'true')
await page.keyboard.press('End')
await expect(row(page, 'more')).toBeFocused()
await page.keyboard.press('ArrowRight')
await expect(row(page, 'ultra')).toBeFocused()
await page.keyboard.press('ArrowLeft')
await expect(row(page, 'more')).toBeFocused()
await page.keyboard.press('Home')
await expect(page.locator('.art-setting-panel.art-current .art-setting-item-back')).toBeFocused()
await expect(page.locator('.art-setting-panel.art-current .art-setting-item-back')).toHaveAttribute('aria-label', 'Back: Quality')
await page.keyboard.press('Space')
await expect(row(page, 'quality')).toBeFocused()
await page.locator('.art-video-player').screenshot({ path: testInfo.outputPath('setting-keyboard.png') })
await page.keyboard.press('Escape')
await expect(button(page)).toBeFocused()
await expect(button(page)).toHaveAttribute('aria-expanded', 'false')
expect(await page.evaluate(() => window.art.fullscreenWeb)).toBe(true)
expect(await page.evaluate(() => window.settingHotkeys)).toEqual([])
await page.keyboard.press('Escape')
await expect.poll(() => page.evaluate(() => window.art.fullscreenWeb)).toBe(false)
})
test('candidate: setting switches and native ranges keep callbacks and avoid player shortcuts', async ({ page }) => {
await setup(page)
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await expect(row(page, 'loop')).toBeFocused()
await expect(row(page, 'loop')).toHaveAttribute('role', 'switch')
await expect(row(page, 'loop')).toHaveAttribute('aria-checked', 'false')
await page.keyboard.down('Space')
await page.keyboard.down('Space')
await page.keyboard.up('Space')
await expect(row(page, 'loop')).toHaveAttribute('aria-checked', 'true')
expect(await page.evaluate(() => window.settingCalls)).toEqual([['switch', true, true, 'click']])
await page.keyboard.press('Tab')
const range = row(page, 'offset').locator('input')
await expect(range).toBeFocused()
await expect(range).toHaveAttribute('aria-label', 'Offset')
await page.keyboard.press('ArrowRight')
await expect(range).toHaveValue('1')
await expect.poll(() => page.evaluate(() => window.settingCalls.filter(item => ['input', 'change'].includes(item[0])))).toEqual([
['input', 1, true, true, 'input'],
['change', 1, true, true, 'change'],
])
await page.evaluate(() => {
window.art.setting.find('offset').html = 'Subtitle delay'
})
await expect(range).toHaveAttribute('aria-label', 'Subtitle delay')
expect(await page.evaluate(() => [window.art.currentTime, window.art.volume, window.art.video.paused])).toEqual([0, 0.7, true])
expect(await page.evaluate(() => window.settingHotkeys)).toEqual([])
await page.keyboard.press('Escape')
await expect(button(page)).toBeFocused()
})
test('candidate: setting Tab leaves without trapping focus and pointer opening keeps its old focus path', async ({ page }) => {
await setup(page)
await page.evaluate(() => {
window.settingFocusOrder = []
window.art.template.$setting.addEventListener('focusin', event => window.settingFocusOrder.push(event.target.dataset.name))
})
await page.keyboard.press('ArrowUp')
await expect(row(page, 'action')).toBeFocused()
expect(await page.evaluate(() => window.settingFocusOrder)).toEqual(['action'])
await page.keyboard.press('Tab')
await expect.poll(() => page.evaluate(() => window.art.setting.show)).toBe(false)
await expect(row(page, 'action')).not.toBeFocused()
await button(page).click()
await expect.poll(() => page.evaluate(() => window.art.setting.show)).toBe(true)
await expect(row(page, 'quality')).not.toBeFocused()
await row(page, 'action').click()
expect(await page.evaluate(() => window.settingCalls)).toEqual([['action', true, true, 'click']])
})
test('candidate: setting update, failed replacement and removal recover visible focus', async ({ page }) => {
await setup(page)
await page.keyboard.press('ArrowUp')
await expect(row(page, 'action')).toBeFocused()
await page.evaluate(() => window.art.setting.update({ name: 'action', html: 'Updated' }))
await expect(row(page, 'action')).toBeFocused()
await expect(row(page, 'action')).toHaveText('Updated')
await page.evaluate(() => {
const setting = window.art.setting
const create = setting.createItem
setting.createItem = function (...args) {
create.apply(this, args)
throw new Error('forced keyboard replacement failure')
}
try {
setting.update({ name: 'action', html: 'Failed' })
}
catch (error) {
window.settingUpdateFailure = error.message
}
finally {
setting.createItem = create
}
})
expect(await page.evaluate(() => window.settingUpdateFailure)).toBe('forced keyboard replacement failure')
await expect(row(page, 'action')).toBeFocused()
await expect(row(page, 'action')).toHaveText('Updated')
await page.keyboard.press('Enter')
expect(await page.evaluate(() => window.settingCalls)).toEqual([['action', true, true, 'click']])
await page.evaluate(() => window.art.setting.remove('action'))
await expect(row(page, 'quality')).toBeFocused()
})
test('candidate: suspended settings ignore keyboard callbacks and restored items resume', async ({ page }) => {
await setup(page)
await page.keyboard.press('ArrowUp')
await page.evaluate(() => {
const target = { name: 'action' }
Object.defineProperty(target, 'html', { enumerable: true, get() {
const item = window.art.setting.find('action').$item
item.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
item.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, cancelable: true }))
throw new Error('resume old keyboard item')
} })
try {
window.art.setting.update(target)
}
catch {}
})
expect(await page.evaluate(() => window.settingCalls)).toEqual([])
expect(await page.evaluate(() => window.art.currentTime)).toBe(0)
await expect(row(page, 'action')).toBeFocused()
await page.keyboard.press('Enter')
expect(await page.evaluate(() => window.settingCalls)).toEqual([['action', true, true, 'click']])
})
test('candidate: native setting buttons retain their role, click path and keyboard navigation', async ({ page }) => {
await setup(page)
await page.evaluate(() => {
window.art.setting.add({ name: 'native', html: '<button id="native-setting">Native action</button>', onClick(item, element, event) {
window.settingCalls.push(['native', this === window.art, element === item.$item, event.type])
return 'Done'
} })
})
await page.keyboard.press('ArrowUp')
const native = page.locator('#native-setting')
await expect(native).toBeFocused()
expect(await row(page, 'native').getAttribute('role')).toBeNull()
await page.keyboard.press('Space')
expect(await page.evaluate(() => window.settingCalls)).toEqual([['native', true, true, 'click']])
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
await page.keyboard.press('ArrowUp')
await expect(row(page, 'action')).toBeFocused()
expect(await page.evaluate(() => window.settingHotkeys)).toEqual([])
})
test('candidate: destroying during moved focus restoration cancels fullscreen notifications', async ({ page }) => {
await setup(page)
await page.evaluate(() => {
window.modeEvents = []
window.art.on('fullscreenWeb', value => window.modeEvents.push(['fullscreenWeb', value]))
window.art.on('resize', () => window.modeEvents.push(['resize']))
window.art.controls.setting.addEventListener('focus', () => window.art.destroy(false), { once: true })
window.art.fullscreenWeb = true
})
expect(await page.evaluate(() => window.art.isDestroy)).toBe(true)
expect(await page.evaluate(() => window.modeEvents)).toEqual([])
})