mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-21 12:41:20 -08:00
refactor(core): CORE-16 type display modes and own fullscreen lifetimes
This commit is contained in:
1 parent
1b83081e8f
commit
88aef830df
81 files changed
+33897
-1503
No files matched your search
@@ -0,0 +1,215 @@
|
||||
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()
|
||||
})
|
||||
Reference in new issue
Block a user