test(asr): [PKG-ASR-01] freeze published interfaces and source contracts

This commit is contained in:
Harvey Zhao committed 2026-09-13 14:03:16 +08:00
1 parent 3b016970f2
commit a22586a172
16 files changed
+543 -7

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Historical ASR contracts use the repository runner.
import test from 'node:test'
import { asrEnvironment, asrHistorical } from './helpers/asr.js'
for (const implementation of await asrHistorical()) {
for (const script of [false, true]) {
test(`ASR ${implementation.name}: ${script ? 'global' : 'CJS'} lazy factory, synchronous result and async stop`, async () => {
const env = asrEnvironment(implementation, { script })
if (!script) {
const namespace = implementation.name.startsWith('published-2.0.0') || implementation.name === 'frozen-source'
assert.equal(typeof env.exported, namespace ? 'object' : 'function')
assert.deepEqual(Object.keys(env.exported), namespace ? ['default'] : [])
}
const register = env.factory()
assert.equal(env.layers.length, 0)
const result = register(env.art)
assert.deepEqual(Object.keys(result), ['name', 'stop', 'hide', 'append'])
assert.equal(result.name, 'artplayerPluginAsr')
assert.equal(result.then, undefined)
assert.equal(result.start, undefined)
assert.equal(env.layers[0].name, 'asr')
assert.deepEqual([...env.listeners.keys()], ['video:volumechange', 'play', 'pause', 'destroy'])
const stopped = result.stop()
assert.equal(typeof stopped.then, 'function')
assert.equal(await stopped, undefined)
assert.equal(env.listeners.get('play').length, 1, 'stop retains historical restart subscription')
assert.equal(env.styles.size, 1)
env.reload()
assert.equal(env.styles.size, 1)
})
}
test(`ASR ${implementation.name}: append replaces punctuation lines, permits markup, and hide retains content`, () => {
const env = asrEnvironment(implementation)
const result = env.factory()(env.art)
assert.equal(result.append('One. Two! 三。 Four?'), undefined)
assert.equal(env.layer.innerHTML, '<div class="art-asr-line">Two!</div><div class="art-asr-line">三。</div><div class="art-asr-line">Four?</div>')
assert.equal([...env.timeouts.values()][0].delay, 10000)
result.append('<b>markup</b>')
assert.equal(env.layer.innerHTML, '<div class="art-asr-line"><b>markup</b></div>')
assert.equal(env.timeouts.size, 1)
result.hide()
assert.equal(env.layer.style.display, 'none')
assert.equal(result.append(null), undefined)
assert.equal(env.layer.style.display, 'none')
assert.equal(env.timeouts.size, 1)
result.append('')
assert.equal(env.layer.innerHTML, '')
assert.equal(env.layer.style.display, '')
;[...env.timeouts.values()][0].callback()
assert.equal(env.layer.style.display, 'none')
})
test(`ASR ${implementation.name}: options are captured at factory call and instances own subtitle timers`, () => {
const option = { length: 1, autoHideTimeout: 25 }
const env = asrEnvironment(implementation)
const register = env.factory(option)
option.length = 4
option.autoHideTimeout = 99
register(env.art).append('one. two.')
assert.equal(env.layer.innerHTML, '<div class="art-asr-line">two.</div>')
assert.equal([...env.timeouts.values()][0].delay, 25)
const other = asrEnvironment(implementation)
other.factory({ length: 0 })(other.art).append('one. two.')
assert.equal(other.layer.innerHTML, '<div class="art-asr-line">one.</div><div class="art-asr-line">two.</div>', 'zero retains historical slice(-0) semantics')
assert.equal(env.timeouts.size, 1)
assert.equal(other.timeouts.size, 1)
})
}
+62
View File
@@ -0,0 +1,62 @@
import assert from 'node:assert/strict'
import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyAsrContract } from '../../refactor/scripts/asr-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
export async function asrHistorical() {
const { baseline, archives, sources } = await verifyAsrContract()
const implementations = []
for (const release of [baseline.release, ...baseline.previous]) {
for (const field of ['main', 'legacy'])
implementations.push({ name: `published-${release.version}-${field}`, code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() })
}
const prefix = 'packages/artplayer-plugin-asr'
const source = sources.get(`${prefix}/src/index.js`).replace('import style from \'./style.less?inline\'', 'const style = \'frozen-source-style\'')
implementations.push({ name: 'frozen-source', code: (await transform(source, { format: 'cjs', target: 'es2020' })).code, scriptCode: (await transform(source, { format: 'iife', globalName: 'artplayerPluginAsr', target: 'es2020' })).code })
for (const suffix of ['js', 'legacy.js'])
implementations.push({ name: `frozen-workspace-${suffix}`, code: sources.get(`${prefix}/dist/artplayer-plugin-asr.${suffix}`) })
return implementations
}
export function asrEnvironment(implementation, { script = false } = {}) {
const listeners = new Map()
const timeouts = new Map()
const styles = new Map()
const layer = { style: {}, innerHTML: '' }
let id = 0
const context = vm.createContext({
console,
setTimeout(callback, delay) {
timeouts.set(++id, { callback, delay })
return id
},
clearTimeout(timer) { timeouts.delete(timer) },
document: {
getElementById: id => styles.get(id),
createElement: () => ({}),
head: { appendChild(element) { styles.set(element.id, element) } },
},
})
context.window = context
if (!script) {
context.module = { exports: {} }
context.exports = context.module.exports
}
const code = script ? implementation.scriptCode || implementation.code : `(function () {\n${implementation.code}\n}).call(globalThis)`
vm.runInContext(code, context, { timeout: 1000 })
const exported = script ? context.artplayerPluginAsr : context.module.exports
const factory = typeof exported === 'function' ? exported : exported.default
assert.equal(typeof factory, 'function', implementation.name)
const layers = []
const art = {
video: {},
volume: 0.5,
layers: { add(option) {
layers.push(option)
return layer
} },
on(name, callback) { listeners.set(name, [...listeners.get(name) || [], callback]) },
}
return { context, factory, exported, styles, layer, layers, listeners, timeouts, art, reload: () => vm.runInContext(code, context), emit: name => Promise.all((listeners.get(name) || []).map(callback => callback())) }
}