mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:17:46 -08:00
268 lines
9.6 KiB
JavaScript
268 lines
9.6 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
|
import { test } from 'node:test'
|
|
import { loadModules } from './helpers/load.js'
|
|
|
|
const resources = 'packages/artplayer/src/setting/resources'
|
|
const lifecycle = 'packages/artplayer/src/lifecycle/instance'
|
|
const { formatTree, bindSettingActions, ownSettingItem, releaseSettingItem, releaseSettingTree, proxySetting, settingScope, suspendSettingItem, beginLifecycle, getScope } = await loadModules({
|
|
formatTree: { file: 'packages/artplayer/src/setting/model', name: 'formatTree' },
|
|
bindSettingActions: { file: 'packages/artplayer/src/setting/selection', name: 'bindSettingActions' },
|
|
...Object.fromEntries(['ownSettingItem', 'releaseSettingItem', 'releaseSettingTree', 'proxySetting', 'settingScope', 'suspendSettingItem'].map(name => [name, { file: resources, name }])),
|
|
...Object.fromEntries(['beginLifecycle', 'getScope'].map(name => [name, { file: lifecycle, name }])),
|
|
})
|
|
|
|
function fixture(items) {
|
|
const listeners = new Set()
|
|
const art = {
|
|
events: {
|
|
remove(cleanup) {
|
|
cleanup()
|
|
listeners.delete(cleanup)
|
|
},
|
|
},
|
|
proxy(target, name, callback) {
|
|
target.addEventListener(name, callback)
|
|
const cleanup = () => target.removeEventListener(name, callback)
|
|
listeners.add(cleanup)
|
|
return cleanup
|
|
},
|
|
}
|
|
beginLifecycle(art)
|
|
formatTree({ id: 0 }, items)
|
|
return { art, listeners, setting: { art, cache: new Map(), option: items } }
|
|
}
|
|
|
|
test('setting callback results use latest operation and stop after item replacement', async () => {
|
|
const pending = []
|
|
const item = { name: 'button', tooltip: 'initial' }
|
|
const { art, setting, listeners } = fixture([item])
|
|
const target = new EventTarget()
|
|
item.onClick = function (owner, element, event) {
|
|
assert.equal(this, art)
|
|
assert.equal(owner, item)
|
|
assert.equal(element, target)
|
|
assert.equal(event.type, 'click')
|
|
return new Promise(resolve => pending.push(resolve))
|
|
}
|
|
ownSettingItem(art, item)
|
|
bindSettingActions(setting, item, target, 'button')
|
|
target.dispatchEvent(new Event('click'))
|
|
target.dispatchEvent(new Event('click'))
|
|
pending[1]('new')
|
|
await Promise.resolve()
|
|
pending[0]('old')
|
|
await Promise.resolve()
|
|
assert.equal(item.tooltip, 'new')
|
|
target.dispatchEvent(new Event('click'))
|
|
const previous = settingScope(item)
|
|
ownSettingItem(art, item)
|
|
assert.equal(previous.closed, true)
|
|
pending[2]('removed')
|
|
await Promise.resolve()
|
|
assert.equal(item.tooltip, 'new')
|
|
assert.equal(listeners.size, 0)
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('setting callback rejection is reported with the original error and no UI write', async (t) => {
|
|
const failure = new Error('selection')
|
|
const warnings = []
|
|
t.mock.method(console, 'warn', (...args) => warnings.push(args))
|
|
const item = { tooltip: 'retained', onClick() {
|
|
return Promise.reject(failure)
|
|
} }
|
|
const { art, setting } = fixture([item])
|
|
const target = new EventTarget()
|
|
ownSettingItem(art, item)
|
|
bindSettingActions(setting, item, target, 'button')
|
|
target.dispatchEvent(new Event('click'))
|
|
await Promise.resolve()
|
|
assert.deepEqual(warnings, [['ArtPlayer setting callback failed:', failure]])
|
|
assert.equal(item.tooltip, 'retained')
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('removing a setting tree releases descendants and their panel caches', () => {
|
|
const child = { name: 'child' }
|
|
const parent = { name: 'parent', selector: [child] }
|
|
const other = { name: 'other' }
|
|
const { art, setting, listeners } = fixture([parent, other])
|
|
const target = new EventTarget()
|
|
let calls = 0
|
|
let removedPanels = 0
|
|
for (const item of [parent, child, other]) {
|
|
ownSettingItem(art, item)
|
|
proxySetting(art, item, target, 'click', () => calls++)
|
|
}
|
|
setting.cache.set(parent.selector, { remove() {
|
|
removedPanels++
|
|
} })
|
|
target.dispatchEvent(new Event('click'))
|
|
assert.equal(calls, 3)
|
|
releaseSettingTree(setting, parent)
|
|
assert.equal(removedPanels, 1)
|
|
assert.equal(setting.cache.has(parent.selector), false)
|
|
assert.equal(parent.$events.length, 0)
|
|
assert.equal(child.$events.length, 0)
|
|
assert.equal(other.$events.length, 1)
|
|
target.dispatchEvent(new Event('click'))
|
|
assert.equal(calls, 4)
|
|
assert.equal(listeners.size, 1)
|
|
getScope(art).dispose()
|
|
assert.equal(listeners.size, 0)
|
|
})
|
|
|
|
test('setting listener cleanup attempts every callback even when one fails', () => {
|
|
const item = {}
|
|
const { art } = fixture([item])
|
|
ownSettingItem(art, item)
|
|
const failure = new Error('cleanup')
|
|
const calls = []
|
|
item.$events.push(() => {
|
|
calls.push('first')
|
|
throw failure
|
|
}, () => {
|
|
calls.push('last')
|
|
})
|
|
assert.throws(() => releaseSettingItem(art, item), error => error.errors.length === 1 && error.errors[0] === failure)
|
|
assert.deepEqual(calls, ['first', 'last'])
|
|
assert.deepEqual(item.$events, [])
|
|
releaseSettingItem(art, item)
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('registration that closes the owner releases the just-returned listener', () => {
|
|
const item = {}
|
|
const { art, listeners } = fixture([item])
|
|
ownSettingItem(art, item)
|
|
const proxy = art.proxy
|
|
art.proxy = (...args) => {
|
|
const result = proxy(...args)
|
|
getScope(art).dispose()
|
|
return result
|
|
}
|
|
let calls = 0
|
|
const target = new EventTarget()
|
|
proxySetting(art, item, target, 'click', () => calls++)
|
|
target.dispatchEvent(new Event('click'))
|
|
assert.equal(calls, 0)
|
|
assert.equal(listeners.size, 0)
|
|
assert.equal(item.$events.length, 0)
|
|
})
|
|
|
|
test('synchronous setting navigation errors are reported at the owned listener boundary', (t) => {
|
|
const item = {}
|
|
const { art } = fixture([item])
|
|
ownSettingItem(art, item)
|
|
const failure = new Error('navigation render')
|
|
const warnings = []
|
|
t.mock.method(console, 'warn', (...args) => warnings.push(args))
|
|
const target = new EventTarget()
|
|
proxySetting(art, item, target, 'click', () => {
|
|
throw failure
|
|
})
|
|
target.dispatchEvent(new Event('click'))
|
|
assert.deepEqual(warnings, [['ArtPlayer setting callback failed:', failure]])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('a failed replacement resumes the original scope and event array without registering it again', () => {
|
|
const item = {}
|
|
const { art, listeners } = fixture([item])
|
|
const oldNode = new EventTarget()
|
|
const newNode = new EventTarget()
|
|
let oldCalls = 0
|
|
let newCalls = 0
|
|
const original = ownSettingItem(art, item)
|
|
proxySetting(art, item, oldNode, 'click', () => oldCalls++)
|
|
const events = item.$events
|
|
const cleanup = events[0]
|
|
const suspended = suspendSettingItem(art, item)
|
|
assert.equal(original.closed, false)
|
|
assert.equal(events.length, 0)
|
|
assert.throws(() => suspendSettingItem(art, item), /already in progress/)
|
|
oldNode.dispatchEvent(new Event('click'))
|
|
const replacement = ownSettingItem(art, item)
|
|
proxySetting(art, item, newNode, 'click', () => newCalls++)
|
|
newNode.dispatchEvent(new Event('click'))
|
|
suspended.resume()
|
|
suspended.resume()
|
|
suspended.dispose()
|
|
assert.equal(replacement.closed, true)
|
|
assert.equal(settingScope(item), original)
|
|
assert.equal(item.$events, events)
|
|
assert.deepEqual(events, [cleanup])
|
|
assert.equal(listeners.size, 1)
|
|
oldNode.dispatchEvent(new Event('click'))
|
|
newNode.dispatchEvent(new Event('click'))
|
|
assert.deepEqual([oldCalls, newCalls], [1, 1])
|
|
getScope(art).dispose()
|
|
})
|
|
|
|
test('successful replacement disposes only the suspended listeners', () => {
|
|
const item = {}
|
|
const { art, listeners } = fixture([item])
|
|
const target = new EventTarget()
|
|
const calls = []
|
|
const original = ownSettingItem(art, item)
|
|
proxySetting(art, item, target, 'click', () => calls.push('old'))
|
|
const suspended = suspendSettingItem(art, item)
|
|
const replacement = ownSettingItem(art, item)
|
|
proxySetting(art, item, target, 'click', () => calls.push('new'))
|
|
const cleanup = item.$events[0]
|
|
suspended.dispose()
|
|
suspended.resume()
|
|
assert.equal(original.closed, true)
|
|
assert.equal(replacement.closed, false)
|
|
assert.deepEqual(item.$events, [cleanup])
|
|
assert.equal(listeners.size, 1)
|
|
target.dispatchEvent(new Event('click'))
|
|
assert.deepEqual(calls, ['new'])
|
|
getScope(art).dispose()
|
|
assert.equal(listeners.size, 0)
|
|
})
|
|
|
|
test('destroying a suspended replacement releases both generations and cannot resume either', () => {
|
|
const item = {}
|
|
const { art, listeners } = fixture([item])
|
|
const target = new EventTarget()
|
|
let calls = 0
|
|
ownSettingItem(art, item)
|
|
proxySetting(art, item, target, 'click', () => calls++)
|
|
const suspended = suspendSettingItem(art, item)
|
|
ownSettingItem(art, item)
|
|
proxySetting(art, item, target, 'click', () => calls++)
|
|
getScope(art).dispose()
|
|
suspended.resume()
|
|
target.dispatchEvent(new Event('click'))
|
|
assert.equal(calls, 0)
|
|
assert.equal(item.$events.length, 0)
|
|
assert.equal(listeners.size, 0)
|
|
})
|
|
|
|
test('replacement cleanup failure still resumes the original registered listener', () => {
|
|
const item = {}
|
|
const { art, listeners } = fixture([item])
|
|
const target = new EventTarget()
|
|
let calls = 0
|
|
ownSettingItem(art, item)
|
|
proxySetting(art, item, target, 'click', () => calls++)
|
|
const suspended = suspendSettingItem(art, item)
|
|
ownSettingItem(art, item)
|
|
proxySetting(art, item, target, 'click', () => calls += 10)
|
|
const replacement = item.$events[0]
|
|
const failure = new Error('replacement cleanup')
|
|
const remove = art.events.remove
|
|
art.events.remove = (cleanup) => {
|
|
remove(cleanup)
|
|
if (cleanup === replacement)
|
|
throw failure
|
|
}
|
|
assert.throws(() => suspended.resume(), error => error.errors.length === 1 && error.errors[0] === failure)
|
|
target.dispatchEvent(new Event('click'))
|
|
assert.equal(calls, 1)
|
|
assert.equal(listeners.size, 1)
|
|
getScope(art).dispose()
|
|
})
|