Files
ArtPlayer/test/initialization.test.js
T

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, [])
})