mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-15 09:06:16 -08:00
refactor(thumbnail): [PKG-TOOL-THUMB-04] type runtime modules and retain emitter attribution
This commit is contained in:
1 parent
efd9f42fb9
commit
15485b6420
37 files changed
+8446
-152
No files matched your search
@@ -0,0 +1,131 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Runtime compatibility tests.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { thumbnailCandidate, thumbnailEnvironment, thumbnailHistorical } from './helpers/thumbnail.js'
|
||||
|
||||
const candidate = await thumbnailCandidate()
|
||||
function create(implementation) {
|
||||
const env = thumbnailEnvironment(implementation)
|
||||
return { ...env, tool: new env.Factory({ fileInput: new env.Element('input') }) }
|
||||
}
|
||||
const referenceModule = { exports: {} }
|
||||
vm.runInNewContext(fs.readFileSync(new URL('../refactor/baselines/thumbnail-vendor/tiny-emitter-2.1.0.txt', import.meta.url), 'utf8'), { module: referenceModule })
|
||||
const Upstream = referenceModule.exports
|
||||
const upstream = () => new Upstream()
|
||||
const emitters = [upstream, ...thumbnailHistorical().map(implementation => () => create(implementation).tool), () => create(candidate).tool]
|
||||
|
||||
test('Thumbnail typed facade preserves descriptors, lazy fields, default class and unknown options', () => {
|
||||
const { tool, Factory, exported } = create(candidate)
|
||||
const baseline = create(thumbnailHistorical().find(item => item.name === 'workspace.js'))
|
||||
const describe = object => Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(object)).map(([name, value]) => [name, {
|
||||
enumerable: value.enumerable,
|
||||
configurable: value.configurable,
|
||||
writable: value.writable,
|
||||
getter: typeof value.get,
|
||||
setter: typeof value.set,
|
||||
type: typeof value.value,
|
||||
arity: typeof value.value === 'function' ? value.value.length : undefined,
|
||||
}]))
|
||||
assert.deepEqual(describe(Factory.prototype), describe(baseline.Factory.prototype))
|
||||
assert.deepEqual(describe(Factory), describe(baseline.Factory))
|
||||
assert.deepEqual(describe(tool), describe(baseline.tool))
|
||||
assert.deepEqual(Object.keys(tool), ['processing', 'option', 'video', 'duration', 'inputChange', 'ondrop'])
|
||||
assert.equal(exported.default, undefined)
|
||||
assert.equal(typeof thumbnailEnvironment(candidate, true).exported, 'function')
|
||||
const metadata = { id: 123 }
|
||||
assert.equal(tool.setup({ custom: metadata, width: 21.5 }), tool)
|
||||
assert.equal(tool.option.custom, metadata)
|
||||
assert.equal(tool.option.width, 21.5)
|
||||
assert.notEqual(Factory.DEFAULTS, Factory.DEFAULTS)
|
||||
assert.deepEqual(Object.keys(Factory.DEFAULTS), Object.keys(baseline.Factory.DEFAULTS))
|
||||
assert(Number.isNaN(Factory.DEFAULTS.end))
|
||||
tool.destroy()
|
||||
})
|
||||
|
||||
test('Thumbnail emitter keeps upstream snapshot order, duplicate removal and original once callback identity', () => {
|
||||
for (const create of emitters) {
|
||||
const emitter = create()
|
||||
const seen = []
|
||||
const callback = value => seen.push(value)
|
||||
assert.equal(emitter.on('value', () => emitter.off('value', callback)), emitter)
|
||||
emitter.on('value', callback).once('value', callback).on('value', callback)
|
||||
emitter.emit('value', 7).emit('value', 8)
|
||||
assert.deepEqual(seen, [7, 7, 7])
|
||||
assert.equal(emitter.off('value'), emitter)
|
||||
assert.equal(emitter.e.value, undefined)
|
||||
emitter.once('removed', callback).off('removed', callback).emit('removed', 9)
|
||||
assert.deepEqual(seen, [7, 7, 7])
|
||||
}
|
||||
})
|
||||
|
||||
test('Thumbnail emitter preserves ctx, variadic arguments, symbol events and ignored callback returns', () => {
|
||||
for (const create of emitters) {
|
||||
const emitter = create()
|
||||
const ctx = { calls: 0 }
|
||||
const name = Symbol('event')
|
||||
function callback(...args) {
|
||||
assert.equal(this, ctx)
|
||||
assert.deepEqual(args, [1, 'two', null])
|
||||
this.calls++
|
||||
return 123
|
||||
}
|
||||
assert.equal(emitter.on(name, callback, ctx), emitter)
|
||||
assert.equal(emitter.once(name, callback, ctx), emitter)
|
||||
assert.equal(emitter.emit(name, 1, 'two', null), emitter)
|
||||
assert.equal(ctx.calls, 2)
|
||||
emitter.off(name, callback).emit(name)
|
||||
assert.equal(ctx.calls, 2)
|
||||
}
|
||||
})
|
||||
|
||||
test('Thumbnail emitter synchronous listener failures retain identity and stop the current snapshot', () => {
|
||||
for (const create of emitters) {
|
||||
const emitter = create()
|
||||
const failure = { reason: 'listener failure' }
|
||||
let later = 0
|
||||
emitter.once('event', () => {
|
||||
throw failure
|
||||
})
|
||||
emitter.on('event', () => later++)
|
||||
assert.throws(() => emitter.emit('event'), error => error === failure)
|
||||
assert.equal(later, 0)
|
||||
emitter.emit('event')
|
||||
assert.equal(later, 1)
|
||||
}
|
||||
})
|
||||
|
||||
test('Thumbnail emitter additions during dispatch wait until the next snapshot', () => {
|
||||
for (const create of emitters) {
|
||||
const emitter = create()
|
||||
const seen = []
|
||||
emitter.once('event', () => {
|
||||
seen.push('first')
|
||||
emitter.on('event', () => seen.push('new'))
|
||||
})
|
||||
emitter.on('event', () => seen.push('second'))
|
||||
emitter.emit('event')
|
||||
assert.deepEqual(seen, ['first', 'second'])
|
||||
emitter.emit('event')
|
||||
assert.deepEqual(seen, ['first', 'second', 'second', 'new'])
|
||||
}
|
||||
})
|
||||
|
||||
test('Thumbnail typed facade retains synchronous validation, option mutation and grid geometry', () => {
|
||||
const { tool } = create(candidate)
|
||||
tool.setup({ number: 10, width: 20, height: 30, column: 3, begin: 5 })
|
||||
tool.duration = 100
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(tool.creatScreenshotDate())), Array.from({ length: 10 }, (_, i) => ({ time: 10 + i * 10, x: i % 3 * 20, y: Math.floor(i / 3) * 30 })))
|
||||
const canvas = tool.creatCanvas()
|
||||
assert.equal(canvas.width, 60)
|
||||
assert.equal(canvas.height, 150)
|
||||
const errors = []
|
||||
tool.on('error', value => errors.push(value))
|
||||
assert.throws(() => tool.setup({ width: '80' }), /not a number/)
|
||||
assert.equal(tool.option.width, 20)
|
||||
assert.throws(() => tool.download(), /does not seem to be ready/)
|
||||
assert.equal(errors.length, 2)
|
||||
assert.equal(tool.errorHandle(true, 'unused'), undefined)
|
||||
tool.destroy()
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Pinned provenance and distributable notice checks.
|
||||
import test from 'node:test'
|
||||
|
||||
const read = file => fs.readFileSync(new URL(`../${file}`, import.meta.url), 'utf8')
|
||||
const base = 'refactor/baselines/thumbnail-vendor/'
|
||||
const license = read(`${base}tiny-emitter-license.txt`).replaceAll('\r\n', '\n').trim()
|
||||
|
||||
test('Thumbnail emitter reference files retain pinned bytes, version and full upstream attribution', () => {
|
||||
const manifest = JSON.parse(read(`${base}sources.json`))
|
||||
for (const source of manifest.sources)
|
||||
assert.equal(createHash('sha256').update(fs.readFileSync(new URL(`../${source.file}`, import.meta.url))).digest('hex'), source.sha256)
|
||||
const pkg = JSON.parse(read(`${base}tiny-emitter-package.json`))
|
||||
assert.equal(pkg.name, 'tiny-emitter')
|
||||
assert.equal(pkg.version, '2.1.0')
|
||||
assert.equal(pkg.license, 'MIT')
|
||||
assert.match(license, /Copyright \(c\) 2017 Scott Corgan/)
|
||||
assert(read('packages/artplayer-tool-thumbnail/THIRD_PARTY_NOTICES').replaceAll('\r\n', '\n').includes(license))
|
||||
})
|
||||
|
||||
test('Thumbnail standalone distributions retain the complete notice and equal their docs copies', () => {
|
||||
for (const suffix of ['.js', '.legacy.js', '.mjs']) {
|
||||
const filename = `artplayer-tool-thumbnail${suffix}`
|
||||
const bundle = read(`packages/artplayer-tool-thumbnail/dist/${filename}`)
|
||||
const banner = bundle.match(/^\/\*![\s\S]*?\*\//)?.[0].replace(/^ \* ?/gm, '').replaceAll('\r\n', '\n')
|
||||
assert(banner?.includes(license), `${filename} must carry the complete upstream notice`)
|
||||
assert.equal(read(`docs/compiled/${filename}`), bundle)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import Thumbnail from '../../packages/artplayer-tool-thumbnail/src'
|
||||
|
||||
const input = document.createElement('input')
|
||||
const tool = new Thumbnail({ fileInput: input, number: 10, tracking: { id: 'example' } })
|
||||
const same: Thumbnail = tool.setup({ width: 80 }).on('update', (url, progress) => {
|
||||
const value: string = url
|
||||
const percent: number = progress
|
||||
void [value, percent]
|
||||
})
|
||||
tool.once('file', function (file) {
|
||||
this.name = file.name
|
||||
}, { name: '' })
|
||||
tool.on('error', (message) => {
|
||||
const value: string | undefined = message
|
||||
void value
|
||||
})
|
||||
tool.on('custom', (...payload) => {
|
||||
const values: unknown[] = payload
|
||||
void values
|
||||
})
|
||||
tool.emit(Symbol('extension'), 'value')
|
||||
const pending: Promise<void> = tool.start()
|
||||
const point: { time: number, x: number, y: number } | undefined = tool.creatScreenshotDate()[0]
|
||||
const canvas: HTMLCanvasElement = tool.creatCanvas()
|
||||
const defaults: number = Thumbnail.DEFAULTS.height
|
||||
const wrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
tool.setup({ fileInput: wrapper })
|
||||
tool.loadVideo(input.files?.[0])
|
||||
tool.loadVideo(null)
|
||||
const destroyed: void = tool.destroy()
|
||||
void [same, pending, point, canvas, defaults, destroyed]
|
||||
|
||||
// @ts-expect-error Numeric options cannot be strings.
|
||||
tool.setup({ width: '80' })
|
||||
// @ts-expect-error A file-input selector string has never been supported.
|
||||
const invalidInput = new Thumbnail({ fileInput: '#input' })
|
||||
// @ts-expect-error Known update events carry a URL followed by numeric progress.
|
||||
tool.emit('update', 123, '50%')
|
||||
// @ts-expect-error Known callbacks must accept their actual payload.
|
||||
tool.on('video', (file: File) => file.name)
|
||||
// @ts-expect-error The public start result is completion, not the thumbnail URL.
|
||||
const incorrect: Promise<string> = tool.start()
|
||||
// @ts-expect-error Runtime ESM exposes only a default class, no .default self-alias.
|
||||
const InvalidAlias = Thumbnail.default
|
||||
void [incorrect, invalidInput, InvalidAlias]
|
||||
Reference in new issue
Block a user