mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-24 06:31:20 -08:00
refactor(core): [CORE-23] complete keyboard controls and focus lifecycle
This commit is contained in:
1 parent
faaa3e8c2d
commit
8ed0a5228c
190 files changed
+53981
-489
No files matched your search
@@ -0,0 +1,161 @@
|
||||
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')
|
||||
})
|
||||
Reference in new issue
Block a user