test(mediabunny): [PKG-MB-02] checkpoint native playback and late load failures

This commit is contained in:
Harvey Zhao committed 2026-09-12 17:12:59 +08:00
1 parent 1cfd2e17f3
commit 63c8938dd7
10 files changed
+908 -10

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import { hash } from '../../refactor/scripts/releases.mjs'
import { mbHistorical } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementations = (await mbHistorical()).filter(item => item.name.startsWith('published'))
for (const implementation of implementations) {
test(`MediaBunny ${implementation.name}: native MP4 playback or exact Windows WebKit capability failure`, async ({ page, browserName }, 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;
})();` })
await page.evaluate(() => {
window.mbEvents = []
window.mbActions = []
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, proxy: window.mbFactory() })
for (const name of window.Artplayer.config.events) {
window.art.on(`video:${name}`, () => window.mbEvents.push({ name, time: performance.now(), currentTime: window.art.video.currentTime, readyState: window.art.video.readyState }))
}
document.querySelector('#play').onclick = () => window.art.play().catch(error => window.mbActions.push({ name: error.name, message: error.message }))
})
const capabilities = await page.evaluate(() => ({
secure: isSecureContext,
VideoDecoder: typeof VideoDecoder,
AudioDecoder: typeof AudioDecoder,
AudioContext: typeof AudioContext,
webkitAudioContext: typeof window.webkitAudioContext,
coreVersion: window.Artplayer.version,
}))
let outcome = 'unverified'
try {
expect(capabilities.secure).toBe(true)
expect(capabilities.coreVersion).toBe('5.4.0')
if (capabilities.VideoDecoder === 'undefined' || (capabilities.AudioContext === 'undefined' && capabilities.webkitAudioContext === 'undefined')) {
expect(browserName).toBe('webkit')
expect(capabilities).toMatchObject({ VideoDecoder: 'undefined', AudioDecoder: 'undefined', AudioContext: 'undefined', webkitAudioContext: 'undefined' })
await expect.poll(() => page.evaluate(() => window.art.video.error?.code)).toBe(4)
expect(await page.evaluate(() => window.art.video.error.message)).toMatch(/not a constructor/)
expect(await page.evaluate(() => ({ ready: window.art.isReady, width: window.art.video.videoWidth, height: window.art.video.videoHeight }))).toEqual({ ready: false, width: 0, height: 0 })
outcome = 'verified-unsupported-capability-control'
}
else {
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.15)
const pixels = await page.evaluate(() => {
const canvas = window.art.video
const context = canvas.getContext('2d')
return { size: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], corner: [...context.getImageData(0, 0, 1, 1).data], center: [...context.getImageData(160, 90, 1, 1).data] }
})
expect(pixels.size).toEqual([320, 180, 320, 180])
expect(pixels.corner[0]).toBeGreaterThan(200)
expect(pixels.center[2]).toBeGreaterThan(200)
expect([pixels.corner[3], pixels.center[3]]).toEqual([255, 255])
await page.evaluate(() => window.art.video.pause())
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
expect(await page.evaluate(() => window.mbActions)).toEqual([])
outcome = 'native-mp4-playback-observed'
}
}
finally {
const state = await page.evaluate(() => {
const canvas = window.art.video
return { events: window.mbEvents, actions: window.mbActions, ready: window.art.isReady, dimensions: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], paused: canvas.paused, currentTime: canvas.currentTime, error: canvas.error, readyState: canvas.readyState, audioState: canvas.engine?.audio?.audioContext?.state }
})
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance.' }) })
await page.evaluate(() => window.art.destroy(false))
}
})
}
+2 -2
View File
@@ -65,8 +65,8 @@ export function mbEnvironment({ code }, option = {}) {
URL,
console,
performance,
setTimeout,
clearTimeout,
setTimeout: option.setTimeout || setTimeout,
clearTimeout: option.clearTimeout || clearTimeout,
requestAnimationFrame(fn) {
const id = nextFrame++
frames.set(id, fn)
+43
View File
@@ -0,0 +1,43 @@
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'
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)
})
}