fix(auto-thumbnail): [PKG-AUTO-THUMB-09] restore historical default calls

This commit is contained in:
Harvey Zhao committed 2026-09-14 04:26:33 +08:00
1 parent c4f5dcf17f
commit f30c701d88
29 files changed
+3331 -44

No files matched your search

+5 -2
View File
@@ -9,8 +9,11 @@ editor globals. It also runs in `test:auto-thumbnail` and `test:baseline`.
the actual npm 1.1.0 archive outside the workspace with a packed core. It verifies
frozen reinstalls, byte identity, conditional declaration resolution, old/current
compiler consumers, no-interop CommonJS, and installed factory registration.
This does not decode video or close the older 1.0.x export-shape, native pixel,
or device acceptance gaps. Candidate entry types use the same runtime files.
It also installs actual 1.0.1 to verify old `.default` calls and confirms 1.0.0's
missing main/legacy artifacts. Candidate direct/default calls share a callable;
runtime types describe the alias without changing the historical root declaration.
This does not decode video or close all older type-shape, native pixel, or device
acceptance gaps. Candidate entry types use the same runtime files.
`yarn test:danmuku-mask` runs candidate run cancellation/resource ownership plus
frozen historical defects and public contracts. Candidate tests also run in
+35
View File
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Native export compatibility regression runner.
import test from 'node:test'
import { autoThumbnailCandidate, autoThumbnailEnvironment } from './helpers/auto-thumbnail.js'
const implementation = await autoThumbnailCandidate()
for (const script of [false, true]) {
test(`Auto-thumbnail ${script ? 'script global' : 'CommonJS'} keeps direct and historical default calls on one factory`, async () => {
const env = autoThumbnailEnvironment(implementation, { script })
const factory = env.factory
assert.equal(typeof factory, 'function')
assert.equal(factory.default, factory)
assert.equal(factory.default.default, factory)
assert.equal(env.videos.length, 0)
const original = Object.getOwnPropertyDescriptor(factory, 'default')
assert.deepEqual(original, { value: factory, writable: true, enumerable: true, configurable: true })
const registrar = factory.default({ width: 80, number: 2 })
const pending = registrar(env.art)
assert.equal(pending.name, undefined)
assert.equal(typeof pending.then, 'function')
assert.deepEqual({ ...await pending }, { name: 'artplayerPluginAutoThumbnail' })
assert.equal(env.videos.length, 0, 'Alias registration must not start extraction')
env.art.emit('video:loadedmetadata')
env.metadata()
env.seeked()
env.finish()
assert.equal(env.updates.length, 1)
assert.equal(env.updates[0].width, 80)
env.art.emit('destroy')
assert.equal(env.attached.size, 0)
assert.equal(env.urls.size, 0)
assert([...env.listeners.values()].every(callbacks => callbacks.size === 0))
})
}
+11 -6
View File
@@ -3,12 +3,14 @@ import { autoThumbnailCandidate } from '../helpers/auto-thumbnail.js'
import { expect, test } from './fixtures.js'
const implementation = await autoThumbnailCandidate()
for (const scenario of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame-restart']) {
test(`Auto-thumbnail candidate native ${scenario} owns decoder and encoded URLs`, async ({ page }, testInfo) => {
for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame-restart', 'alias-complete']) {
const useDefault = variant === 'alias-complete'
const scenario = useDefault ? 'complete' : variant
test(`Auto-thumbnail candidate native ${variant} owns decoder and encoded URLs`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=published')
await page.setContent('<!doctype html><div></div>')
await page.addScriptTag({ content: implementation.code })
await page.evaluate(async (scenario) => {
await page.evaluate(async ({ scenario, useDefault }) => {
const probe = window.probe = { videos: [], canvases: [], updates: [], urls: new Set(), blobs: [], frames: [], frameSupport: false, listeners: new Map(), hold: scenario !== 'complete' }
const createElement = document.createElement.bind(document)
const createUrl = URL.createObjectURL.bind(URL)
@@ -71,9 +73,12 @@ for (const scenario of ['destroy', 'restart', 'complete', 'frame-destroy', 'fram
get thumbnails() { return probe.updates.at(-1) },
set thumbnails(value) { probe.updates.push(value) },
}
probe.result = await window.artplayerPluginAutoThumbnail({ width: 80, number: 2 })(probe.art)
const factory = window.artplayerPluginAutoThumbnail
if (useDefault && factory.default !== factory)
throw new Error('The default alias must be the original factory')
probe.result = await (useDefault ? factory.default : factory)({ width: 80, number: 2 })(probe.art)
probe.art.emit('video:loadedmetadata')
}, scenario)
}, { scenario, useDefault })
const holdFrame = scenario.startsWith('frame-') && await page.evaluate(() => window.probe.frameSupport)
await expect.poll(() => page.evaluate(holdFrame => holdFrame ? window.probe.frames.length : window.probe.blobs.length, holdFrame)).toBe(scenario === 'complete' ? 2 : 1)
const state = await page.evaluate(async (scenario) => {
@@ -108,7 +113,7 @@ for (const scenario of ['destroy', 'restart', 'complete', 'frame-destroy', 'fram
probe.restore()
return { before, after, video, canvasDimensions, publishedImage, final, blobs, heldFrames: probe.frames.length, frameSupport: probe.frameSupport, result: probe.result }
}, scenario)
await testInfo.attach('auto-thumbnail-candidate-native-lifecycle', { contentType: 'application/json', body: JSON.stringify({ scenario, sha256: hash(implementation.code), scope: 'Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.', ...state }) })
await testInfo.attach('auto-thumbnail-candidate-native-lifecycle', { contentType: 'application/json', body: JSON.stringify({ scenario, useDefault, sha256: hash(implementation.code), scope: 'Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.', ...state }) })
expect(state.result).toEqual({ name: 'artplayerPluginAutoThumbnail' })
expect(state.before).toEqual(scenario === 'complete' ? { urls: 1, updates: 2 } : { urls: 0, updates: 0 })
expect(state.after).toEqual(state.before)
+6 -3
View File
@@ -1,10 +1,10 @@
import type Artplayer from 'artplayer'
import type { Factory, Option, Result } from 'artplayer-plugin-auto-thumbnail/runtime'
import type { Factory, Option, Result, RuntimeFactory } from 'artplayer-plugin-auto-thumbnail/runtime'
import runtime from 'artplayer-plugin-auto-thumbnail/runtime'
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
type Assert<T extends true> = T
type ExactFactory = Assert<Equal<typeof runtime, Factory>>
type ExactFactory = Assert<Equal<typeof runtime, RuntimeFactory>>
type ExactOption = Assert<Equal<Parameters<typeof runtime>, [option: Option]>>
type ExactResult = Assert<Equal<ReturnType<ReturnType<typeof runtime>>, Promise<Result>>>
@@ -14,5 +14,8 @@ const pending: Promise<Result> = runtime(option)(art)
const name: Promise<'artplayerPluginAutoThumbnail'> = pending.then(value => value.name)
const plugins: NonNullable<Artplayer['option']['plugins']> = [runtime(option)]
const replacement: Factory = _option => async _art => ({ name: 'artplayerPluginAutoThumbnail' })
void [name, plugins, replacement]
const alias: RuntimeFactory = runtime.default.default
const aliasResult: Promise<Result> = alias(option)(art)
runtime.default = runtime
void [name, plugins, replacement, aliasResult]
export type { ExactFactory, ExactOption, ExactResult }