Files
ArtPlayer/test/thumbnail-input.test.js

219 lines
9.2 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Candidate fixes also run against the frozen baseline.
import test from 'node:test'
import { thumbnailCandidate, thumbnailEnvironment } from './helpers/thumbnail.js'
const implementation = await thumbnailCandidate()
function setup(wrapper = false) {
const env = thumbnailEnvironment(implementation)
const target = new env.Element(wrapper ? 'div' : 'input')
const instance = new env.Factory({ fileInput: target, number: 10, width: 20, height: 30, column: 3, compatibility: 'workspace-4.4' })
return { ...env, target, instance }
}
test('Thumbnail input ownership preserves class methods, constructor fields and defaults', () => {
const { instance, Factory, exported } = setup()
assert.equal(typeof exported, 'function')
assert.equal(exported.default, undefined)
assert.deepEqual(Object.keys(instance), ['processing', 'option', 'video', 'duration', 'inputChange', 'ondrop'])
assert.deepEqual(Object.getOwnPropertyNames(Factory.prototype), ['constructor', 'ondrop', 'setup', 'inputChange', 'loadVideo', 'start', 'creatScreenshotDate', 'creatCanvas', 'download', 'errorHandle', 'destroy'])
assert.deepEqual(Object.keys(Factory.DEFAULTS), ['delay', 'number', 'width', 'height', 'column', 'begin', 'end'])
assert.equal(instance.setup({ custom: 7 }), instance)
assert.equal(instance.option.custom, 7)
instance.destroy()
})
test('Thumbnail actual registered drop callback loads the first file and prevents default', () => {
const { instance, target } = setup()
const file = { name: 'drop.mp4', type: 'video/mp4' }
let prevented = 0
for (const callback of target.handlers.get('drop') || [])
callback({ preventDefault() { prevented++ }, dataTransfer: { files: [file] } })
assert.equal(instance.file, file)
assert.equal(prevented, 1)
assert.equal(target.handlers.get('drop').has(instance.ondrop), true)
instance.destroy()
})
test('Thumbnail replacing input moves all listeners and reads the new file', () => {
const { instance, target, Element } = setup()
const next = new Element('input')
assert.equal(instance.setup({ fileInput: next }), instance)
for (const name of ['change', 'dragover', 'drop']) {
assert.equal(target.handlers.get(name)?.size || 0, 0)
assert.equal(next.handlers.get(name)?.size, 1)
}
next.files = [{ name: 'new.mp4', type: 'video/mp4' }]
next.value = 'new.mp4'
for (const callback of next.handlers.get('change'))
callback({ target: next })
assert.equal(instance.file, next.files[0])
assert.equal(next.value, '')
instance.destroy()
assert.equal(next.handlers.get('change').size, 0)
})
test('Thumbnail repeated wrapper setup reuses the owned input and restores replaced wrapper style', () => {
const env = thumbnailEnvironment(implementation)
const first = new env.Element('div')
first.style.position = 'sticky'
const instance = new env.Factory({ fileInput: first })
const input = instance.option.fileInput
instance.setup({ fileInput: first })
assert.equal(first.children.length, 1)
assert.equal(instance.option.fileInput, input)
const second = new env.Element('div')
second.style.position = 'absolute'
instance.setup({ fileInput: second })
assert.equal(first.children.length, 0)
assert.equal(first.style.position, 'sticky')
assert.equal(input.handlers.get('change').size, 0)
instance.destroy()
assert.equal(second.children.length, 0)
assert.equal(second.style.position, 'absolute')
})
test('Thumbnail does not overwrite a wrapper style changed by its owner', () => {
const { instance, target } = setup(true)
target.style.position = 'fixed'
instance.destroy()
assert.equal(target.children.length, 0)
assert.equal(target.style.position, 'fixed')
})
test('Thumbnail setup validation is atomic and cannot append an orphan input', () => {
const { instance, Element } = setup()
const previous = instance.option
const wrapper = new Element('div')
assert.throws(() => instance.setup({ fileInput: wrapper, width: 'invalid' }), /width/)
assert.equal(instance.option, previous)
assert.equal(wrapper.children.length, 0)
assert.equal(wrapper.style.position, undefined)
instance.destroy()
})
test('Thumbnail failed replacement listener installation removes partial listeners and keeps old ownership', () => {
const { instance, target, Element } = setup()
const next = new Element('input')
const failure = new Error('listener failed')
const add = next.addEventListener
next.addEventListener = function (name, callback) {
add.call(this, name, callback)
if (name === 'dragover')
throw failure
}
assert.throws(() => instance.setup({ fileInput: next }), error => error === failure)
assert.equal(instance.option.fileInput, target)
assert.equal(target.handlers.get('change').size, 1)
for (const callbacks of next.handlers.values())
assert.equal(callbacks.size, 0)
instance.destroy()
})
test('Thumbnail failed video attachment rolls back generated input and attached video', () => {
const env = thumbnailEnvironment(implementation)
const wrapper = new env.Element('div')
wrapper.style.position = 'fixed'
const failure = new Error('append failed')
const append = env.body.appendChild
env.body.appendChild = function (child) {
append.call(this, child)
throw failure
}
assert.throws(() => new env.Factory({ fileInput: wrapper }), error => error === failure)
assert.equal(env.body.children.length, 0)
assert.equal(wrapper.children.length, 0)
assert.equal(wrapper.style.position, 'fixed')
})
test('Thumbnail failed constructor listener attachment removes every partial resource', () => {
const env = thumbnailEnvironment(implementation)
const target = new env.Element('input')
const failure = new Error('drop registration failed')
const add = target.addEventListener
target.addEventListener = function (name, callback) {
add.call(this, name, callback)
if (name === 'drop')
throw failure
}
assert.throws(() => new env.Factory({ fileInput: target }), error => error === failure)
assert.equal(env.body.children.length, 0)
for (const callbacks of target.handlers.values())
assert.equal(callbacks.size, 0)
})
test('Thumbnail externally detached video and repeated destroy are safe', () => {
const { instance, body, target } = setup()
let count = 0
instance.on('destroy', () => count++)
body.removeChild(instance.video)
assert.equal(instance.destroy(), undefined)
assert.equal(instance.destroy(), undefined)
assert.equal(count, 1)
for (const callbacks of target.handlers.values())
assert.equal(callbacks.size, 0)
})
test('Thumbnail destroy uses private input ownership despite public option replacement', () => {
const { instance, target, Element } = setup(true)
const ownedInput = instance.option.fileInput
instance.option = { fileInput: new Element('input') }
instance.destroy()
assert.equal(target.children.length, 0)
for (const callbacks of ownedInput.handlers.values())
assert.equal(callbacks.size, 0)
})
test('Thumbnail cleanup continues after listener removal failure and cannot reload after destroy', () => {
const { instance, target, body, operations, Element } = setup()
instance.loadVideo({ name: 'first.mp4', type: 'video/mp4' })
const failure = new Error('remove failed')
const remove = target.removeEventListener
target.removeEventListener = function (name, callback) {
if (name === 'change')
throw failure
remove.call(this, name, callback)
}
let count = 0
instance.on('destroy', () => count++)
assert.throws(() => instance.destroy(), error => error === failure)
assert.equal(body.children.length, 0)
assert.equal(count, 1)
assert.equal(target.handlers.get('dragover').size, 0)
assert.equal(operations.filter(item => item.name === 'revokeURL').length, 1)
const wrapper = new Element('div')
assert.equal(instance.setup({ fileInput: wrapper }), instance)
instance.loadVideo({ name: 'late.mp4', type: 'video/mp4' })
instance.inputChange({ target })
instance.ondrop({ preventDefault() {
throw new Error('should not run')
} })
assert.equal(wrapper.children.length, 0)
assert.equal(operations.filter(item => item.name === 'createURL').length, 1)
assert.equal(instance.destroy(), undefined)
})
test('Thumbnail download removes its temporary anchor even when click fails', () => {
const { instance, Element, body } = setup()
instance.file = { name: 'video.part.mp4' }
instance.thumbnailUrl = 'blob:sheet'
const failure = new Error('click failed')
Element.prototype.click = () => {
throw failure
}
assert.throws(() => instance.download(), error => error === failure)
assert.deepEqual(body.children.map(child => child.tagName), ['VIDEO'])
instance.destroy()
})
test('Thumbnail extracted sheet helpers preserve midpoint coordinates and footer', () => {
const { instance, operations } = setup()
instance.duration = 100
assert.deepEqual(JSON.parse(JSON.stringify(instance.creatScreenshotDate())), Array.from({ length: 10 }, (_, i) => ({ time: 5 + i * 10, x: i % 3 * 20, y: Math.floor(i / 3) * 30 })))
const canvas = instance.creatCanvas()
assert.equal(canvas.width, 60)
assert.equal(canvas.height, 150)
assert.deepEqual(operations.find(item => item.name === 'fillText').args, ['From: https://artplayer.org/, Number: 10, Width: 20, Height: 30, Column: 3', 10, 139])
instance.destroy()
})