mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-13 07:46:17 -08:00
test(jassub): [PKG-JASSUB-01] freeze published contracts and vendor evidence
This commit is contained in:
1 parent
d44e81ce22
commit
2c73e2b105
20 files changed
+1949
-16
No files matched your search
@@ -0,0 +1,158 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { transform } from 'esbuild'
|
||||
import { verifyJassubContract } from '../../refactor/scripts/jassub-contract.mjs'
|
||||
import { readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from './load.js'
|
||||
|
||||
export async function jassubCandidate() {
|
||||
return { name: 'candidate', version: 'candidate', code: process.env.ARTPLAYER_JASSUB_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_JASSUB_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-jassub', 'umd') }
|
||||
}
|
||||
export async function jassubHistorical() {
|
||||
const { baseline, archives, sources } = await verifyJassubContract()
|
||||
const prefix = 'packages/artplayer-plugin-jassub'
|
||||
const implementations = []
|
||||
for (const release of [baseline.release, ...baseline.previous]) {
|
||||
for (const field of ['main', 'legacy'])
|
||||
implementations.push({ name: `published-${release.version}-${field}`, version: release.version, code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() })
|
||||
}
|
||||
const vendor = sources.get(`${prefix}/src/jassub.es.js`)
|
||||
const exported = vendor.match(/export\s*\{\s*(\w+) as default\s*\};?\s*$/)
|
||||
assert(exported, 'Frozen vendor must have one explicit default class export')
|
||||
const source = vendor.slice(0, exported.index) + sources.get(`${prefix}/src/index.js`).replace('import JASSUB from \'./jassub.es.js\'', `const JASSUB = ${exported[1]}`)
|
||||
implementations.push({ name: 'frozen-workspace-source', version: 'workspace', code: (await transform(source, { format: 'cjs', target: 'es2020' })).code, scriptCode: (await transform(source, { format: 'iife', globalName: 'artplayerPluginJassub', target: 'es2020' })).code })
|
||||
for (const suffix of ['js', 'legacy.js'])
|
||||
implementations.push({ name: `frozen-workspace-${suffix}`, version: 'workspace', code: sources.get(`${prefix}/dist/artplayer-plugin-jassub.${suffix}`) })
|
||||
return implementations
|
||||
}
|
||||
export function jassubEnvironment(implementation, { script = false, simd = false } = {}) {
|
||||
const workers = []
|
||||
const errors = []
|
||||
const listeners = new Map()
|
||||
class Element extends EventTarget {
|
||||
constructor() {
|
||||
super()
|
||||
this.style = {}
|
||||
this.children = []
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
this.children.push(child)
|
||||
child.parentNode = this
|
||||
return child
|
||||
}
|
||||
|
||||
removeChild(child) {
|
||||
assert(this.children.includes(child))
|
||||
this.children.splice(this.children.indexOf(child), 1)
|
||||
child.parentNode = null
|
||||
return child
|
||||
}
|
||||
|
||||
remove() { this.parentNode?.removeChild(this) }
|
||||
insertAdjacentElement(position, child) {
|
||||
assert.equal(position, 'afterend')
|
||||
this.parentNode.appendChild(child)
|
||||
}
|
||||
|
||||
getBoundingClientRect() { return { x: 0, y: 0, top: 0, left: 0, width: 640, height: 360 } }
|
||||
}
|
||||
class Video extends Element {
|
||||
constructor() {
|
||||
super()
|
||||
this.videoWidth = 0
|
||||
this.videoHeight = 0
|
||||
this.paused = true
|
||||
this.currentTime = 0
|
||||
this.readyState = 0
|
||||
this.playbackRate = 1
|
||||
}
|
||||
}
|
||||
class Canvas extends Element {
|
||||
constructor() {
|
||||
super()
|
||||
this.width = 300
|
||||
this.height = 150
|
||||
this.context = { clearRect() { }, putImageData() { }, drawImage() { }, getImageData: () => ({ data: new Uint8ClampedArray(4) }) }
|
||||
}
|
||||
|
||||
getContext(type) {
|
||||
assert.equal(type, '2d')
|
||||
return this.context
|
||||
}
|
||||
}
|
||||
class Worker {
|
||||
constructor(url) {
|
||||
this.url = url
|
||||
this.messages = []
|
||||
this.terminated = 0
|
||||
workers.push(this)
|
||||
}
|
||||
|
||||
postMessage(message) { this.messages.push(message) }
|
||||
terminate() { this.terminated++ }
|
||||
}
|
||||
class ImageData {
|
||||
constructor(data, width, height) {
|
||||
this.data = data
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
}
|
||||
class ErrorEvent extends Event {
|
||||
constructor(type, options) {
|
||||
super(type)
|
||||
this.error = options.error
|
||||
}
|
||||
}
|
||||
const parent = new Element()
|
||||
const video = parent.appendChild(new Video())
|
||||
const module = { exports: {} }
|
||||
const context = {
|
||||
EventTarget,
|
||||
Event,
|
||||
CustomEvent,
|
||||
ErrorEvent,
|
||||
HTMLVideoElement: Video,
|
||||
HTMLCanvasElement: Canvas,
|
||||
ImageData,
|
||||
Worker,
|
||||
WebAssembly: { validate: () => simd },
|
||||
document: { createElement: name => name === 'canvas' ? new Canvas() : new Element() },
|
||||
console: {
|
||||
log() {},
|
||||
error(error) { errors.push(error) },
|
||||
},
|
||||
performance,
|
||||
}
|
||||
context.window = context
|
||||
context.self = context
|
||||
if (!script) {
|
||||
context.module = module
|
||||
context.exports = module.exports
|
||||
}
|
||||
vm.runInNewContext(script && implementation.scriptCode ? implementation.scriptCode : implementation.code, context, { timeout: 5000 })
|
||||
const exported = script ? context.artplayerPluginJassub : module.exports
|
||||
const factory = exported.default || exported
|
||||
const art = { video, on(name, callback) {
|
||||
if (!listeners.has(name))
|
||||
listeners.set(name, [])
|
||||
listeners.get(name).push(callback)
|
||||
return art
|
||||
} }
|
||||
const emit = name => listeners.get(name)?.forEach(callback => callback())
|
||||
const flush = async () => {
|
||||
for (let index = 0;
|
||||
index < 12;
|
||||
index++)
|
||||
await Promise.resolve()
|
||||
}
|
||||
const ready = async () => {
|
||||
await flush()
|
||||
workers.at(-1).onmessage({ data: { target: 'ready' } })
|
||||
await flush()
|
||||
}
|
||||
return { factory, exported, art, workers, parent, errors, listeners, emit, ready, flush, createVideo: () => parent.appendChild(new Video()) }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Frozen JASSUB consumer contracts use the repository runner.
|
||||
import test from 'node:test'
|
||||
import { jassubCandidate, jassubEnvironment, jassubHistorical } from './helpers/jassub.js'
|
||||
|
||||
const implementations = process.env.ARTPLAYER_JASSUB_CANDIDATE === '1' || process.env.ARTPLAYER_JASSUB_ARTIFACT ? [await jassubCandidate()] : await jassubHistorical()
|
||||
for (const implementation of implementations) {
|
||||
for (const script of [false, true]) {
|
||||
test(`JASSUB ${implementation.name}: ${script ? 'global' : 'CJS'} factory is lazy and registration synchronously exposes the actual instance`, async () => {
|
||||
const env = jassubEnvironment(implementation, { script })
|
||||
const register = env.factory({ workerUrl: 'worker.js', subContent: '[Script Info]' })
|
||||
assert.equal(env.workers.length, 0)
|
||||
const result = register(env.art)
|
||||
assert.deepEqual(Object.keys(result), ['name', 'instance'])
|
||||
assert.equal(result.name, 'artplayerPluginJassub')
|
||||
assert.equal(result.then, undefined)
|
||||
assert.equal(result.instance._video, env.art.video)
|
||||
assert.equal(String(result.instance._canvasParent.style.zIndex), '20')
|
||||
assert.equal(env.listeners.get('destroy').length, 1)
|
||||
assert.equal(env.workers[0].url, 'worker.js')
|
||||
await env.ready()
|
||||
assert.equal(env.workers[0].messages[0].subContent, '[Script Info]')
|
||||
env.emit('destroy')
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
})
|
||||
}
|
||||
for (const simd of [false, true]) {
|
||||
test(`JASSUB ${implementation.name}: resource URLs and vendor options retain selection semantics SIMD=${simd}`, async () => {
|
||||
const env = jassubEnvironment(implementation, { simd })
|
||||
const fonts = ['a.ttf']
|
||||
const availableFonts = { test: 'test.woff2' }
|
||||
const result = env.factory({ workerUrl: '/worker.js', wasmUrl: '/base.wasm', modernWasmUrl: '/simd.wasm', subUrl: '/sub.ass', fonts, availableFonts, fallbackFont: 'test', timeOffset: -0.041, targetFps: 30, libassMemoryLimit: 64 })(env.art)
|
||||
await env.ready()
|
||||
const init = env.workers[0].messages[0]
|
||||
assert.equal(init.target, 'init')
|
||||
assert.equal(init.wasmUrl, simd ? '/simd.wasm' : '/base.wasm')
|
||||
assert.equal(init.fonts, fonts)
|
||||
assert.equal(init.availableFonts, availableFonts)
|
||||
assert.equal(init.subUrl, '/sub.ass')
|
||||
assert.equal(init.fallbackFont, 'test')
|
||||
assert.equal(init.targetFps, 30)
|
||||
assert.equal(init.libassMemoryLimit, 64)
|
||||
assert.equal(result.instance.timeOffset, -0.041)
|
||||
env.emit('destroy')
|
||||
})
|
||||
}
|
||||
test(`JASSUB ${implementation.name}: mutable options override video and historical instance methods return synchronously`, async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const option = { workerUrl: 'old.js' }
|
||||
const register = env.factory(option)
|
||||
option.workerUrl = 'new.js'
|
||||
option.video = env.createVideo()
|
||||
const result = register(env.art)
|
||||
assert.equal(result.instance._video, option.video)
|
||||
assert.equal(env.workers[0].url, 'new.js')
|
||||
await env.ready()
|
||||
assert.equal(result.instance.resize(640, 360, 4, 5, false), undefined)
|
||||
assert.equal(result.instance.setVideo(option.video), undefined)
|
||||
assert.equal(result.instance.setTrack('[Script Info]'), undefined)
|
||||
assert.equal(result.instance.destroy(), undefined)
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
const resize = env.workers[0].messages.find(message => message.target === 'canvas')
|
||||
assert.equal(resize.width, 640)
|
||||
assert.equal(resize.height, 360)
|
||||
})
|
||||
test(`JASSUB ${implementation.name}: omitted options retain worker and font defaults`, async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const result = env.factory()(env.art)
|
||||
assert.equal(env.workers[0].url, 'jassub-worker.js')
|
||||
await env.ready()
|
||||
const init = env.workers[0].messages[0]
|
||||
assert.equal(init.wasmUrl, 'jassub-worker.wasm')
|
||||
assert.equal(init.availableFonts['liberation sans'], './default.woff2')
|
||||
result.instance.destroy()
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user