Files
ArtPlayer/test/thumbnail-policy.test.js
T

189 lines
7.0 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Approved runtime policy compatibility.
import test from 'node:test'
import { thumbnailCandidate, thumbnailEnvironment, thumbnailHistorical } from './helpers/thumbnail.js'
const candidate = await thumbnailCandidate()
const flush = () => new Promise(resolve => setImmediate(resolve))
function create(option = {}) {
const env = thumbnailEnvironment(candidate)
env.input = new env.Element('input')
env.tool = new env.Factory({ fileInput: env.input, number: 10, width: 40, height: 25, ...option })
return env
}
function tick(env, delay) {
const selected = [...env.timers].find(([, timer]) => Object.is(timer.delay, delay))
assert(selected, `Expected owned timer for ${delay} ms`)
env.timers.delete(selected[0])
selected[1].callback()
}
function ready(env) {
env.tool.video.duration = 100
env.tool.video.readyState = 2
env.tool.video.videoWidth = 160
env.tool.video.videoHeight = 90
env.tool.loadVideo({ name: 'sample.mp4', type: 'video/mp4' })
for (const fn of env.tool.video.handlers.get('loadedmetadata'))
fn()
}
test('Thumbnail root defaults match the recovered published profile, including key order', () => {
const actual = create()
const old = thumbnailEnvironment(thumbnailHistorical().find(item => item.legacy))
assert.deepEqual(Object.keys(actual.Factory.DEFAULTS), Object.keys(old.Factory.DEFAULTS))
for (const key of Object.keys(old.Factory.DEFAULTS))
assert.equal(actual.Factory.DEFAULTS[key], old.Factory.DEFAULTS[key])
actual.tool.destroy()
})
test('Thumbnail published policy validates and clamps delay/height while workspace preserves its inputs', () => {
const published = create({ height: 2, delay: 4000 })
assert.equal(published.tool.option.height, 10)
assert.equal(published.tool.option.delay, 1000)
assert.throws(() => published.tool.setup({ delay: 'bad' }), /delay.*number/)
assert.throws(() => published.tool.setup({ height: 'bad' }), /height.*number/)
assert.throws(() => published.tool.setup({ compatibility: 'unknown' }), /compatibility/)
const workspace = create({ compatibility: 'workspace-4.4', height: 2, delay: 4000 })
assert.equal(workspace.tool.option.height, 2)
assert.equal(workspace.tool.option.delay, 4000)
published.tool.destroy()
workspace.tool.destroy()
})
for (const compatibility of [undefined, 'published-3.5', 'workspace-4.4']) {
test(`Thumbnail ${compatibility || 'default'} preserves file/video timing and input reset policy`, () => {
const env = create({ compatibility, delay: 25 })
const seen = []
env.input.files = [{ name: 'sample.mp4', type: 'video/mp4' }]
env.input.value = 'selected'
env.tool.on('file', () => seen.push(['file', env.tool.video.src]))
env.tool.on('video', video => seen.push(['video', video.src]))
env.tool.inputChange({ target: env.input })
const workspace = compatibility === 'workspace-4.4'
assert.deepEqual(seen, workspace ? [['file', undefined], ['video', env.tool.videoUrl]] : [['file', undefined]])
assert.equal(env.input.value, workspace ? '' : 'selected')
if (!workspace) {
tick(env, 25)
assert.deepEqual(seen[1], ['video', env.tool.videoUrl])
}
env.tool.destroy()
assert.equal(env.timers.size, 0)
})
}
test('Thumbnail default extraction keeps configured height and requires delay plus frame readiness', async () => {
const env = create({ delay: 25 })
ready(env)
tick(env, 25)
env.tool.video.seeking = true
const work = env.tool.start()
await flush()
assert.equal(env.tool.option.height, 25)
tick(env, 25)
assert.equal(env.operations.filter(op => op.name === 'drawImage').length, 0)
env.tool.video.seeking = false
env.tool.video.oncanplay()
await flush()
assert.equal(env.operations.filter(op => op.name === 'drawImage').length, 1)
env.tool.destroy()
await assert.rejects(work, { name: 'AbortError' })
assert.equal(env.timers.size, 0)
})
test('Thumbnail delayed video notifications are cancelled by replacement, failure and destroy', () => {
for (const action of ['replace', 'error', 'destroy']) {
const env = create({ delay: 25 })
const seen = []
env.tool.on('video', () => seen.push(env.tool.file.name)).on('error', () => {})
env.tool.loadVideo({ name: 'old.mp4', type: 'video/mp4' })
const stale = [...env.timers.values()].find(timer => timer.delay === 25)
assert(stale)
if (action === 'replace') {
env.tool.loadVideo({ name: 'new.mp4', type: 'video/mp4' })
}
else if (action === 'error') {
for (const fn of env.tool.video.handlers.get('error'))
fn()
}
else {
env.tool.destroy()
}
stale.callback()
assert.deepEqual(seen, [])
if (action === 'replace') {
tick(env, 25)
assert.deepEqual(seen, ['new.mp4'])
}
env.tool.destroy()
assert.equal(env.timers.size, 0)
}
})
test('Thumbnail numeric NaN delay keeps the historical asynchronous timer boundary', () => {
const env = create({ delay: Number.NaN })
let events = 0
env.tool.on('video', () => events++)
env.tool.loadVideo({ name: 'sample.mp4', type: 'video/mp4' })
assert.equal(events, 0)
tick(env, Number.NaN)
assert.equal(events, 1)
env.tool.destroy()
})
for (const finish of ['complete', 'destroy', 'replace', 'error']) {
test(`Thumbnail default final delay owns completion and ${finish} settlement`, async () => {
const env = create({ delay: 25 })
ready(env)
tick(env, 25)
let done = 0
const updates = []
env.tool.on('done', () => done++).on('update', (_, progress) => updates.push(progress))
const work = env.tool.start()
await flush()
for (let frame = 0; frame < 10; frame++) {
tick(env, 25)
await flush()
}
assert.deepEqual(updates, Array.from({ length: 10 }, (_, index) => (index + 1) / 10))
assert.equal(done, 0)
assert.equal(env.tool.processing, true)
const final = [...env.timers.values()].find(timer => timer.delay === 50)
assert(final)
if (finish === 'complete') {
tick(env, 50)
await work
assert.equal(done, 1)
}
else {
if (finish === 'destroy') {
env.tool.destroy()
}
else if (finish === 'replace') {
env.tool.loadVideo({ name: 'next.mp4', type: 'video/mp4' })
}
else {
env.tool.on('error', () => {})
for (const fn of [...env.tool.video.handlers.get('error')])
fn()
}
await assert.rejects(work, finish === 'error' ? /Unable to load video/ : { name: 'AbortError' })
final.callback()
assert.equal(done, 0)
}
assert.equal(env.tool.processing, false)
env.tool.destroy()
assert.equal(env.timers.size, 0)
})
}
test('Thumbnail explicit workspace extraction retains aspect height with no policy timers', async () => {
const env = create({ compatibility: 'workspace-4.4', delay: 25 })
ready(env)
const work = env.tool.start()
await work
assert.equal(env.tool.option.height, 22.5)
assert.equal(env.operations.filter(op => op.name === 'drawImage').length, 10)
assert.equal(env.timers.size, 0)
env.tool.destroy()
})