mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
260 lines
11 KiB
JavaScript
260 lines
11 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
// eslint-disable-next-line test/no-import-node-test -- Historical extraction and resource baseline.
|
|
import test from 'node:test'
|
|
import { thumbnailEnvironment, thumbnailHistorical } from './helpers/thumbnail.js'
|
|
|
|
const flush = () => new Promise(resolve => setImmediate(resolve))
|
|
function observe(promise) {
|
|
const state = { status: 'pending' }
|
|
promise.then(value => Object.assign(state, { status: 'resolved', value }), error => Object.assign(state, { status: 'rejected', error }))
|
|
return state
|
|
}
|
|
async function tick(env) {
|
|
const next = env.timers.entries().next().value
|
|
assert(next, 'Expected a scheduled timer')
|
|
env.timers.delete(next[0])
|
|
next[1].callback()
|
|
await flush()
|
|
return next[1].delay
|
|
}
|
|
async function frame(env, legacy) {
|
|
await flush()
|
|
if (legacy) {
|
|
await tick(env)
|
|
}
|
|
else {
|
|
assert.equal(typeof env.instance.video.oncanplay, 'function')
|
|
env.instance.video.oncanplay()
|
|
await flush()
|
|
}
|
|
}
|
|
|
|
for (const implementation of thumbnailHistorical()) {
|
|
async function setup(options = {}) {
|
|
const env = thumbnailEnvironment(implementation)
|
|
env.instance = new env.Factory({ fileInput: new env.Element('input'), number: 10, width: 40, height: 30, column: 3, ...options })
|
|
env.instance.loadVideo({ name: 'first.mp4', type: 'video/mp4' })
|
|
if (implementation.legacy)
|
|
await tick(env)
|
|
env.instance.video.duration = 100
|
|
return env
|
|
}
|
|
async function complete(env, state, count = 10) {
|
|
for (let index = 0; index < count; index++)
|
|
await frame(env, implementation.legacy)
|
|
if (implementation.legacy) {
|
|
assert.equal(state.status, 'pending')
|
|
assert.equal(await tick(env), env.instance.option.delay * 2)
|
|
}
|
|
await flush()
|
|
assert.equal(state.status, 'resolved')
|
|
assert.equal(state.value, undefined)
|
|
}
|
|
|
|
test(`Thumbnail ${implementation.name}: sequential extraction, progress, done and rerun`, async () => {
|
|
const env = await setup({ begin: 10, end: 90 })
|
|
const seen = []
|
|
env.instance.on('canvas', canvas => seen.push(['canvas', canvas.width, canvas.height, env.instance.processing]))
|
|
env.instance.on('update', (url, progress) => seen.push(['update', url, progress, env.instance.processing]))
|
|
env.instance.on('done', () => seen.push(['done', env.instance.processing]))
|
|
const state = observe(env.instance.start())
|
|
assert.equal(env.instance.duration, 80)
|
|
assert.equal(env.instance.density, 0.125)
|
|
const height = implementation.legacy ? 30 : 22.5
|
|
assert.deepEqual(seen, [['canvas', 120, height * 4 + 30, false]])
|
|
await complete(env, state)
|
|
assert.deepEqual(seen.filter(item => item[0] === 'update').map(item => [item[2], item[3]]), Array.from({ length: 10 }, (_, i) => [(i + 1) / 10, true]))
|
|
assert.deepEqual(seen.at(-1), ['done', false])
|
|
const draws = env.operations.filter(item => item.name === 'drawImage')
|
|
assert.equal(draws.length, 10)
|
|
assert.deepEqual(draws.map(item => item.args.slice(1)), Array.from({ length: 10 }, (_, i) => [i % 3 * 40, Math.floor(i / 3) * height, 40, height]))
|
|
assert.equal(env.instance.video.currentTime, 86)
|
|
assert.equal(env.operations.filter(item => item.name === 'revokeURL').length, 9)
|
|
const second = observe(env.instance.start())
|
|
await complete(env, second)
|
|
assert.equal(seen.filter(item => item[0] === 'done').length, 2)
|
|
env.instance.destroy()
|
|
assert.equal(env.operations.filter(item => item.name === 'revokeURL').length, 21)
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: range and density guards emit before throwing`, async () => {
|
|
for (const [options, pattern] of [[{ begin: 50, end: 20 }, /End time/], [{ begin: 1000 }, /End time/], [{ begin: 0, end: 5 }, /density/]]) {
|
|
const env = await setup(options)
|
|
const errors = []
|
|
env.instance.on('error', message => errors.push(message))
|
|
assert.throws(() => env.instance.start(), pattern)
|
|
assert.equal(errors.length, 1)
|
|
assert.equal(env.instance.processing, false)
|
|
assert.equal(env.operations.filter(item => item.name === 'drawImage').length, 0)
|
|
env.instance.destroy()
|
|
}
|
|
const env = await setup({ begin: -20, end: 200 })
|
|
const state = observe(env.instance.start())
|
|
assert.equal(env.instance.option.begin, 0)
|
|
assert.equal(env.instance.option.end, 100)
|
|
await complete(env, state)
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: active duplicate start/download reject without replacing extraction`, async () => {
|
|
const env = await setup()
|
|
const state = observe(env.instance.start())
|
|
await frame(env, implementation.legacy)
|
|
assert.throws(() => env.instance.start(), /task in progress/)
|
|
assert.throws(() => env.instance.download(), /task in progress/)
|
|
await complete(env, state, 9)
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: blob callback gates subsequent frames`, async () => {
|
|
const env = await setup()
|
|
env.controls.deferBlobs = true
|
|
const state = observe(env.instance.start())
|
|
await frame(env, implementation.legacy)
|
|
assert.equal(env.blobs.length, 1)
|
|
assert.equal(env.operations.filter(item => item.name === 'drawImage').length, 1)
|
|
assert.equal(env.instance.video.currentTime, 5)
|
|
assert.equal(state.status, 'pending')
|
|
env.controls.deferBlobs = false
|
|
env.blobs.shift()({ kind: 'image/png' })
|
|
await flush()
|
|
await complete(env, state, 9)
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: null blob callback throws outside the unsettled extraction promise`, async () => {
|
|
const env = await setup()
|
|
env.controls.deferBlobs = true
|
|
const state = observe(env.instance.start())
|
|
await frame(env, implementation.legacy)
|
|
assert.throws(() => env.blobs.shift()(null), /Blob required/)
|
|
await flush()
|
|
assert.equal(state.status, 'pending')
|
|
assert.equal(env.instance.processing, true)
|
|
env.instance.destroy()
|
|
await flush()
|
|
assert.equal(state.status, 'pending')
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: late blob after destroy creates an unowned URL and continues work`, async () => {
|
|
const env = await setup()
|
|
env.controls.deferBlobs = true
|
|
const events = []
|
|
env.instance.on('destroy', () => events.push('destroy'))
|
|
env.instance.on('update', () => events.push('update'))
|
|
const state = observe(env.instance.start())
|
|
await frame(env, implementation.legacy)
|
|
env.instance.destroy()
|
|
env.blobs.shift()({ kind: 'image/png' })
|
|
await flush()
|
|
assert.deepEqual(events, ['destroy', 'update'])
|
|
assert.equal(state.status, 'pending')
|
|
assert.equal(env.instance.video.parentNode, null)
|
|
assert.equal(env.instance.processing, true)
|
|
const last = env.operations.filter(item => item.name === 'createURL').at(-1).url
|
|
assert.equal(env.operations.some(item => item.name === 'revokeURL' && item.url === last), false)
|
|
assert(implementation.legacy ? env.timers.size > 0 : typeof env.instance.video.oncanplay === 'function')
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: file replacement leaks old URL and preserves the running task`, async () => {
|
|
const env = await setup()
|
|
const first = env.instance.videoUrl
|
|
const state = observe(env.instance.start())
|
|
await frame(env, implementation.legacy)
|
|
const next = { name: 'second.mp4', type: 'video/mp4' }
|
|
env.instance.loadVideo(next)
|
|
assert.notEqual(env.instance.videoUrl, first)
|
|
assert.equal(env.instance.file, next)
|
|
assert.equal(env.instance.processing, true)
|
|
assert.equal(state.status, 'pending')
|
|
env.instance.destroy()
|
|
assert.equal(env.operations.some(item => item.name === 'revokeURL' && item.url === first), false)
|
|
assert.equal(env.instance.processing, true)
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: missing metadata start and destruction do not have owned cancellation`, async () => {
|
|
const env = await setup()
|
|
env.instance.video.duration = 0
|
|
const state = observe(env.instance.start())
|
|
assert.equal(env.timers.size, 1)
|
|
env.instance.destroy()
|
|
assert.equal(env.timers.size, 1)
|
|
assert.equal(await tick(env), 1000)
|
|
assert.equal(state.status, implementation.legacy ? 'rejected' : 'pending')
|
|
if (implementation.legacy)
|
|
assert.equal(state.error.name, 'TypeError')
|
|
else
|
|
assert.equal(env.timers.size, 1)
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: changed input and generated wrapper remain unowned`, async () => {
|
|
const env = await setup()
|
|
const first = env.instance.option.fileInput
|
|
const wrapper = new env.Element('div')
|
|
env.instance.setup({ fileInput: wrapper })
|
|
assert.equal(wrapper.children.length, 1)
|
|
assert.equal(env.instance.option.fileInput.handlers.size, 0)
|
|
assert.equal(first.handlers.get('change').size, 1)
|
|
env.instance.destroy()
|
|
assert.equal(first.handlers.get('change').size, 1)
|
|
assert.equal(wrapper.children.length, 1)
|
|
assert.equal(wrapper.style.position, 'relative')
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: currentTime setter failure rejects and releases processing`, async () => {
|
|
const env = await setup()
|
|
const failure = new Error('seek assignment failed')
|
|
const errors = []
|
|
env.instance.on('error', message => errors.push(message))
|
|
Object.defineProperty(env.instance.video, 'currentTime', {
|
|
get() { return 0 },
|
|
set() {
|
|
throw failure
|
|
},
|
|
})
|
|
const state = observe(env.instance.start())
|
|
await flush()
|
|
assert.equal(state.status, 'rejected')
|
|
assert.equal(state.error, failure)
|
|
assert.equal(env.instance.processing, false)
|
|
assert.deepEqual(errors, ['seek assignment failed'])
|
|
env.instance.destroy()
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: throwing done listener rejects with historical error multiplicity`, async () => {
|
|
const env = await setup()
|
|
const failure = new Error('done listener failed')
|
|
const errors = []
|
|
env.instance.on('error', message => errors.push(message))
|
|
env.instance.on('done', () => {
|
|
throw failure
|
|
})
|
|
const state = observe(env.instance.start())
|
|
for (let index = 0; index < 10; index++)
|
|
await frame(env, implementation.legacy)
|
|
if (implementation.legacy)
|
|
await tick(env)
|
|
await flush()
|
|
assert.equal(state.status, 'rejected')
|
|
assert.equal(state.error, failure)
|
|
assert.equal(env.instance.processing, false)
|
|
assert.deepEqual(errors, Array.from({ length: implementation.legacy ? 2 : 1 }).fill('done listener failed'))
|
|
env.instance.destroy()
|
|
})
|
|
|
|
test(`Thumbnail ${implementation.name}: update listener failure in deferred callback leaves work pending`, async () => {
|
|
const env = await setup()
|
|
env.controls.deferBlobs = true
|
|
const failure = new Error('update listener failed')
|
|
env.instance.on('update', () => {
|
|
throw failure
|
|
})
|
|
const state = observe(env.instance.start())
|
|
await frame(env, implementation.legacy)
|
|
assert.throws(() => env.blobs.shift()({ kind: 'image/png' }), error => error === failure)
|
|
await flush()
|
|
assert.equal(state.status, 'pending')
|
|
assert.equal(env.instance.processing, true)
|
|
assert.equal(typeof env.instance.thumbnailUrl, 'string')
|
|
env.instance.destroy()
|
|
assert.equal(state.status, 'pending')
|
|
})
|
|
}
|