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

162 lines
7.4 KiB
JavaScript

import { expect, test } from './fixtures.js'
async function setup(page, core = 'candidate') {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
await page.evaluate(() => {
const before = document.createElement('button')
before.id = 'before-player'
before.textContent = 'Before player'
document.body.prepend(before)
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, hotkey: true, setting: true, fullscreenWeb: true, screenshot: true })
window.keyboardHits = []
window.art.on('hotkey', event => window.keyboardHits.push(event.code))
})
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.locator('#before-player').focus()
}
async function tabPastNativeVideo(page) {
await page.keyboard.press('Tab')
// Firefox includes the native video in its existing sequential focus order.
if (await page.locator('.art-video').evaluate(element => element.ownerDocument.activeElement === element))
await page.keyboard.press('Tab')
}
async function tabToPlay(page) {
await tabPastNativeVideo(page)
await expect(page.locator('.art-control-progress')).toBeFocused()
await page.keyboard.press('Tab')
}
test('published: Tab skips the non-interactive main controls', async ({ page }) => {
await setup(page, 'published')
await tabPastNativeVideo(page)
await expect(page.locator('#play')).toBeFocused()
expect(await page.locator('.art-controls [tabindex="0"]').count()).toBe(0)
})
test('candidate: keyboard reaches stable play control and toggles once with focus retained', async ({ page }) => {
await setup(page)
await tabToPlay(page)
const play = page.locator('.art-control-playAndPause')
await expect(play).toBeFocused()
await expect(play).toHaveAttribute('role', 'button')
await expect(play).toHaveAttribute('aria-label', 'Play')
await page.keyboard.press('Enter')
await expect.poll(() => page.evaluate(() => window.art.playing && window.art.currentTime > 0.1)).toBe(true)
await expect(play).toBeFocused()
await expect(play).toHaveAttribute('aria-label', 'Pause')
await page.keyboard.down('Space')
expect(await page.evaluate(() => window.art.playing)).toBe(true)
await page.keyboard.up('Space')
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
await expect(play).toBeFocused()
expect(await page.evaluate(() => window.keyboardHits)).toEqual([])
await page.keyboard.press('Tab')
await expect(page.locator('.art-control-volume .art-icon-volumeClose')).toBeFocused()
})
test('candidate: mute state, focus visibility and automatic hiding follow keyboard focus', async ({ page }) => {
await setup(page)
await tabToPlay(page)
await page.keyboard.press('Enter')
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
await page.evaluate(() => {
window.Artplayer.CONTROL_HIDE_TIME = 0
})
await page.keyboard.press('Tab')
const muted = page.locator('.art-control-volume .art-icon-volumeClose')
const audible = page.locator('.art-control-volume .art-icon-volume')
await expect(muted).toBeFocused()
await expect(muted).toHaveAttribute('aria-label', 'Mute')
await expect(muted).toHaveAttribute('aria-pressed', 'true')
await page.keyboard.press('Space')
await expect(audible).toBeFocused()
await expect(audible).toHaveAttribute('aria-pressed', 'false')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.5)
expect(await page.evaluate(() => window.art.controls.show)).toBe(true)
expect(await audible.evaluate(element => getComputedStyle(element).outlineStyle)).not.toBe('none')
expect(await page.evaluate(() => window.keyboardHits)).toEqual([])
await page.locator('#before-player').focus()
await expect.poll(() => page.evaluate(() => window.art.controls.show)).toBe(false)
expect(await page.evaluate(() => window.art.isFocus)).toBe(false)
})
test('candidate: custom control keyboard clicks retain the callback and release on replacement', async ({ page }) => {
await setup(page)
await page.evaluate(() => {
const art = window.art
window.customHits = []
art.controls.add({ name: 'keyboard-custom', position: 'right', html: 'Custom', click(component, event) {
window.customHits.push({ same: this === art, component: component === art.controls, type: event.type })
} })
})
const control = page.locator('.art-control-keyboard-custom')
await control.focus()
await page.keyboard.press('Enter')
await page.keyboard.down('Space')
await page.keyboard.down('Space')
await page.keyboard.up('Space')
expect(await page.evaluate(() => window.customHits)).toEqual([
{ same: true, component: true, type: 'click' },
{ same: true, component: true, type: 'click' },
])
await page.keyboard.down('Space')
await page.evaluate(() => {
const art = window.art
window.oldControl = art.controls['keyboard-custom']
art.controls.update({ name: 'keyboard-custom', html: 'Replaced' })
})
await page.keyboard.up('Space')
await page.evaluate(() => {
window.oldControl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
})
expect(await page.evaluate(() => window.customHits.length)).toBe(2)
await control.focus()
await page.keyboard.press('Enter')
expect(await page.evaluate(() => window.customHits.length)).toBe(3)
await page.evaluate(() => window.art.destroy(false))
expect(await page.evaluate(() => {
window.oldControl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
return window.customHits.length
})).toBe(3)
})
test('candidate: native buttons inside custom controls retain activation without player hotkeys', async ({ page }) => {
await setup(page)
await page.evaluate(() => {
const art = window.art
window.nativeButtonHits = 0
window.parentButtonHits = 0
const button = document.createElement('button')
button.textContent = 'Native action'
button.addEventListener('click', () => window.nativeButtonHits++)
art.controls.add({ name: 'native-action', position: 'right', html: button, click() {
window.parentButtonHits++
} })
art.hotkey.add('KeyK', () => window.keyboardHits.push('custom-k'))
})
const parent = page.locator('.art-control-native-action')
const button = parent.locator('button')
await expect(parent).not.toHaveAttribute('role', 'button')
await button.focus()
await page.keyboard.press('Space')
await page.keyboard.press('Enter')
expect(await page.evaluate(() => ({ native: window.nativeButtonHits, parent: window.parentButtonHits, paused: window.art.video.paused, hotkeys: window.keyboardHits }))).toEqual({ native: 2, parent: 2, paused: true, hotkeys: [] })
await page.keyboard.press('k')
expect(await page.evaluate(() => window.keyboardHits)).toEqual(['custom-k', 'KeyK'])
})
test('candidate: pointer focus does not prevent the existing automatic control hiding', async ({ page }) => {
await setup(page)
// Start a pointer interaction after setup's programmatic focus.
await page.locator('.art-control-playAndPause .art-icon-play').click()
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
await page.evaluate(() => {
window.Artplayer.CONTROL_HIDE_TIME = 0
})
await page.mouse.move(900, 650)
await expect.poll(() => page.evaluate(() => window.art.controls.show)).toBe(false)
await expect.poll(() => page.locator('.art-bottom').evaluate(element => getComputedStyle(element).opacity)).toBe('0')
})