Files
ArtPlayer/test/browser/display-native.spec.js

252 lines
12 KiB
JavaScript

import { expect, test } from './fixtures.js'
async function setup(page, core) {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
await page.evaluate(() => {
const maps = [
['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenchange'],
['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitfullscreenchange'],
['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitfullscreenchange'],
['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozfullscreenchange'],
['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'MSFullscreenChange'],
]
window.fullscreenAPI = maps.find(([, exit]) => exit in document)
window.createPlayer('/test/pattern.mp4')
window.art.template.$container.id = 'native-fullscreen-main'
const container = document.createElement('div')
Object.assign(container.style, { width: '320px', height: '180px' })
document.body.append(container)
window.other = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true })
window.fullscreenEvents = [[], []]
window.art.on('fullscreen', value => window.fullscreenEvents[0].push(value))
window.other.on('fullscreen', value => window.fullscreenEvents[1].push(value))
document.querySelector('#play').onclick = () => {
window.art.fullscreen = true
}
})
await expect.poll(() => page.evaluate(() => window.art.isReady && window.other.isReady)).toBe(true)
expect(await page.evaluate(() => Boolean(window.fullscreenAPI))).toBe(true)
}
test('candidate: void exit waits for the owned video to leave and accepts another owner as completion', async ({ page }) => {
await setup(page, 'candidate')
expect(await page.evaluate(async () => {
const art = window.art
const [, exitName, elementName, eventName] = window.fullscreenAPI
const elementDescriptor = Object.getOwnPropertyDescriptor(document, elementName)
const originalExit = document[exitName]
let element = art.template.$video
Object.defineProperty(document, elementName, { configurable: true, get: () => element })
document.dispatchEvent(new Event(eventName))
document[exitName] = () => undefined
let settled = false
const result = Object.getOwnPropertyDescriptor(art, 'fullscreen').set(false).then(() => settled = true)
await new Promise(resolve => setTimeout(resolve, 0))
const early = settled
element = window.other.template.$player
document.dispatchEvent(new Event(eventName))
await result
const completed = settled && !art.fullscreen && window.other.fullscreen
element = null
document.dispatchEvent(new Event(eventName))
if (elementDescriptor)
Object.defineProperty(document, elementName, elementDescriptor)
else
delete document[elementName]
document[exitName] = originalExit
art.destroy()
window.other.destroy()
return { early, completed }
})).toEqual({ early: false, completed: true })
})
for (const core of ['published', 'candidate']) {
test(`${core}: native fullscreen belongs to its player and other players cannot exit it`, async ({ page }) => {
await setup(page, core)
await page.click('#play')
await expect.poll(() => page.evaluate(() => document[window.fullscreenAPI[2]] === window.art.template.$player)).toBe(true)
await expect.poll(() => page.evaluate(() => window.fullscreenEvents[0])).toEqual([true])
expect(await page.evaluate(() => ({ own: window.art.fullscreen, other: window.other.fullscreen, className: window.other.template.$player.classList.contains('art-fullscreen'), otherEvents: window.fullscreenEvents[1] }))).toEqual({ own: true, other: core === 'published', className: core === 'published', otherEvents: core === 'published' ? [true] : [] })
await page.evaluate(async () => {
await Object.getOwnPropertyDescriptor(window.other, 'fullscreen').set.call(window.other, false)
})
await expect.poll(() => page.evaluate(() => Boolean(document[window.fullscreenAPI[2]]))).toBe(core === 'candidate')
await page.evaluate(async () => {
if (document[window.fullscreenAPI[2]])
await document[window.fullscreenAPI[1]]()
window.art.destroy()
window.other.destroy()
})
})
test(`${core}: destroyed players stop receiving later native fullscreen events`, async ({ page }) => {
await setup(page, core)
await page.evaluate(() => window.other.destroy(false))
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.fullscreenEvents[0])).toEqual([true])
expect(await page.evaluate(() => ({ events: window.fullscreenEvents[1], className: window.other.template.$player.classList.contains('art-fullscreen') }))).toEqual({ events: core === 'published' ? [true] : [], className: core === 'published' })
await page.evaluate(async () => {
await document[window.fullscreenAPI[1]]()
window.art.destroy()
})
})
test(`${core}: rejected native fullscreen request removes its temporary change listener`, async ({ page }) => {
await setup(page, core)
expect(await page.evaluate(async () => {
const art = window.art
const [request, , , change] = window.fullscreenAPI
const add = document.addEventListener
const remove = document.removeEventListener
const active = new Set()
document.addEventListener = function (name, callback, options) {
if (name === change)
active.add(callback)
return add.call(this, name, callback, options)
}
document.removeEventListener = function (name, callback, options) {
if (name === change)
active.delete(callback)
return remove.call(this, name, callback, options)
}
const failure = new Error('controlled fullscreen rejection')
art.template.$player[request] = () => Promise.reject(failure)
const rejected = await Object.getOwnPropertyDescriptor(art, 'fullscreen').set.call(art, true).then(() => false, error => error === failure)
const result = { rejected, leaked: active.size }
document.addEventListener = add
document.removeEventListener = remove
art.destroy()
window.other.destroy()
return result
})).toEqual({ rejected: true, leaked: core === 'published' ? 1 : 0 })
})
}
test('candidate: fullscreen assignment rejection shows notice without an unhandled rejection', async ({ page }) => {
await setup(page, 'candidate')
await page.evaluate(() => {
const [request] = window.fullscreenAPI
window.art.template.$player[request] = () => Promise.reject(new Error('controlled assignment denial'))
window.art.fullscreen = true
})
await expect(page.locator('#native-fullscreen-main .art-notice')).toContainText('controlled assignment denial')
await page.evaluate(() => {
window.art.destroy()
window.other.destroy()
})
})
test('candidate: reentrant entry preserves real fullscreen UI and event order', async ({ page }) => {
await setup(page, 'candidate')
await page.evaluate(() => {
window.fullscreenOrder = []
window.art.on('fullscreen', (value) => {
if (value) {
window.fullscreenOrder.push(['fullscreen', window.art.template.$player.classList.contains('art-fullscreen')])
window.art.fullscreen = true
}
})
window.art.on('resize', () => {
if (window.art.fullscreen)
window.fullscreenOrder.push(['resize', window.art.template.$player.classList.contains('art-fullscreen')])
})
})
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.fullscreen && window.art.template.$player.classList.contains('art-fullscreen'))).toBe(true)
expect(await page.evaluate(() => window.fullscreenOrder.slice(0, 2))).toEqual([['fullscreen', false], ['resize', true]])
await page.evaluate(async () => {
await document[window.fullscreenAPI[1]]()
window.art.destroy()
window.other.destroy()
})
})
for (const mode of ['promise-exit', 'promise-destroy', 'void-exit', 'void-destroy']) {
test(`candidate: pending native entry ${mode} releases a late own entry`, async ({ page }) => {
await setup(page, 'candidate')
expect(await page.evaluate(async (mode) => {
const art = window.art
const [request, exit, element, change] = window.fullscreenAPI
const originalExit = document[exit]
const originalElement = Object.getOwnPropertyDescriptor(document, element)
let current = null
let complete
let exits = 0
Object.defineProperty(document, element, { configurable: true, get: () => current })
art.template.$player[request] = () => {
if (mode.startsWith('promise'))
return new Promise(resolve => complete = resolve)
complete = () => document.dispatchEvent(new Event(change))
}
document[exit] = () => {
exits++
current = null
document.dispatchEvent(new Event(change))
return Promise.resolve()
}
const setter = Object.getOwnPropertyDescriptor(art, 'fullscreen').set
const pending = setter.call(art, true)
if (mode.endsWith('destroy'))
art.destroy(false)
else
await setter.call(art, false)
await pending
current = art.template.$player
complete()
// Flush the request's native Promise observer and setter settlement chain.
await new Promise(resolve => setTimeout(resolve, 0))
const result = { exits, fullscreen: art.fullscreen, className: art.template.$player.classList.contains('art-fullscreen'), events: window.fullscreenEvents[0] }
art.destroy()
window.other.destroy()
document[exit] = originalExit
if (originalElement)
Object.defineProperty(document, element, originalElement)
else
delete document[element]
return result
}, mode)).toEqual({ exits: 1, fullscreen: false, className: false, events: [] })
})
}
test('candidate: cancelling an actual native request in the initiating click releases fullscreen', async ({ page }) => {
await setup(page, 'candidate')
await page.evaluate(() => {
window.entrySettled = false
document.querySelector('#play').onclick = () => {
const setter = Object.getOwnPropertyDescriptor(window.art, 'fullscreen').set
const entering = setter.call(window.art, true)
window.art.fullscreen = false
void entering.then(() => window.entrySettled = true)
}
})
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.entrySettled && !document[window.fullscreenAPI[2]])).toBe(true)
await page.evaluate(() => {
window.art.destroy()
window.other.destroy()
})
})
test('candidate: rejected exit retains actual fullscreen state and can be retried', async ({ page }) => {
await setup(page, 'candidate')
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.template.$player.classList.contains('art-fullscreen'))).toBe(true)
expect(await page.evaluate(async () => {
const [, exit] = window.fullscreenAPI
const originalExit = document[exit]
const failure = new Error('controlled exit denial')
document[exit] = () => Promise.reject(failure)
const setter = Object.getOwnPropertyDescriptor(window.art, 'fullscreen').set
const rejected = await setter.call(window.art, false).then(() => false, error => error === failure)
const result = { rejected, fullscreen: window.art.fullscreen, className: window.art.template.$player.classList.contains('art-fullscreen') }
document[exit] = originalExit
await setter.call(window.art, false)
return result
})).toEqual({ rejected: true, fullscreen: true, className: true })
await expect.poll(() => page.evaluate(() => window.art.fullscreen || window.art.template.$player.classList.contains('art-fullscreen'))).toBe(false)
await page.evaluate(() => {
window.art.destroy()
window.other.destroy()
})
})