mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-22 05:11:20 -08:00
refactor(jassub): [PKG-JASSUB-03] isolate registration and fix adapter cleanup
This commit is contained in:
1 parent
944e2cc9d0
commit
38e7b37878
21 files changed
+6553
-70
No files matched your search
@@ -178,3 +178,10 @@ selected ARTPLAYER_JASSUB_ON_DEMAND=false diagnostic passes on the candidate cor
|
||||
This is a retained historical failure, not a waived browser gate. The server must
|
||||
serve WASM as application/wasm for native streaming compilation. Archive failed
|
||||
and successful runs independently. See the PKG-JASSUB-02 change record.
|
||||
|
||||
For candidate registration regressions, set ARTPLAYER_JASSUB_ARTIFACT to a built
|
||||
main/legacy global file. Those runs also destroy the exposed instance before host
|
||||
cleanup. ARTPLAYER_JASSUB_CUSTOM_CANVAS=true supplies a caller-owned canvas and
|
||||
asserts it remains connected after destroy(false). The native report records the
|
||||
artifact hash and both mode choices. This does not change the default published
|
||||
baseline or establish ESM/offscreen/device acceptance.
|
||||
@@ -7,7 +7,9 @@ const baseline = JSON.parse(fs.readFileSync('refactor/baselines/jassub-release.j
|
||||
const release = baseline.release
|
||||
const archive = await ensureArchive(release)
|
||||
const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
|
||||
const code = readMember(archive, member).toString()
|
||||
const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT
|
||||
const customCanvas = process.env.ARTPLAYER_JASSUB_CUSTOM_CANVAS === 'true'
|
||||
const code = artifact ? fs.readFileSync(artifact, 'utf8') : readMember(archive, member).toString()
|
||||
const onDemandRender = process.env.ARTPLAYER_JASSUB_ON_DEMAND !== 'false'
|
||||
const subtitles = `[Script Info]
|
||||
ScriptType: v4.00+
|
||||
@@ -23,7 +25,7 @@ Dialogue: 0,0:00:05.00,0:00:30.00,Default,,0,0,0,,After seek with actual WASM
|
||||
`
|
||||
|
||||
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
test(`JASSUB published ${release.version} renders actual WASM subtitles through seek and layout with ${core} core`, async ({ page, context }, testInfo) => {
|
||||
test(`JASSUB ${artifact ? 'candidate artifact' : `published ${release.version}`} renders actual WASM subtitles through seek and layout with ${core} core, customCanvas=${customCanvas}`, async ({ page, context }, testInfo) => {
|
||||
const resources = []
|
||||
const external = []
|
||||
const origin = new URL(testInfo.project.use.baseURL).origin
|
||||
@@ -68,8 +70,17 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await page.evaluate(async ({ subContent, onDemandRender }) => {
|
||||
await page.evaluate(async ({ subContent, onDemandRender, customCanvas }) => {
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
let canvas
|
||||
if (customCanvas) {
|
||||
canvas = document.createElement('canvas')
|
||||
canvas.style.position = 'absolute'
|
||||
canvas.style.pointerEvents = 'none'
|
||||
canvas.style.zIndex = '20'
|
||||
window.art.video.insertAdjacentElement('afterend', canvas)
|
||||
window.jassubUserCanvas = canvas
|
||||
}
|
||||
window.art.plugins.add(register({
|
||||
subContent,
|
||||
workerUrl: '/assets/jassub/jassub-worker.js',
|
||||
@@ -79,6 +90,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
fonts: ['/assets/jassub/default.woff2'],
|
||||
offscreenRender: false,
|
||||
onDemandRender,
|
||||
...(canvas ? { canvas } : {}),
|
||||
}))
|
||||
window.jassub = window.art.plugins.artplayerPluginJassub.instance
|
||||
window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error)))
|
||||
@@ -104,7 +116,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
window.jassubNative.snapshots.push(state)
|
||||
return state
|
||||
}
|
||||
}, { subContent: subtitles, onDemandRender })
|
||||
}, { subContent: subtitles, onDemandRender, customCanvas })
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
const before = await page.evaluate(() => window.jassubPixels('before-seek'))
|
||||
@@ -128,7 +140,11 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
await page.evaluate(() => window.jassubPixels('fullscreen'))
|
||||
await testInfo.attach('actual-jassub-page', { body: await page.screenshot(), contentType: 'image/png' })
|
||||
if (artifact)
|
||||
await page.evaluate(() => window.jassub.destroy())
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
if (customCanvas)
|
||||
expect(await page.evaluate(() => window.jassubUserCanvas.isConnected)).toBe(true)
|
||||
expect(await page.evaluate(() => document.querySelectorAll('.JASSUB').length)).toBe(0)
|
||||
expect(await page.evaluate(() => window.jassubNative.workers.map(worker => worker.terminated))).toEqual([1])
|
||||
expect(await page.evaluate(() => window.jassubNative.errors)).toEqual([])
|
||||
@@ -138,7 +154,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message }))
|
||||
await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, release: { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance.' }, null, 2), contentType: 'application/json' })
|
||||
await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, customCanvas, source: artifact ? { artifact, sha256: hash(code) } : { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery or device acceptance; candidate adds direct-then-host destruction.' }, null, 2), contentType: 'application/json' })
|
||||
if (!page.isClosed()) {
|
||||
await page.evaluate(() => {
|
||||
if (window.art && !window.art.isDestroy)
|
||||
|
||||
@@ -141,6 +141,12 @@ export function jassubEnvironment(implementation, { script = false, simd = false
|
||||
listeners.set(name, [])
|
||||
listeners.get(name).push(callback)
|
||||
return art
|
||||
}, off(name, callback) {
|
||||
const callbacks = listeners.get(name)
|
||||
const index = callbacks?.indexOf(callback) ?? -1
|
||||
if (index >= 0)
|
||||
callbacks.splice(index, 1)
|
||||
return art
|
||||
} }
|
||||
const emit = name => listeners.get(name)?.forEach(callback => callback())
|
||||
const flush = async () => {
|
||||
@@ -154,5 +160,5 @@ export function jassubEnvironment(implementation, { script = false, simd = false
|
||||
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()) }
|
||||
return { factory, exported, art, workers, parent, errors, listeners, emit, ready, flush, createVideo: () => parent.appendChild(new Video()), createCanvas: () => new Canvas() }
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Candidate adapter regressions use the repository runner.
|
||||
import test from 'node:test'
|
||||
import { jassubCandidate, jassubEnvironment } from './helpers/jassub.js'
|
||||
|
||||
const implementation = await jassubCandidate()
|
||||
|
||||
test('JASSUB custom canvas registration keeps caller DOM while releasing its actual vendor instance', async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const canvas = env.parent.appendChild(env.createCanvas())
|
||||
const result = env.factory({ canvas })(env.art)
|
||||
assert.equal(result.instance._canvas, canvas)
|
||||
assert.equal(result.instance._canvasParent, undefined)
|
||||
assert.equal(env.listeners.get('destroy').length, 1)
|
||||
await env.ready()
|
||||
assert.equal(env.workers[0].messages[0].target, 'init')
|
||||
env.emit('destroy')
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(canvas.parentNode, env.parent)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
})
|
||||
|
||||
test('JASSUB host cleanup tolerates an already destroyed exposed instance', async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance.destroy()
|
||||
assert.doesNotThrow(() => env.emit('destroy'))
|
||||
assert.doesNotThrow(() => env.emit('destroy'))
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
})
|
||||
|
||||
test('JASSUB host cleanup before ready terminates once and retains the vendor destroyed-frame guard', async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
env.emit('destroy')
|
||||
env.emit('destroy')
|
||||
await env.flush()
|
||||
assert.equal(instance._destroyed, true)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
})
|
||||
|
||||
test('JASSUB host cleanup guards synchronous reentry without replacing the exposed destroy method', async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const original = instance.destroy
|
||||
let calls = 0
|
||||
const custom = function () {
|
||||
calls++
|
||||
env.emit('destroy')
|
||||
return original.call(this)
|
||||
}
|
||||
instance.destroy = custom
|
||||
env.emit('destroy')
|
||||
assert.equal(calls, 1)
|
||||
assert.equal(instance.destroy, custom)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
})
|
||||
|
||||
test('JASSUB failing host disposal preserves its error and allows another cleanup attempt', async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const original = instance.destroy
|
||||
const failure = new Error('consumer cleanup failure')
|
||||
instance.destroy = () => {
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => env.emit('destroy'), error => error === failure)
|
||||
assert.equal(env.workers[0].terminated, 0)
|
||||
instance.destroy = original
|
||||
env.emit('destroy')
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
})
|
||||
|
||||
for (const offThrows of [false, true]) {
|
||||
test(`JASSUB registration failure rolls back its instance and retains the original error when offThrows=${offThrows}`, async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const unrelated = () => {}
|
||||
env.art.on('destroy', unrelated)
|
||||
const on = env.art.on
|
||||
const failure = new Error('host listener registration failed')
|
||||
env.art.on = (name, listener) => {
|
||||
on(name, listener)
|
||||
throw failure
|
||||
}
|
||||
if (offThrows)
|
||||
env.art.off = () => { throw new Error('secondary rollback failure') }
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
if (!offThrows)
|
||||
assert.deepEqual(env.listeners.get('destroy'), [unrelated])
|
||||
assert.doesNotThrow(() => env.emit('destroy'))
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
})
|
||||
}
|
||||
|
||||
test('JASSUB style setup failure releases the created instance before returning the original exception', async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const insert = env.art.video.insertAdjacentElement
|
||||
const failure = new Error('style setter failed')
|
||||
env.art.video.insertAdjacentElement = function (position, child) {
|
||||
insert.call(this, position, child)
|
||||
Object.defineProperty(child.style, 'zIndex', {
|
||||
get() { return undefined },
|
||||
set() {
|
||||
throw failure
|
||||
},
|
||||
})
|
||||
}
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
assert.equal(env.listeners.get('destroy'), undefined)
|
||||
})
|
||||
Reference in new issue
Block a user