mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
216 lines
8.0 KiB
JavaScript
216 lines
8.0 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { getEventListeners } from 'node:events'
|
|
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
|
import { test } from 'node:test'
|
|
import { setImmediate } from 'node:timers/promises'
|
|
import { loadModules } from './helpers/load.js'
|
|
|
|
const { Emitter, nativePip, webkitPip, beginLifecycle, getScope } = await loadModules({
|
|
Emitter: 'packages/artplayer/src/utils/emitter',
|
|
nativePip: { file: 'packages/artplayer/src/display/native-pip', name: 'nativePip' },
|
|
webkitPip: { file: 'packages/artplayer/src/display/webkit-pip', name: 'webkitPip' },
|
|
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
|
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
|
})
|
|
|
|
function fixture(kind = 'native') {
|
|
const video = new EventTarget()
|
|
video.ownerDocument = {
|
|
pictureInPictureElement: null,
|
|
exitPictureInPicture() {
|
|
const previous = this.pictureInPictureElement
|
|
this.pictureInPictureElement = null
|
|
previous?.dispatchEvent(new Event('leavepictureinpicture'))
|
|
return Promise.resolve()
|
|
},
|
|
}
|
|
video.requestPictureInPicture = () => {
|
|
video.ownerDocument.pictureInPictureElement = video
|
|
video.dispatchEvent(new Event('enterpictureinpicture'))
|
|
return Promise.resolve({ width: 320, height: 180 })
|
|
}
|
|
video.webkitPresentationMode = 'inline'
|
|
video.webkitSetPresentationMode = (mode) => {
|
|
video.webkitPresentationMode = mode
|
|
video.dispatchEvent(new Event('webkitpresentationmodechanged'))
|
|
video.dispatchEvent(new Event(mode === 'picture-in-picture' ? 'enterpictureinpicture' : 'leavepictureinpicture'))
|
|
}
|
|
const art = Object.assign(new Emitter(), { template: { $video: video }, state: 'standard', notice: { show: '' }, i18n: { get: key => key } })
|
|
beginLifecycle(art)
|
|
Object.defineProperty(art, 'pip', kind === 'native' ? nativePip(art) : webkitPip(art))
|
|
const events = []
|
|
art.on('pip', value => events.push(value))
|
|
return { art, video, document: video.ownerDocument, events }
|
|
}
|
|
|
|
test('native PiP invokes immediately, retains element identity and void setter', () => {
|
|
const { art, video, events } = fixture()
|
|
assert.equal(Object.getOwnPropertyDescriptor(art, 'pip').set(true), undefined)
|
|
assert.equal(art.pip, video)
|
|
assert.deepEqual(events, [true])
|
|
art.pip = false
|
|
assert.equal(art.pip, null)
|
|
assert.deepEqual(events, [true, false])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('native PiP ignores a queued leave from an older session while the video still owns PiP', () => {
|
|
const { art, video, events } = fixture()
|
|
art.pip = true
|
|
video.dispatchEvent(new Event('leavepictureinpicture'))
|
|
assert.equal(art.pip, video)
|
|
assert.deepEqual(events, [true])
|
|
art.pip = false
|
|
assert.deepEqual(events, [true, false])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('native PiP rejection preserves the notice error without unhandled rejection', async () => {
|
|
const { art, video, events } = fixture()
|
|
const failure = new Error('denied')
|
|
video.requestPictureInPicture = () => Promise.reject(failure)
|
|
art.pip = true
|
|
await setImmediate()
|
|
assert.equal(art.notice.show, failure)
|
|
assert.equal(art.pip, null)
|
|
assert.deepEqual(events, [])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('native PiP synchronous errors remain synchronous and retain identity', () => {
|
|
const { art, video } = fixture()
|
|
const failure = new Error('sync denial')
|
|
video.requestPictureInPicture = () => {
|
|
throw failure
|
|
}
|
|
assert.throws(() => {
|
|
art.pip = true
|
|
}, error => error === failure)
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('obsolete native PiP failure does not replace the latest notice', async () => {
|
|
const { art, video } = fixture()
|
|
let reject
|
|
video.requestPictureInPicture = () => new Promise((resolve, fail) => reject = fail)
|
|
art.pip = true
|
|
art.pip = false
|
|
art.notice.show = 'current notice'
|
|
reject(new Error('old request'))
|
|
await setImmediate()
|
|
assert.equal(art.notice.show, 'current notice')
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('destroyed native PiP ignores late completion when another video owns PiP', async () => {
|
|
const { art, video, document } = fixture()
|
|
let complete
|
|
video.requestPictureInPicture = () => new Promise(resolve => complete = resolve)
|
|
art.pip = true
|
|
getScope(art).dispose()
|
|
document.pictureInPictureElement = {}
|
|
document.exitPictureInPicture = () => assert.fail('exited another video')
|
|
complete({})
|
|
await setImmediate()
|
|
assert.equal(getEventListeners(video, 'enterpictureinpicture').length, 0)
|
|
assert.equal(getEventListeners(video, 'leavepictureinpicture').length, 0)
|
|
})
|
|
|
|
test('native PiP exit rejection keeps real state and permits retry', async () => {
|
|
const { art, document, video, events } = fixture()
|
|
art.pip = true
|
|
await setImmediate()
|
|
const exit = document.exitPictureInPicture
|
|
const failure = new Error('exit denied')
|
|
document.exitPictureInPicture = () => Promise.reject(failure)
|
|
art.pip = false
|
|
await setImmediate()
|
|
assert.equal(art.pip, video)
|
|
assert.equal(art.notice.show, failure)
|
|
document.exitPictureInPicture = exit
|
|
art.pip = false
|
|
await setImmediate()
|
|
assert.equal(art.pip, null)
|
|
assert.deepEqual(events, [true, false])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('WebKit PiP retains boolean getter and repeated synchronous setter events', () => {
|
|
const { art, events } = fixture('webkit')
|
|
art.pip = true
|
|
art.pip = true
|
|
assert.equal(art.pip, true)
|
|
art.pip = false
|
|
assert.equal(art.pip, false)
|
|
assert.deepEqual(events, [true, true, false])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('WebKit PiP observes external presentation transitions without duplicate native signals', () => {
|
|
const { art, video, events } = fixture('webkit')
|
|
video.webkitSetPresentationMode('picture-in-picture')
|
|
video.webkitSetPresentationMode('inline')
|
|
assert.deepEqual(events, [true, false])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('WebKit PiP false does not exit a different fullscreen presentation', () => {
|
|
const { art, video } = fixture('webkit')
|
|
video.webkitPresentationMode = 'fullscreen'
|
|
art.pip = false
|
|
assert.equal(video.webkitPresentationMode, 'fullscreen')
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('destroyed WebKit PiP releases delayed entry without retaining instance listeners', () => {
|
|
const { art, video, events } = fixture('webkit')
|
|
const setMode = video.webkitSetPresentationMode
|
|
video.webkitSetPresentationMode = () => {}
|
|
art.pip = true
|
|
getScope(art).dispose()
|
|
video.webkitSetPresentationMode = setMode
|
|
setMode('picture-in-picture')
|
|
assert.equal(video.webkitPresentationMode, 'inline')
|
|
assert.deepEqual(events, [])
|
|
assert.equal(getEventListeners(video, 'webkitpresentationmodechanged').length, 0)
|
|
})
|
|
|
|
test('WebKit PiP destruction inside entry callback stops stale notifications', () => {
|
|
const { art, video, events } = fixture('webkit')
|
|
art.on('pip', () => getScope(art).dispose())
|
|
art.pip = true
|
|
assert.equal(video.webkitPresentationMode, 'inline')
|
|
assert.deepEqual(events, [true])
|
|
})
|
|
|
|
for (const kind of ['native', 'webkit']) {
|
|
test(`${kind} PiP reentrant cancellation retains its branch-specific setter event contract`, async () => {
|
|
const { art, video, events } = fixture(kind)
|
|
// eslint-disable-next-line accessor-pairs -- Intercept mode synchronization without implementing the unrelated state getter.
|
|
Object.defineProperty(art, 'state', { set(value) {
|
|
if (value === 'pip')
|
|
art.pip = false
|
|
} })
|
|
if (kind === 'native')
|
|
await video.requestPictureInPicture()
|
|
else
|
|
video.webkitSetPresentationMode('picture-in-picture')
|
|
assert.deepEqual(events, kind === 'native' ? [] : [false])
|
|
getScope(art).dispose()
|
|
})
|
|
}
|
|
|
|
test('WebKit PiP checks current support and can retry after media becomes eligible', () => {
|
|
const { art, video, events } = fixture('webkit')
|
|
video.webkitSupportsPresentationMode = () => false
|
|
art.pip = true
|
|
assert.equal(art.pip, false)
|
|
assert.equal(art.notice.show, 'PIP Not Supported')
|
|
assert.deepEqual(events, [])
|
|
video.webkitSupportsPresentationMode = () => true
|
|
art.pip = true
|
|
assert.equal(art.pip, true)
|
|
assert.deepEqual(events, [true])
|
|
getScope(art).dispose()
|
|
})
|