mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
237 lines
9.8 KiB
JavaScript
237 lines
9.8 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
// eslint-disable-next-line test/no-import-node-test -- Accurate runtime contracts use the repository runner.
|
|
import test from 'node:test'
|
|
import { isPromise } from 'node:util/types'
|
|
import vm from 'node:vm'
|
|
import { jassubCandidate, jassubEnvironment, jassubHistorical } from './helpers/jassub.js'
|
|
|
|
const candidate = await jassubCandidate()
|
|
const frozen = (await jassubHistorical()).find(item => item.name === 'frozen-workspace-source')
|
|
|
|
function environment(implementation) {
|
|
const code = `${implementation.code}\nObject.defineProperty(module.exports.default || module.exports, '__runtimePlatform', { value: { ErrorEvent, setGlobal: (name, value) => { globalThis[name] = value } } });`
|
|
const env = jassubEnvironment({ ...implementation, code })
|
|
env.platform = env.factory.__runtimePlatform
|
|
return env
|
|
}
|
|
|
|
function requestListeners(worker) {
|
|
const listeners = new Map()
|
|
worker.addEventListener = (type, callback) => {
|
|
if (!listeners.has(type))
|
|
listeners.set(type, new Set())
|
|
listeners.get(type).add(callback)
|
|
}
|
|
worker.removeEventListener = (type, callback) => listeners.get(type)?.delete(callback)
|
|
return {
|
|
size: () => [...listeners.values()].reduce((sum, entries) => sum + entries.size, 0),
|
|
emit(type, event) {
|
|
for (const callback of [...listeners.get(type) || []]) callback(event)
|
|
},
|
|
}
|
|
}
|
|
|
|
test('JASSUB accurate runtime resize uses width/height/top/left/force and lifecycle methods return synchronously', async () => {
|
|
const env = environment(candidate)
|
|
const { instance } = env.factory()(env.art)
|
|
await env.ready()
|
|
assert.equal(instance.resize(320, 180, 4, 7, false), undefined)
|
|
assert.equal(instance._canvas.style.top, '4px')
|
|
assert.equal(instance._canvas.style.left, '7px')
|
|
await env.flush()
|
|
const resize = env.workers[0].messages.at(-1)
|
|
assert.equal(resize.target, 'canvas')
|
|
assert.equal(resize.width, 320)
|
|
assert.equal(resize.height, 180)
|
|
assert.equal(resize.force, false)
|
|
const video = env.createVideo()
|
|
assert.equal(instance.setVideo(video), undefined)
|
|
assert.equal(instance._video, video)
|
|
assert.equal(instance.destroy(), undefined)
|
|
assert.equal(env.workers[0].terminated, 1)
|
|
assert.equal(env.parent.children.some(child => child.className === 'JASSUB'), false)
|
|
})
|
|
|
|
test('JASSUB accurate sendMessage returns a Promise waiting for ready and resolving after the actual post', async () => {
|
|
const env = environment(candidate)
|
|
const { instance } = env.factory()(env.art)
|
|
const payload = { value: 42 }
|
|
const pending = instance.sendMessage('runtime-probe', payload)
|
|
assert(isPromise(pending))
|
|
let settled = false
|
|
pending.then(() => {
|
|
settled = true
|
|
})
|
|
await env.flush()
|
|
assert.equal(settled, false)
|
|
assert.deepEqual(env.workers[0].messages.map(message => message.target), ['init'])
|
|
await env.ready()
|
|
assert.equal(await pending, undefined)
|
|
assert.equal(settled, true)
|
|
assert.equal(env.workers[0].messages.at(-1).target, 'runtime-probe')
|
|
assert.equal(env.workers[0].messages.at(-1).value, 42)
|
|
instance.destroy()
|
|
})
|
|
|
|
test('JASSUB accurate font options preserve mixed sources and font methods return synchronously', async () => {
|
|
const env = environment(candidate)
|
|
const bytes = new Uint8Array([1, 2, 3])
|
|
const fonts = ['fixture.woff2', bytes]
|
|
const availableFonts = { fixture: bytes, fallback: 'fallback.woff2' }
|
|
const { instance } = env.factory({ fonts, availableFonts })(env.art)
|
|
await env.ready()
|
|
const init = env.workers[0].messages[0]
|
|
assert.equal(init.fonts, fonts)
|
|
assert.equal(init.fonts[1], bytes)
|
|
assert.equal(init.availableFonts, availableFonts)
|
|
assert.equal(instance.addFont(bytes), undefined)
|
|
assert.equal(instance.setDefaultFont('fixture'), undefined)
|
|
await env.flush()
|
|
const added = env.workers[0].messages.find(message => message.target === 'addFont')
|
|
assert.equal(added.font, bytes)
|
|
assert.equal(env.workers[0].messages.at(-1).target, 'defaultFont')
|
|
assert.equal(env.workers[0].messages.at(-1).font, 'fixture')
|
|
instance.destroy()
|
|
})
|
|
|
|
const subtitles = `[Script Info]
|
|
ScriptType: v4.00+
|
|
PlayResX: 640
|
|
PlayResY: 360
|
|
[V4+ Styles]
|
|
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
Style: Default,Arial,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
|
|
[Events]
|
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,Runtime fixture`
|
|
|
|
test('JASSUB real Worker JS/WASM queries return numeric style indices and full event/style data through controlled transport', { timeout: 10000 }, async (context) => {
|
|
const env = environment(candidate)
|
|
env.platform.setGlobal('setTimeout', setTimeout)
|
|
env.platform.setGlobal('clearTimeout', clearTimeout)
|
|
const { instance } = env.factory({ subContent: subtitles, availableFonts: {}, fallbackFont: 'fixture', wasmUrl: '/runtime-fixture.wasm', onDemandRender: false, offscreenRender: false, asyncRender: false })(env.art)
|
|
const worker = env.workers[0]
|
|
const listeners = requestListeners(worker)
|
|
const wasm = fs.readFileSync('packages/artplayer-plugin-jassub/worker/jassub-worker.wasm')
|
|
const source = fs.readFileSync('packages/artplayer-plugin-jassub/worker/jassub-worker.js', 'utf8')
|
|
const requests = []
|
|
const messages = []
|
|
let initialized = false
|
|
let stopped = false
|
|
const workerContext = {
|
|
WebAssembly,
|
|
TextDecoder,
|
|
TextEncoder,
|
|
console,
|
|
onmessage: null,
|
|
async fetch(url) {
|
|
assert.equal(url, '/runtime-fixture.wasm')
|
|
requests.push(url)
|
|
return new Response(wasm, { headers: { 'Content-Type': 'application/wasm' } })
|
|
},
|
|
postMessage(message) {
|
|
const data = structuredClone(message)
|
|
messages.push(data)
|
|
if (data.target === 'ready')
|
|
initialized = true
|
|
queueMicrotask(() => {
|
|
if (stopped)
|
|
return
|
|
worker.onmessage({ data })
|
|
listeners.emit('message', { data })
|
|
})
|
|
},
|
|
}
|
|
workerContext.self = workerContext
|
|
vm.createContext(workerContext)
|
|
// Execute complete production Worker JS and actual WASM. Only transport and fetch are controlled.
|
|
vm.runInContext(source, workerContext, { timeout: 5000 })
|
|
const post = worker.postMessage
|
|
worker.postMessage = function (message) {
|
|
post.call(this, message)
|
|
if (!stopped)
|
|
workerContext.onmessage({ data: structuredClone(message) })
|
|
}
|
|
const terminate = worker.terminate
|
|
worker.terminate = function () {
|
|
if (initialized && !stopped)
|
|
workerContext.onmessage({ data: { target: 'destroy' } })
|
|
stopped = true
|
|
terminate.call(this)
|
|
}
|
|
context.after(() => instance.destroy())
|
|
let readyEvents = 0
|
|
instance.addEventListener('ready', (event) => {
|
|
readyEvents++
|
|
assert.equal(event.detail, null)
|
|
assert.equal(event.target, instance)
|
|
})
|
|
assert(instance instanceof EventTarget)
|
|
await new Promise(resolve => instance.addEventListener('ready', resolve, { once: true }))
|
|
assert.equal(readyEvents, 1)
|
|
assert.deepEqual(requests, ['/runtime-fixture.wasm'])
|
|
const query = method => new Promise((resolve, reject) => {
|
|
assert.equal(instance[method]((error, rows) => {
|
|
if (error !== null) {
|
|
reject(error ?? new Error('Successful queries must pass null as their error argument'))
|
|
return
|
|
}
|
|
resolve(rows)
|
|
}), undefined)
|
|
})
|
|
const events = await query('getEvents')
|
|
const styles = await query('getStyles')
|
|
assert.equal(events.length, 1)
|
|
assert.equal(events[0].Start, 1000)
|
|
assert.equal(events[0].Duration, 2000)
|
|
assert.equal(events[0].Text, 'Runtime fixture')
|
|
assert.equal(typeof events[0].Style, 'number')
|
|
assert.equal(Object.hasOwn(events[0], '_index'), false)
|
|
assert.equal(styles[events[0].Style].FontName, 'Arial')
|
|
assert.equal(styles[events[0].Style].FontSize, 24)
|
|
assert.equal(typeof styles[events[0].Style].PrimaryColour, 'number')
|
|
assert.equal(listeners.size(), 0)
|
|
assert.deepEqual(messages.find(message => message.target === 'getEvents').events, events)
|
|
assert.deepEqual(messages.find(message => message.target === 'getStyles').styles, styles)
|
|
assert.equal(instance.setEvent({ Text: 'Updated through Worker', Style: events[0].Style }, 0), undefined)
|
|
await env.flush()
|
|
assert.equal((await query('getEvents'))[0].Text, 'Updated through Worker')
|
|
assert.equal(instance.setStyle({ FontSize: 32 }, events[0].Style), undefined)
|
|
await env.flush()
|
|
assert.equal((await query('getStyles'))[events[0].Style].FontSize, 32)
|
|
})
|
|
|
|
for (const method of ['getEvents', 'getStyles']) {
|
|
for (const mode of ['timeout', 'worker-error']) {
|
|
test(`JASSUB frozen PKG-JASSUB-07 defect: ${method} ${mode} throws before the public error callback`, async () => {
|
|
const env = environment(frozen)
|
|
const timers = new Map()
|
|
let next = 0
|
|
env.platform.setGlobal('setTimeout', (callback) => {
|
|
timers.set(++next, callback)
|
|
return next
|
|
})
|
|
env.platform.setGlobal('clearTimeout', id => timers.delete(id))
|
|
const { instance } = env.factory()(env.art)
|
|
await env.ready()
|
|
const listeners = requestListeners(env.workers[0])
|
|
let calls = 0
|
|
instance[method](() => calls++)
|
|
assert.equal(listeners.size(), 2)
|
|
const trigger = mode === 'timeout'
|
|
? () => {
|
|
const [id, callback] = [...timers][0]
|
|
timers.delete(id)
|
|
callback()
|
|
}
|
|
: () => listeners.emit('error', new env.platform.ErrorEvent('error', { error: new Error('controlled Worker error') }))
|
|
assert.throws(trigger, { name: 'TypeError', message: /Cannot destructure property/ })
|
|
assert.equal(calls, 0)
|
|
assert.equal(listeners.size(), 2)
|
|
assert.equal(timers.size, mode === 'timeout' ? 0 : 1)
|
|
instance.destroy()
|
|
})
|
|
}
|
|
}
|