mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
203 lines
8.2 KiB
JavaScript
203 lines
8.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
// eslint-disable-next-line test/no-import-node-test -- Freeze historical lifecycle failures before candidate fixes.
|
|
import test from 'node:test'
|
|
import { mbEnvironment, mbHistorical } from './helpers/mediabunny.js'
|
|
|
|
function deferred() {
|
|
let resolve
|
|
let reject
|
|
const promise = new Promise((yes, no) => {
|
|
resolve = yes
|
|
reject = no
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
function timedEnvironment(implementation, option) {
|
|
const timers = new Map()
|
|
let sequence = 0
|
|
const env = mbEnvironment(implementation, {
|
|
setTimeout(fn, delay) {
|
|
const id = sequence++
|
|
timers.set(id, { fn, delay })
|
|
return id
|
|
},
|
|
clearTimeout(id) { timers.delete(id) },
|
|
})
|
|
env.eventNames.push('playing')
|
|
const canvas = env.factory(option)(env.art)
|
|
return { ...env, canvas, engine: canvas.engine, timers }
|
|
}
|
|
|
|
for (const implementation of await mbHistorical()) {
|
|
test(`MediaBunny historical ${implementation.name}: destroy still permits late load errors and scheduled events`, async () => {
|
|
const timers = new Map()
|
|
let nextTimer = 0
|
|
const env = mbEnvironment(implementation, {
|
|
setTimeout(fn) {
|
|
const id = nextTimer++
|
|
timers.set(id, fn)
|
|
return id
|
|
},
|
|
clearTimeout(id) { timers.delete(id) },
|
|
})
|
|
const canvas = env.factory()(env.art)
|
|
const engine = canvas.engine
|
|
const events = []
|
|
for (const name of ['waiting', 'loadstart', 'error'])
|
|
canvas.events.addEventListener(name, () => events.push(name))
|
|
let reject
|
|
const pending = new Promise((resolve, fail) => {
|
|
reject = fail
|
|
})
|
|
engine.performLoad = () => pending
|
|
const loading = engine.load('controlled-input')
|
|
const generation = engine.loadSeq
|
|
env.art.emit('destroy')
|
|
assert.equal(engine.loadSeq, generation, 'Destroy does not invalidate the load generation')
|
|
assert.equal(env.art.mediabunny, undefined)
|
|
assert.equal(timers.size, 2, 'Waiting and loadstart timers still exist after destroy')
|
|
reject(new Error('late controlled rejection'))
|
|
await loading
|
|
for (const fn of timers.values()) fn()
|
|
timers.clear()
|
|
assert.deepEqual(events, ['error', 'waiting', 'loadstart'])
|
|
assert.equal(env.emitted.at(-1).name, 'video:error')
|
|
assert.deepEqual({ ...engine.error }, { code: 4, message: 'late controlled rejection' })
|
|
assert.equal(engine.networkState, 3)
|
|
})
|
|
|
|
test(`MediaBunny historical ${implementation.name}: pending play starts video after a newer pause`, async () => {
|
|
const { engine, emitted } = timedEnvironment(implementation)
|
|
const audio = deferred()
|
|
let starts = 0
|
|
engine.audio.play = () => audio.promise
|
|
engine.video.start = () => starts++
|
|
const playing = engine.play()
|
|
assert.equal(engine.paused, false)
|
|
engine.pause()
|
|
audio.resolve()
|
|
await playing
|
|
assert.equal(engine.paused, true)
|
|
assert.equal(starts, 1)
|
|
assert.deepEqual(emitted.map(item => item.name), ['video:pause', 'video:play', 'video:playing'])
|
|
})
|
|
|
|
test(`MediaBunny historical ${implementation.name}: pending play starts video after destroy`, async () => {
|
|
const { art, engine, emitted } = timedEnvironment(implementation)
|
|
const audio = deferred()
|
|
let starts = 0
|
|
engine.audio.play = () => audio.promise
|
|
engine.video.start = () => starts++
|
|
const playing = engine.play()
|
|
art.emit('destroy')
|
|
const cursor = emitted.length
|
|
audio.resolve()
|
|
await playing
|
|
assert.equal(starts, 1)
|
|
assert.deepEqual(emitted.slice(cursor).map(item => item.name), ['video:play', 'video:playing'])
|
|
assert.equal(engine.paused, true)
|
|
})
|
|
|
|
test(`MediaBunny historical ${implementation.name}: rejected audio play leaves false paused state and prevents retry`, async () => {
|
|
const { engine } = timedEnvironment(implementation)
|
|
const error = new Error('controlled audio resume rejection')
|
|
let attempts = 0
|
|
engine.audio.play = () => {
|
|
attempts++
|
|
return Promise.reject(error)
|
|
}
|
|
await assert.rejects(engine.play(), item => item === error)
|
|
assert.equal(engine.paused, false)
|
|
await engine.play()
|
|
assert.equal(attempts, 1)
|
|
})
|
|
|
|
test(`MediaBunny historical ${implementation.name}: pending seek emits and resumes playback after destroy`, async () => {
|
|
const { art, engine, canvas } = timedEnvironment(implementation)
|
|
const audio = deferred()
|
|
const events = []
|
|
for (const name of ['seeking', 'waiting', 'pause', 'seeked'])
|
|
canvas.events.addEventListener(name, () => events.push(name))
|
|
engine.paused = false
|
|
engine.audio.seek = () => audio.promise
|
|
engine.video.seek = () => Promise.resolve()
|
|
engine.play = () => {
|
|
events.push('resumed-after-destroy')
|
|
return Promise.resolve()
|
|
}
|
|
const seeking = engine.seek(1)
|
|
art.emit('destroy')
|
|
audio.resolve()
|
|
await seeking
|
|
assert.deepEqual(events, ['seeking', 'waiting', 'pause', 'seeked', 'resumed-after-destroy'])
|
|
assert.equal(engine.seeking, false)
|
|
})
|
|
|
|
test(`MediaBunny historical ${implementation.name}: successful load and destroy retain their timeout timer`, async () => {
|
|
const { art, engine, timers } = timedEnvironment(implementation, { loadTimeout: 1234 })
|
|
engine.performLoad = () => Promise.resolve()
|
|
await engine.load('controlled-input')
|
|
assert.equal(timers.size, 3)
|
|
assert.equal([...timers.values()].filter(item => item.delay === 1234).length, 1)
|
|
art.emit('destroy')
|
|
assert.equal(timers.size, 3)
|
|
for (const { fn } of timers.values()) fn()
|
|
timers.clear()
|
|
await Promise.resolve()
|
|
assert.equal(engine.error, null, 'Settled Promise.race observes the late timeout rejection without changing the successful result')
|
|
})
|
|
|
|
test(`MediaBunny historical ${implementation.name}: active timeout keeps its error when the underlying load rejects later`, async () => {
|
|
const { engine, timers } = timedEnvironment(implementation, { loadTimeout: 5678 })
|
|
const pending = deferred()
|
|
engine.performLoad = () => pending.promise
|
|
const loading = engine.load('controlled-input')
|
|
const timeout = [...timers.values()].find(item => item.delay === 5678)
|
|
timeout.fn()
|
|
await loading
|
|
assert.deepEqual({ ...engine.error }, { code: 4, message: 'Load timeout' })
|
|
const error = engine.error
|
|
pending.reject(new Error('late underlying rejection'))
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
assert.equal(engine.error, error)
|
|
timers.clear()
|
|
})
|
|
|
|
test(`MediaBunny historical ${implementation.name}: Range preflight rejects absent support, tolerates fetch errors and keeps its HLS version boundary`, async () => {
|
|
const { engine, canvas, globals } = timedEnvironment(implementation, { preflightRange: true })
|
|
const calls = []
|
|
const errors = []
|
|
const warnings = []
|
|
canvas.events.addEventListener('error', event => errors.push(event.detail.type))
|
|
globals.console = { ...console, warn: (...args) => warnings.push(args) }
|
|
globals.fetch = async (url, options) => {
|
|
calls.push({ url, options: { ...options } })
|
|
return { headers: { get: () => 'bytes' } }
|
|
}
|
|
assert.equal(await engine.video.preflight('https://example.invalid/media.mp4'), true)
|
|
assert.deepEqual(calls, [{ url: 'https://example.invalid/media.mp4', options: { method: 'HEAD' } }])
|
|
globals.fetch = async () => ({ headers: { get: () => null } })
|
|
assert.equal(await engine.video.preflight('https://example.invalid/media.mp4'), false)
|
|
assert.deepEqual(errors, ['RangeNotSupported'])
|
|
assert.equal(engine.error, null, 'Range refusal is an event, not engine.error state')
|
|
const failure = new Error('controlled HEAD rejection')
|
|
globals.fetch = async () => {
|
|
throw failure
|
|
}
|
|
assert.equal(await engine.video.preflight('https://example.invalid/media.mp4'), true)
|
|
assert.equal(warnings.length, 1)
|
|
assert.equal(warnings[0][1], failure)
|
|
let requests = 0
|
|
globals.fetch = async () => {
|
|
requests++
|
|
return { headers: { get: () => 'bytes' } }
|
|
}
|
|
assert.equal(await engine.video.preflight('https://example.invalid/media.m3u8'), true)
|
|
assert.equal(requests, implementation.name === 'published-1.0.0' ? 1 : 0)
|
|
assert.equal(await engine.video.preflight(new Blob([])), true)
|
|
assert.equal(requests, implementation.name === 'published-1.0.0' ? 1 : 0)
|
|
})
|
|
}
|