fix(dash): [PKG-DASH-CLEANUP-01] preserve first cleanup exception

This commit is contained in:
Harvey Zhao committed 2026-09-15 15:57:04 +08:00
1 parent 1c522f181b
commit 25faf88e80
23 files changed
+49546 -147

No files matched your search

+59
View File
@@ -99,6 +99,65 @@ async function openDash(page, core, plugin, sdk, testInfo) {
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('en')
}
for (const core of ['published', 'candidate']) {
for (const sdk of [4, 5]) {
test(`${core} core + candidate DASH SDK ${sdk}: falsy unmount errors preserve identity and release sibling menus`, async ({ page }, testInfo) => {
await openDash(page, core, 'candidate', sdk, testInfo)
const result = await page.evaluate((sdk) => {
const art = window.art
const plugin = art.plugins.artplayerPluginDashControl
const method = sdk === 4 ? 'getBitrateInfoListFor' : 'getRepresentationsByType'
const original = art.dash[method]
const results = []
for (const failure of [undefined, null, false, 0, -0, '', Number.NaN]) {
let armed = false
art.controls.update({ name: 'dash-quality', selector: art.controls.cache.get('dash-quality').option.selector.map(item => ({ ...item })), beforeUnmount() {
if (armed)
throw failure
} })
armed = true
art.dash[method] = () => []
let threw = false
let same = false
try {
plugin.update()
}
catch (error) {
threw = true
same = Object.is(error, failure)
}
results.push({ threw, same, audioRemoved: !art.controls['dash-audio'], settingsRemoved: !art.setting.find('dash-quality') && !art.setting.find('dash-audio') })
armed = false
art.dash[method] = original
if (art.controls['dash-quality'])
art.controls.remove('dash-quality')
plugin.update()
}
let armed = false
art.controls.update({ name: 'dash-quality', selector: art.controls.cache.get('dash-quality').option.selector.map(item => ({ ...item })), beforeUnmount() {
if (armed)
// eslint-disable-next-line no-throw-literal -- Verify the public callback's primitive exception identity.
throw 0
} })
armed = true
let destroyThrew = false
let destroySame = false
try {
art.destroy(false)
}
catch (error) {
destroyThrew = true
destroySame = Object.is(error, 0)
}
const siblingRemoved = !art.controls['dash-audio']
armed = false
if (art.controls['dash-quality'])
art.controls.remove('dash-quality')
return { results, destroyThrew, destroySame, siblingRemoved }
}, sdk)
await testInfo.attach('dash-cleanup-errors', { contentType: 'application/json', body: JSON.stringify({ result, scope: 'Real core beforeUnmount callbacks and native DOM; controlled SDK topology, no adaptive playback claim.' }) })
expect(result).toEqual({ results: Array.from({ length: 7 }, () => ({ threw: true, same: true, audioRemoved: true, settingsRemoved: true })), destroyThrew: true, destroySame: true, siblingRemoved: true })
})
}
for (const [plugin, sdk] of [['published', 4], ['candidate', 4], ['candidate', 5]]) {
for (const surface of ['control', 'setting']) {
for (const type of ['quality', 'audio']) {
+87
View File
@@ -0,0 +1,87 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Cleanup exception identity uses the Node lifecycle runner.
import test from 'node:test'
import { bothMenus, dashHost, dashImplementations } from './helpers/dash-control.js'
const implementations = (await dashImplementations()).filter(item => item.name.startsWith('source') || item.name.startsWith('artifact'))
const failures = [undefined, null, false, 0, -0, '', Number.NaN]
function attempt(callback) {
try {
callback()
return { threw: false }
}
catch (error) {
return { threw: true, error }
}
}
for (const { sdk, name, factory } of implementations) {
for (const trigger of ['update', 'destroy']) {
test(`DASH ${sdk} (${name}): ${trigger} preserves the first falsy menu cleanup failure`, (t) => {
const warnings = []
t.mock.method(console, 'warn', (...args) => warnings.push(args))
for (const failure of failures) {
const host = dashHost(sdk)
const plugin = factory(bothMenus())(host.art)
plugin.update()
const calls = []
for (const surface of ['controls', 'setting']) {
const remove = host.art[surface].remove
host.art[surface].remove = (key) => {
remove(key)
calls.push([surface, key])
if (surface === 'controls' && key === 'dash-quality')
throw failure
throw new Error(`later ${surface}/${key} failure`)
}
}
if (trigger === 'update') {
host.state.levels = []
host.state.tracks = []
}
const result = attempt(() => trigger === 'update' ? plugin.update() : host.art.destroy())
assert.equal(result.threw, true)
assert(Object.is(result.error, failure), 'Later menu cleanup errors cannot replace the first value')
assert.deepEqual(calls, [['controls', 'dash-quality'], ['setting', 'dash-quality'], ['controls', 'dash-audio'], ['setting', 'dash-audio']])
assert.equal(host.controls.size + host.settings.size, 0)
if (trigger === 'destroy') {
assert.equal([...host.listeners.values()].reduce((sum, list) => sum + list.size, 0), 0)
assert.doesNotThrow(() => host.art.destroy())
}
}
if (trigger === 'update') {
assert.equal(warnings.length, failures.length)
assert(warnings.every(([message, error]) => message === 'ArtPlayer DASH cleanup failed:' && error instanceof Error))
}
else {
assert.equal(warnings.length, 0)
}
})
}
test(`DASH ${sdk} (${name}): SDK unsubscribe preserves a falsy first failure while releasing all resources`, () => {
for (const failure of failures) {
const host = dashHost(sdk)
const events = new Map()
host.dash.on = (name, callback) => events.set(name, callback)
let offCalls = 0
host.dash.off = (name) => {
events.delete(name)
if (++offCalls === 1)
throw failure
throw new Error('later SDK removal failure')
}
factory(bothMenus())(host.art).update()
const count = events.size
assert(count > 0)
const result = attempt(() => host.art.destroy())
assert.equal(result.threw, true)
assert(Object.is(result.error, failure))
assert.equal(offCalls, count)
assert.equal(events.size, 0)
assert.equal(host.controls.size + host.settings.size, 0)
assert.equal([...host.listeners.values()].reduce((sum, list) => sum + list.size, 0), 0)
assert(!host.calls.some(call => call[0] === 'dash.destroy'), 'SDK remains caller-owned')
}
})
}