mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-27 17:51:20 -08:00
test(canvas): [PKG-CANVAS-02] reproduce frame leaks and native pixel boundaries
This commit is contained in:
1 parent
5e26d2298d
commit
e931520e66
11 files changed
+642
-12
No files matched your search
@@ -0,0 +1,173 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Node historical regression runner.
|
||||
import test from 'node:test'
|
||||
import { canvasEnvironment, canvasHistorical } from './helpers/canvas.js'
|
||||
|
||||
function deferred() {
|
||||
let resolve, reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
for (const implementation of await canvasHistorical()) {
|
||||
test(`Canvas ${implementation.name}: historical pending bitmap draws and reschedules after pause or destroy`, async () => {
|
||||
for (const operation of ['video:pause', 'destroy']) {
|
||||
const pending = deferred()
|
||||
let closed = 0
|
||||
const bitmap = { close() {
|
||||
closed++
|
||||
} }
|
||||
const env = await canvasEnvironment(implementation, { createImageBitmap: () => pending.promise })
|
||||
env.factory()(env.art)
|
||||
env.art.emit('video:play')
|
||||
env.art.emit(operation)
|
||||
assert.equal(env.frames.size, 0)
|
||||
pending.resolve(bitmap)
|
||||
await env.flush()
|
||||
assert.equal(env.draws.length, 1)
|
||||
assert.equal(closed, 1)
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.emitted.at(-1).name, 'artplayerProxyCanvas:draw')
|
||||
}
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical repeated play creates concurrent stale bitmap loops`, async () => {
|
||||
const pending = [deferred(), deferred()]
|
||||
let index = 0
|
||||
const env = await canvasEnvironment(implementation, { createImageBitmap: () => pending[index++].promise })
|
||||
env.factory()(env.art)
|
||||
env.art.emit('video:play')
|
||||
env.art.emit('video:play')
|
||||
const newer = { close() {} }
|
||||
const older = { close() {} }
|
||||
pending[1].resolve(newer)
|
||||
await env.flush()
|
||||
pending[0].resolve(older)
|
||||
await env.flush()
|
||||
assert.deepEqual(env.draws.map(args => args[0]), [newer, older])
|
||||
assert.equal(env.frames.size, 2)
|
||||
env.art.destroy()
|
||||
assert.equal(env.frames.size, 1, 'Only the latest stored RAF handle is cancelled')
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical draw failure emits its error but leaks the acquired bitmap`, async () => {
|
||||
const failure = new Error('draw failed')
|
||||
let closed = 0
|
||||
const bitmap = { close() {
|
||||
closed++
|
||||
} }
|
||||
const env = await canvasEnvironment(implementation, { createImageBitmap: () => Promise.resolve(bitmap), onDraw() {
|
||||
throw failure
|
||||
} })
|
||||
env.factory()(env.art)
|
||||
env.art.emit('video:play')
|
||||
await env.flush()
|
||||
assert.equal(closed, 0)
|
||||
assert.equal(env.emitted.at(-1).name, 'artplayerProxyCanvas:error')
|
||||
assert.equal(env.emitted.at(-1).args[0], failure)
|
||||
assert.equal(env.frames.size, 1)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical bitmap rejection emits the original error without direct-video fallback`, async () => {
|
||||
const failure = new Error('bitmap rejected')
|
||||
const env = await canvasEnvironment(implementation, { createImageBitmap: () => Promise.reject(failure) })
|
||||
env.factory()(env.art)
|
||||
env.art.emit('video:play')
|
||||
await env.flush()
|
||||
assert.equal(env.draws.length, 0)
|
||||
assert.equal(env.emitted.at(-1).name, 'artplayerProxyCanvas:error')
|
||||
assert.equal(env.emitted.at(-1).args[0], failure)
|
||||
assert.equal(env.frames.size, 1)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: callback errors occur after bitmap close and replace the draw event with an error`, async () => {
|
||||
const failure = new Error('callback failed')
|
||||
let closed = 0
|
||||
const env = await canvasEnvironment(implementation, { createImageBitmap: () => Promise.resolve({ close() {
|
||||
closed++
|
||||
} }) })
|
||||
env.factory(() => {
|
||||
throw failure
|
||||
})(env.art)
|
||||
env.art.emit('resize')
|
||||
await env.flush()
|
||||
assert.equal(closed, 1)
|
||||
assert.equal(env.emitted.filter(event => event.name === 'artplayerProxyCanvas:draw').length, 0)
|
||||
assert.equal(env.emitted.at(-1).args[0], failure)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical callback destruction still emits draw and queues a new frame`, async () => {
|
||||
const env = await canvasEnvironment(implementation)
|
||||
env.factory(() => env.art.destroy())(env.art)
|
||||
env.art.emit('video:play')
|
||||
await env.flush()
|
||||
assert.deepEqual(env.emitted.map(event => event.name), ['video:play', 'destroy', 'artplayerProxyCanvas:draw'])
|
||||
assert.equal(env.frames.size, 1)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical destroy before setup timeout retains host handlers and attaches media listeners later`, async () => {
|
||||
const env = await canvasEnvironment(implementation)
|
||||
env.factory()(env.art)
|
||||
env.art.destroy()
|
||||
assert.equal(env.timers.size, 1)
|
||||
assert.equal([...env.listeners.values()].reduce((count, set) => count + set.size, 0), 5)
|
||||
env.flushTimers()
|
||||
assert.equal(env.proxies.length, 5)
|
||||
env.emitVideo({ type: 'play' })
|
||||
await env.flush()
|
||||
assert.equal(env.frames.size, 1)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical late attachment to a destroyed host still registers and draws`, async () => {
|
||||
const env = await canvasEnvironment(implementation)
|
||||
env.art.destroy()
|
||||
const result = env.factory()(env.art)
|
||||
assert.equal(result, env.canvas)
|
||||
env.flushTimers()
|
||||
env.emitVideo({ type: 'play' })
|
||||
await env.flush()
|
||||
assert.equal(env.proxies.length, 5)
|
||||
assert.equal(env.draws.length, 1)
|
||||
assert.equal(env.frames.size, 1)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical setup subscription failure leaves deferred work and the partial host registration`, async () => {
|
||||
const env = await canvasEnvironment(implementation)
|
||||
const on = env.art.on
|
||||
const failure = new Error('subscription failed')
|
||||
env.art.on = (...args) => {
|
||||
on(...args)
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
assert.equal(env.timers.size, 1)
|
||||
assert.equal([...env.listeners.values()].reduce((count, set) => count + set.size, 0), 1)
|
||||
env.flushTimers()
|
||||
assert.equal(env.proxies.length, 5)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical zero media dimensions reach unguarded resize and drawing`, async () => {
|
||||
const env = await canvasEnvironment(implementation)
|
||||
env.video.videoWidth = 0
|
||||
env.video.videoHeight = 0
|
||||
env.factory()(env.art)
|
||||
env.art.emit('resize')
|
||||
await env.flush()
|
||||
assert(Number.isNaN(env.canvas.height), 'This controlled element does not apply native unsigned dimension coercion')
|
||||
assert.equal(env.canvas.style.padding, 'NaNpx 0px')
|
||||
assert.equal(env.draws.length, 1)
|
||||
})
|
||||
|
||||
test(`Canvas ${implementation.name}: historical missing 2D context repeats a TypeError path with no capability guard`, async () => {
|
||||
const env = await canvasEnvironment(implementation, { noContext: true })
|
||||
env.factory()(env.art)
|
||||
env.art.emit('video:play')
|
||||
await env.flush()
|
||||
assert.equal(env.emitted.at(-1).name, 'artplayerProxyCanvas:error')
|
||||
assert.equal(env.emitted.at(-1).args[0].name, 'TypeError')
|
||||
assert.equal(env.frames.size, 1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user