mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 10:41:20 -08:00
refactor(mediabunny): [PKG-MB-04] checkpoint typed shim and event cleanup
This commit is contained in:
1 parent
adba8a3d12
commit
9af253a5a9
24 files changed
+1478
-250
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { mbCandidate } from '../helpers/mediabunny.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await mbCandidate()
|
||||
for (const scenario of ['callback', 'destroy', 'independent']) {
|
||||
test(`MediaBunny ${implementation.name}: native synthetic RAF ${scenario}`, async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=published')
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports;
|
||||
${implementation.code}; window.mbFactory = module.exports.default || module.exports; })();` })
|
||||
const result = await page.evaluate(async (scenario) => {
|
||||
function create() {
|
||||
const listeners = new Map()
|
||||
const host = {
|
||||
constructor: window.Artplayer,
|
||||
option: { url: '', autoSize: false },
|
||||
template: { $player: document.querySelector('.player') },
|
||||
on(name, callback) {
|
||||
const list = listeners.get(name) || []
|
||||
list.push(callback)
|
||||
listeners.set(name, list)
|
||||
},
|
||||
emit(name, ...args) {
|
||||
for (const callback of listeners.get(name) || []) callback(...args)
|
||||
},
|
||||
}
|
||||
const canvas = window.mbFactory()(host)
|
||||
return { host, canvas }
|
||||
}
|
||||
const first = create()
|
||||
const second = scenario === 'independent' ? create() : null
|
||||
const calls = []
|
||||
const id = first.canvas.requestVideoFrameCallback((time, metadata) => calls.push({ owner: 'first', time: typeof time, mediaTime: metadata.mediaTime, presentedFrames: metadata.presentedFrames }))
|
||||
second?.canvas.requestVideoFrameCallback(() => calls.push({ owner: 'second' }))
|
||||
if (scenario !== 'callback') {
|
||||
first.host.emit('destroy')
|
||||
first.canvas.requestVideoFrameCallback(() => calls.push({ owner: 'after-destroy' }))
|
||||
}
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
first.host.emit('destroy')
|
||||
second?.host.emit('destroy')
|
||||
return { idType: typeof id, calls, listeners: first.canvas.events.listeners.size }
|
||||
}, scenario)
|
||||
expect(result.idType).toBe('number')
|
||||
expect(result.listeners).toBe(0)
|
||||
expect(result.calls).toEqual(scenario === 'callback' ? [{ owner: 'first', time: 'number', mediaTime: 0, presentedFrames: 0 }] : scenario === 'independent' ? [{ owner: 'second' }] : [])
|
||||
await testInfo.attach('mediabunny-shim-frames', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, result, scope: 'Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed.' }) })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Exercise actual proxy instances, including historical event semantics.
|
||||
import test from 'node:test'
|
||||
import { mbCandidate, mbEnvironment, mbHistorical } from './helpers/mediabunny.js'
|
||||
|
||||
const candidate = await mbCandidate()
|
||||
const frozen = (await mbHistorical()).find(item => item.name === 'frozen-workspace')
|
||||
function environment(implementation = candidate, option = {}) {
|
||||
const env = mbEnvironment(implementation)
|
||||
const canvas = env.factory(option)(env.art)
|
||||
return { ...env, canvas, shim: env.art.mediabunny, events: canvas.events }
|
||||
}
|
||||
|
||||
test('MediaBunny candidate retains exact shim own/prototype surface, canvas descriptors and bound forwarding', () => {
|
||||
const old = environment(frozen)
|
||||
const next = environment()
|
||||
assert.deepEqual(Object.keys(next.canvas), Object.keys(old.canvas))
|
||||
assert.deepEqual(Object.getOwnPropertyNames(next.shim), Object.getOwnPropertyNames(old.shim))
|
||||
assert.deepEqual(Object.getOwnPropertyNames(Object.getPrototypeOf(next.shim)), Object.getOwnPropertyNames(Object.getPrototypeOf(old.shim)))
|
||||
for (const name of Object.keys(next.canvas)) {
|
||||
const a = Object.getOwnPropertyDescriptor(next.canvas, name)
|
||||
const b = Object.getOwnPropertyDescriptor(old.canvas, name)
|
||||
assert.deepEqual([a.enumerable, a.configurable, a.writable, typeof a.get, typeof a.set], [b.enumerable, b.configurable, b.writable, typeof b.get, typeof b.set], name)
|
||||
}
|
||||
assert.notEqual(next.canvas.play, next.canvas.play)
|
||||
assert.throws(() => Reflect.set(next.canvas, 'duration', 3), { name: 'TypeError' })
|
||||
assert.equal(next.canvas.getContext('2d'), next.context)
|
||||
next.canvas.setAttribute('src', 'native-only.mp4')
|
||||
assert.equal(next.canvas.src, null)
|
||||
assert.deepEqual(next.nativeCalls.at(-1), ['attribute', 'src', 'native-only.mp4'])
|
||||
})
|
||||
|
||||
for (const implementation of [frozen, candidate]) {
|
||||
test(`MediaBunny ${implementation.name}: event dispatch retains duplicates, first removal, detail identity and live-array mutation`, () => {
|
||||
const env = environment(implementation)
|
||||
const calls = []
|
||||
const detail = { value: 7 }
|
||||
const duplicate = (event) => {
|
||||
assert(event instanceof Event)
|
||||
assert.equal(event.detail, detail)
|
||||
calls.push('duplicate')
|
||||
}
|
||||
env.events.addEventListener('custom', duplicate)
|
||||
env.events.addEventListener('custom', duplicate)
|
||||
assert.equal(env.events.removeEventListener('custom', duplicate), undefined)
|
||||
env.events.addEventListener('custom', () => {
|
||||
calls.push('mutate')
|
||||
env.events.removeEventListener('custom', duplicate)
|
||||
env.events.addEventListener('custom', () => calls.push('new'))
|
||||
})
|
||||
env.events.addEventListener('custom', () => calls.push('last'))
|
||||
assert.equal(env.events.emit('custom', detail), undefined)
|
||||
assert.deepEqual(calls, ['duplicate', 'mutate', 'new'])
|
||||
})
|
||||
|
||||
test(`MediaBunny ${implementation.name}: event errors propagate and explicit nested emits preserve ordering`, () => {
|
||||
const env = environment(implementation)
|
||||
const calls = []
|
||||
env.events.addEventListener('outer', () => {
|
||||
calls.push('outer')
|
||||
env.events.emit('inner')
|
||||
})
|
||||
env.events.addEventListener('inner', () => calls.push('inner'))
|
||||
env.events.addEventListener('outer', () => calls.push('tail'))
|
||||
env.events.emit('outer')
|
||||
assert.deepEqual(calls, ['outer', 'inner', 'tail'])
|
||||
const error = new Error('user callback')
|
||||
env.events.addEventListener('failure', () => {
|
||||
throw error
|
||||
})
|
||||
env.events.addEventListener('failure', () => calls.push('unreachable'))
|
||||
assert.throws(() => env.events.emit('failure'), item => item === error)
|
||||
assert.equal(calls.includes('unreachable'), false)
|
||||
})
|
||||
|
||||
test(`MediaBunny ${implementation.name}: coercion, no-op setters and pseudo ranges keep normal behavior`, () => {
|
||||
const option = { volume: 2, autoplay: true, loop: true, crossOrigin: 'anonymous', poster: 'first.png' }
|
||||
const env = environment(implementation, option)
|
||||
assert.equal(env.canvas.option, option)
|
||||
assert.equal(env.canvas.volume, 2)
|
||||
env.canvas.volume = '0.4'
|
||||
assert.equal(env.canvas.volume, 0.4)
|
||||
assert.equal(env.canvas.muted, false)
|
||||
env.canvas.muted = 'yes'
|
||||
assert.equal(env.canvas.muted, true)
|
||||
env.canvas.playbackRate = '2'
|
||||
env.canvas.playbackRate = 0
|
||||
assert.equal(env.canvas.playbackRate, 2)
|
||||
env.canvas.autoplay = false
|
||||
env.canvas.loop = false
|
||||
env.canvas.crossOrigin = 'use-credentials'
|
||||
assert.deepEqual([env.canvas.autoplay, env.canvas.loop, env.canvas.crossOrigin], [true, true, 'anonymous'])
|
||||
env.canvas.poster = 'next.png'
|
||||
assert.equal(option.poster, 'next.png')
|
||||
Object.defineProperty(env.canvas.engine, 'duration', { value: 12 })
|
||||
Object.defineProperty(env.canvas.engine, 'currentTime', { value: 3 })
|
||||
assert.equal(env.canvas.buffered.end(99), 12)
|
||||
assert.equal(env.canvas.played.end(-1), 3)
|
||||
assert.equal(env.canvas.seekable.start(99), 0)
|
||||
assert.equal(env.canvas.canPlayType('invalid/media'), 'maybe')
|
||||
})
|
||||
|
||||
test(`MediaBunny ${implementation.name}: public forwarding retains exact Promise identity and synchronous void methods`, async () => {
|
||||
const env = environment(implementation)
|
||||
const calls = []
|
||||
const result = Promise.resolve()
|
||||
env.canvas.engine.play = () => result
|
||||
env.canvas.engine.pause = () => calls.push('pause')
|
||||
env.canvas.engine.load = source => calls.push(['load', source])
|
||||
env.canvas.engine.seek = time => calls.push(['seek', time])
|
||||
assert.equal(env.canvas.play(), result)
|
||||
assert.equal(env.canvas.pause(), undefined)
|
||||
env.canvas.src = 'movie.mp4'
|
||||
assert.equal(env.canvas.load(), undefined)
|
||||
env.canvas.currentTime = '3.5'
|
||||
assert.deepEqual(calls, ['pause', ['load', 'movie.mp4'], ['load', 'movie.mp4'], ['seek', 3.5]])
|
||||
await result
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny candidate destroy cancels synthetic frame callbacks including handle zero', () => {
|
||||
const env = environment()
|
||||
let calls = 0
|
||||
const id = env.canvas.requestVideoFrameCallback(() => calls++)
|
||||
assert.equal(id, 0)
|
||||
const queued = env.frames.get(id)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.frames.size, 0)
|
||||
queued(100)
|
||||
assert.equal(calls, 0)
|
||||
env.canvas.requestVideoFrameCallback(() => calls++)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test('MediaBunny candidate teardown stops the current dispatch after a listener destroys the shim', () => {
|
||||
const env = environment()
|
||||
const calls = []
|
||||
env.events.addEventListener('custom', () => {
|
||||
calls.push('destroy')
|
||||
env.shim.destroy()
|
||||
})
|
||||
env.events.addEventListener('custom', () => calls.push('obsolete'))
|
||||
env.events.emit('custom')
|
||||
assert.deepEqual(calls, ['destroy'])
|
||||
assert.equal(env.events.listeners.size, 0)
|
||||
const cursor = env.emitted.length
|
||||
env.events.emit('volumechange')
|
||||
assert.equal(env.emitted.length, cursor)
|
||||
})
|
||||
|
||||
test('MediaBunny candidate teardown releases event and frame resources even when engine cleanup throws', () => {
|
||||
const env = environment()
|
||||
const failure = new Error('cleanup failed')
|
||||
env.canvas.requestVideoFrameCallback(() => {})
|
||||
env.canvas.engine.destroy = () => {
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => env.shim.destroy(), error => error === failure)
|
||||
assert.equal(env.events.listeners.size, 0)
|
||||
assert.equal(env.frames.size, 0)
|
||||
assert.doesNotThrow(() => env.shim.destroy())
|
||||
})
|
||||
Reference in new issue
Block a user