mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(jassub): [PKG-JASSUB-02] establish lifecycle failures and native render baseline
This commit is contained in:
1 parent
0cf56b64d2
commit
944e2cc9d0
14 files changed
+4311
-11
No files matched your search
@@ -0,0 +1,239 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Frozen failure observations use the repository runner.
|
||||
import test from 'node:test'
|
||||
import { jassubEnvironment, jassubHistorical } from '../../test/helpers/jassub.js'
|
||||
|
||||
const observations = []
|
||||
const implementations = await jassubHistorical()
|
||||
|
||||
function environment(implementation) {
|
||||
// Expose only VM platform constructors for controlled faults; vendor methods stay unchanged.
|
||||
const code = `${implementation.code}\nObject.defineProperty(module.exports.default || module.exports, '__reviewPlatform', { value: { createCanvas: () => document.createElement('canvas'), setGlobal: (name, value) => { globalThis[name] = value }, ErrorEvent, Video: HTMLVideoElement } });`
|
||||
const env = jassubEnvironment({ ...implementation, code })
|
||||
env.platform = env.factory.__reviewPlatform
|
||||
return env
|
||||
}
|
||||
|
||||
function trackListeners(video) {
|
||||
const listeners = new Map()
|
||||
const add = video.addEventListener
|
||||
const remove = video.removeEventListener
|
||||
video.addEventListener = function (type, callback, ...options) {
|
||||
if (!listeners.has(type))
|
||||
listeners.set(type, new Set())
|
||||
listeners.get(type).add(callback)
|
||||
return add.call(this, type, callback, ...options)
|
||||
}
|
||||
video.removeEventListener = function (type, callback, ...options) {
|
||||
listeners.get(type)?.delete(callback)
|
||||
return remove.call(this, type, callback, ...options)
|
||||
}
|
||||
return () => [...listeners.values()].reduce((count, entries) => count + entries.size, 0)
|
||||
}
|
||||
|
||||
function frames(env) {
|
||||
const pending = new Map()
|
||||
const cancelled = []
|
||||
let next = 0
|
||||
env.platform.Video.prototype.requestVideoFrameCallback = function (callback) {
|
||||
pending.set(++next, { video: this, callback })
|
||||
return next
|
||||
}
|
||||
env.platform.Video.prototype.cancelVideoFrameCallback = function (id) {
|
||||
cancelled.push(id)
|
||||
pending.delete(id)
|
||||
}
|
||||
return {
|
||||
pending,
|
||||
cancelled,
|
||||
deliver(video, metadata) {
|
||||
const entry = [...pending].find(([, row]) => row.video === video)
|
||||
assert(entry, 'must deliver an actually queued frame callback')
|
||||
const [id, row] = entry
|
||||
pending.delete(id)
|
||||
row.callback(0, metadata)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for (const implementation of implementations) {
|
||||
test(`JASSUB failure baseline ${implementation.name}: fallback ratechange forwards the Event instead of the numeric media rate`, async () => {
|
||||
const env = environment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance.setRate(2)
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.at(-1).rate, 2)
|
||||
env.art.video.playbackRate = 1.5
|
||||
const event = new Event('ratechange')
|
||||
env.art.video.dispatchEvent(event)
|
||||
await env.flush()
|
||||
const message = env.workers[0].messages.at(-1)
|
||||
assert.equal(message.target, 'video')
|
||||
assert.equal(message.rate, event)
|
||||
assert.notEqual(message.rate, env.art.video.playbackRate)
|
||||
observations.push({ implementation: implementation.name, case: 'rate-event-payload', mediaRate: 1.5, forwardedType: message.rate.type, directNumericRatePreserved: true })
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: direct destroy followed by art destroy throws on the removed container`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
assert.equal(active(), 7)
|
||||
instance.destroy()
|
||||
assert.equal(active(), 0)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
assert.throws(() => env.emit('destroy'), { code: 'ERR_ASSERTION' })
|
||||
assert.throws(() => instance.destroy(), { code: 'ERR_ASSERTION' })
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
observations.push({ implementation: implementation.name, case: 'repeated-destroy', throws: true, workerTerminations: 1 })
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: setVideo across parents makes destroy throw before releasing the worker`, async () => {
|
||||
const env = environment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const secondParent = new env.parent.constructor()
|
||||
const secondVideo = secondParent.appendChild(new env.platform.Video())
|
||||
const active = trackListeners(secondVideo)
|
||||
instance.setVideo(secondVideo)
|
||||
assert.equal(active(), 7)
|
||||
assert.throws(() => instance.destroy(), { code: 'ERR_ASSERTION' })
|
||||
assert.notEqual(instance._destroyed, true)
|
||||
assert.equal(env.workers[0].terminated, 0)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
assert.equal(active(), 7)
|
||||
observations.push({ implementation: implementation.name, case: 'set-video-parent', workerTerminations: 0, listenersRetained: active(), originalContainerRetained: true })
|
||||
instance.setVideo(env.art.video)
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: a supplied canvas makes the adapter throw after allocating the worker`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const canvas = env.parent.appendChild(env.platform.createCanvas())
|
||||
assert.throws(() => env.factory({ canvas })(env.art), { name: 'TypeError', message: /reading 'style'/ })
|
||||
await env.flush()
|
||||
assert.equal(env.workers.length, 1)
|
||||
assert.equal(env.workers[0].terminated, 0)
|
||||
assert.equal(env.workers[0].messages[0].target, 'init')
|
||||
assert.equal(env.listeners.has('destroy'), false)
|
||||
assert.equal(canvas.parentNode, env.parent)
|
||||
assert.equal(active(), 7)
|
||||
observations.push({ implementation: implementation.name, case: 'custom-canvas', workerTerminations: 0, listenersRetained: active(), hostDestroyHandlerRegistered: false, initSent: true })
|
||||
env.workers[0].terminate()
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: Worker construction failure leaves the already inserted container and video listeners`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const failure = new DOMException('Controlled Worker construction failure', 'SecurityError')
|
||||
env.platform.setGlobal('Worker', class {
|
||||
constructor() { throw failure }
|
||||
})
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
await env.flush()
|
||||
assert.equal(env.workers.length, 0)
|
||||
assert.equal(env.listeners.has('destroy'), false)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
assert.equal(env.parent.children[1].className, 'JASSUB')
|
||||
assert.equal(active(), 7)
|
||||
observations.push({ implementation: implementation.name, case: 'worker-construction-failure', originalErrorPreserved: true, insertedContainerRetained: true, listenersRetained: active() })
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: a previous video's queued frame drives the replacement video and creates a second loop`, async () => {
|
||||
const env = environment(implementation)
|
||||
const clock = frames(env)
|
||||
const oldVideo = env.art.video
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
assert.equal(clock.pending.size, 1)
|
||||
const newVideo = env.createVideo()
|
||||
newVideo.currentTime = 9
|
||||
instance.setVideo(newVideo)
|
||||
await env.flush()
|
||||
assert.equal(clock.pending.size, 2)
|
||||
assert.equal(clock.cancelled.length, 0)
|
||||
clock.deliver(oldVideo, { mediaTime: 123, width: 640, height: 360 })
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.filter(item => item.target === 'demand').at(-1).time, 123)
|
||||
assert.equal([...clock.pending.values()].filter(row => row.video === newVideo).length, 2)
|
||||
observations.push({ implementation: implementation.name, case: 'old-video-frame', replacementCurrentTime: newVideo.currentTime, staleDemandTime: 123, replacementPendingCallbacks: 2, cancelledCallbacks: 0 })
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test(`JASSUB containment baseline ${implementation.name}: destroy before ready terminates and detaches, but initialization continues after termination`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
const worker = env.workers[0]
|
||||
const post = worker.postMessage
|
||||
const afterTermination = []
|
||||
worker.postMessage = function (message) {
|
||||
if (this.terminated)
|
||||
afterTermination.push(message.target)
|
||||
return post.call(this, message)
|
||||
}
|
||||
let settled = false
|
||||
instance._loaded.then(() => {
|
||||
settled = true
|
||||
})
|
||||
instance.setTrack('[Script Info]')
|
||||
instance.destroy()
|
||||
await env.flush()
|
||||
assert.equal(worker.terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
assert.equal(active(), 0)
|
||||
assert.deepEqual(afterTermination, ['init'])
|
||||
assert.equal(settled, false)
|
||||
assert.equal(worker.messages.some(item => item.target === 'setTrack'), false)
|
||||
observations.push({ implementation: implementation.name, case: 'destroy-before-ready', workerTerminations: 1, listenersRetained: 0, postAfterTermination: afterTermination, loadedSettledWithinFixtureFlush: settled })
|
||||
})
|
||||
|
||||
test(`JASSUB containment baseline ${implementation.name}: Worker errors reach the instance and explicit host destroy still releases resources`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
const failure = new Error('Controlled Worker script load failure')
|
||||
const errors = []
|
||||
instance.addEventListener('error', event => errors.push(event.error))
|
||||
env.workers[0].onerror(new env.platform.ErrorEvent('error', { error: failure }))
|
||||
await env.flush()
|
||||
assert.deepEqual(errors, [failure])
|
||||
assert.equal(env.errors.at(-1), failure)
|
||||
assert.equal(env.workers[0].terminated, 0)
|
||||
assert.equal(active(), 7)
|
||||
env.emit('destroy')
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
assert.equal(active(), 0)
|
||||
observations.push({ implementation: implementation.name, case: 'worker-error', exactErrorForwarded: true, automaticallyTerminated: false, explicitHostCleanup: true })
|
||||
})
|
||||
|
||||
test(`JASSUB containment baseline ${implementation.name}: the outstanding frame after destroy does no work and does not rearm`, async () => {
|
||||
const env = environment(implementation)
|
||||
const clock = frames(env)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance.destroy()
|
||||
await env.flush()
|
||||
assert.equal(clock.pending.size, 1)
|
||||
assert.equal(clock.cancelled.length, 0)
|
||||
const before = env.workers[0].messages.length
|
||||
clock.deliver(env.art.video, { mediaTime: 123, width: 640, height: 360 })
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.length, before)
|
||||
assert.equal(clock.pending.size, 0)
|
||||
observations.push({ implementation: implementation.name, case: 'frame-after-destroy', activelyCancelled: false, callbackDoesNoWork: true, callbackRearms: false })
|
||||
})
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
if (process.env.ARTPLAYER_JASSUB_FAILURE_REPORT)
|
||||
fs.writeFileSync(process.env.ARTPLAYER_JASSUB_FAILURE_REPORT, `${JSON.stringify(observations, null, 2)}\n`)
|
||||
})
|
||||
Reference in new issue
Block a user