mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:39:41 -08:00
215 lines
8.6 KiB
JavaScript
215 lines
8.6 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
|
import { test } from 'node:test'
|
|
import { loadModules } from './helpers/load.js'
|
|
|
|
const { optionInit, previousInit, airplayMix, previousAirplay, beginLifecycle, getScope } = await loadModules({
|
|
optionInit: 'packages/artplayer/src/player/optionInit',
|
|
previousInit: 'test/helpers/legacy-option-init',
|
|
airplayMix: 'packages/artplayer/src/player/airplayMix',
|
|
previousAirplay: 'test/helpers/legacy-airplay',
|
|
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
|
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
|
})
|
|
|
|
function initialHost(boundary) {
|
|
const calls = []
|
|
let art
|
|
const record = (kind, key, value) => {
|
|
calls.push([kind, key, value])
|
|
if (`${kind}:${key}` === boundary)
|
|
getScope(art).dispose()
|
|
}
|
|
const video = new Proxy({}, { set(target, key, value) {
|
|
record('video', key, value)
|
|
return Reflect.set(target, key, value)
|
|
} })
|
|
const poster = { style: new Proxy({}, { set(target, key, value) {
|
|
record('poster', key, value)
|
|
return Reflect.set(target, key, value)
|
|
} }) }
|
|
art = {
|
|
option: { moreVideoAttr: { controls: false }, muted: true, volume: 2, poster: 'poster.png', autoplay: true, playsInline: true, theme: 'green', cssVar: { '--custom': '2' }, url: 'video.mp4' },
|
|
storage: { get(key) {
|
|
record('storage', key, undefined)
|
|
return 0.4
|
|
} },
|
|
template: { $video: video, $poster: poster },
|
|
attr: (key, value) => record('attr', key, value),
|
|
cssVar: (key, value) => record('css', key, value),
|
|
get muted() { return undefined },
|
|
set muted(value) { record('player', 'muted', value) },
|
|
get url() { return undefined },
|
|
set url(value) { record('player', 'url', value) },
|
|
}
|
|
beginLifecycle(art)
|
|
return { art, calls }
|
|
}
|
|
|
|
test('option initialization retains native/DOM/write order, storage precedence and CSS mutation', () => {
|
|
const old = initialHost()
|
|
const current = initialHost()
|
|
assert.equal(previousInit(old.art), undefined)
|
|
assert.equal(optionInit(current.art), undefined)
|
|
assert.deepEqual(current.calls, old.calls)
|
|
assert.deepEqual(current.calls, [
|
|
['attr', 'controls', false],
|
|
['player', 'muted', true],
|
|
['video', 'volume', 1],
|
|
['storage', 'volume', undefined],
|
|
['video', 'volume', 0.4],
|
|
['poster', 'backgroundImage', 'url(poster.png)'],
|
|
['video', 'autoplay', true],
|
|
['video', 'playsInline', true],
|
|
['video', 'webkit-playsinline', true],
|
|
['css', '--custom', '2'],
|
|
['css', '--art-theme', 'green'],
|
|
['player', 'url', 'video.mp4'],
|
|
])
|
|
assert.deepEqual(current.art.option.cssVar, { '--custom': '2', '--art-theme': 'green' })
|
|
})
|
|
|
|
test('option initialization stops after reentrant teardown from each external write boundary', () => {
|
|
for (const boundary of ['attr:controls', 'player:muted', 'video:volume', 'storage:volume', 'poster:backgroundImage', 'video:autoplay', 'video:playsInline', 'css:--custom']) {
|
|
const old = initialHost(boundary)
|
|
const current = initialHost(boundary)
|
|
previousInit(old.art)
|
|
optionInit(current.art)
|
|
assert(current.calls.length < old.calls.length, boundary)
|
|
assert.deepEqual(current.calls.at(-1).slice(0, 2).join(':'), boundary)
|
|
assert.equal(current.calls.some(call => call[0] === 'player' && call[1] === 'url'), false)
|
|
}
|
|
})
|
|
|
|
test('initial option value getters and native conversion cannot write after teardown', () => {
|
|
for (const field of ['moreVideoAttr', 'cssVar', 'url', 'volume']) {
|
|
const { art, calls } = initialHost()
|
|
if (field === 'moreVideoAttr' || field === 'cssVar') {
|
|
const key = field === 'cssVar' ? '--custom' : 'controls'
|
|
Object.defineProperty(art.option[field], key, { enumerable: true, get() {
|
|
getScope(art).dispose()
|
|
return 'stale'
|
|
} })
|
|
}
|
|
else {
|
|
Object.defineProperty(art.option, field, { get() {
|
|
getScope(art).dispose()
|
|
return field === 'url' ? 'stale.mp4' : 1
|
|
} })
|
|
}
|
|
optionInit(art)
|
|
assert.equal(calls.some(call => call[2] === 'stale' || call[2] === 'stale.mp4'), false)
|
|
assert.equal(calls.some(call => call[0] === 'player' && call[1] === 'url'), false)
|
|
if (field === 'volume')
|
|
assert.equal(calls.some(call => call[0] === 'video' && call[1] === 'volume'), false)
|
|
}
|
|
})
|
|
|
|
function airplayHost(t, supported = true) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
Object.defineProperty(globalThis, 'window', { configurable: true, writable: true, value: supported ? { WebKitPlaybackTargetAvailabilityEvent() {} } : {} })
|
|
t.after(() => descriptor ? Object.defineProperty(globalThis, 'window', descriptor) : delete globalThis.window)
|
|
const calls = []
|
|
let listener
|
|
const video = Object.assign(new EventTarget(), { webkitShowPlaybackTargetPicker() {
|
|
assert.equal(this, video)
|
|
calls.push('picker')
|
|
} })
|
|
const art = { template: { $video: video }, proxy(target, name, callback) {
|
|
assert.equal(target, video)
|
|
assert.equal(name, 'webkitplaybacktargetavailabilitychanged')
|
|
listener = callback
|
|
}, i18n: { get: key => key }, notice: { show: '' }, emit: name => calls.push(name) }
|
|
beginLifecycle(art)
|
|
return { art, calls, availability: value => listener({ availability: value }) }
|
|
}
|
|
|
|
test('AirPlay retains availability, captured native receiver, event order and unavailable notice', (t) => {
|
|
const { art, calls, availability } = airplayHost(t)
|
|
airplayMix(art)
|
|
const airplay = art.airplay
|
|
assert.equal(airplay(), undefined)
|
|
assert.deepEqual(calls, ['picker', 'airplay'])
|
|
availability('not-available')
|
|
airplay()
|
|
assert.equal(art.notice.show, 'AirPlay Not Available')
|
|
assert.deepEqual(calls, ['picker', 'airplay'])
|
|
availability('available')
|
|
airplay()
|
|
assert.deepEqual(calls, ['picker', 'airplay', 'picker', 'airplay'])
|
|
assert.deepEqual(Object.getOwnPropertyDescriptor(art, 'airplay'), { value: airplay, configurable: false, enumerable: false, writable: false })
|
|
})
|
|
|
|
test('AirPlay installation stops after capability getters or registration destroy its owner', (t) => {
|
|
for (const install of [previousAirplay, airplayMix]) {
|
|
for (const boundary of ['capability', 'picker', 'registration']) {
|
|
const { art } = airplayHost(t)
|
|
const reads = []
|
|
Object.defineProperty(globalThis.window, 'WebKitPlaybackTargetAvailabilityEvent', { get() {
|
|
reads.push('capability')
|
|
if (boundary === 'capability')
|
|
getScope(art).dispose()
|
|
return true
|
|
} })
|
|
Object.defineProperty(art.template.$video, 'webkitShowPlaybackTargetPicker', { get() {
|
|
reads.push('picker')
|
|
if (boundary === 'picker')
|
|
getScope(art).dispose()
|
|
return () => {}
|
|
} })
|
|
art.proxy = () => {
|
|
reads.push('registration')
|
|
if (boundary === 'registration')
|
|
getScope(art).dispose()
|
|
}
|
|
install(art)
|
|
assert.equal(Object.hasOwn(art, 'airplay'), install === previousAirplay)
|
|
const expected = ['capability', 'picker', 'registration']
|
|
assert.deepEqual(reads, install === previousAirplay ? expected : expected.slice(0, expected.indexOf(boundary) + 1))
|
|
}
|
|
}
|
|
})
|
|
|
|
test('AirPlay calls and picker completion cannot publish effects after destruction', (t) => {
|
|
for (const install of [previousAirplay, airplayMix]) {
|
|
const { art, calls, availability } = airplayHost(t)
|
|
install(art)
|
|
getScope(art).dispose()
|
|
availability('available')
|
|
art.airplay()
|
|
assert.deepEqual(calls, install === previousAirplay ? ['picker', 'airplay'] : [])
|
|
}
|
|
const { art, calls } = airplayHost(t)
|
|
art.template.$video.webkitShowPlaybackTargetPicker = () => {
|
|
calls.push('picker')
|
|
getScope(art).dispose()
|
|
}
|
|
airplayMix(art)
|
|
art.airplay()
|
|
assert.deepEqual(calls, ['picker'])
|
|
})
|
|
|
|
test('AirPlay unavailable translation and picker getters respect teardown; native errors still throw', (t) => {
|
|
const unavailable = airplayHost(t, false)
|
|
unavailable.art.i18n.get = () => {
|
|
getScope(unavailable.art).dispose()
|
|
return 'stale notice'
|
|
}
|
|
airplayMix(unavailable.art)
|
|
unavailable.art.airplay()
|
|
assert.equal(unavailable.art.notice.show, '')
|
|
const { art, calls } = airplayHost(t)
|
|
airplayMix(art)
|
|
const error = new Error('picker failed')
|
|
art.template.$video.webkitShowPlaybackTargetPicker = () => {
|
|
throw error
|
|
}
|
|
assert.throws(() => art.airplay(), value => value === error)
|
|
Object.defineProperty(art.template.$video, 'webkitShowPlaybackTargetPicker', { get() {
|
|
getScope(art).dispose()
|
|
return () => calls.push('stale picker')
|
|
} })
|
|
art.airplay()
|
|
assert.deepEqual(calls, [])
|
|
})
|