mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
202 lines
7.2 KiB
JavaScript
202 lines
7.2 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 file = 'packages/artplayer/src/lifecycle/instance'
|
|
const { beginLifecycle, destroyInstance, duringTemplateMount, finishLifecycle, getScope, getFinalizationScope, ownContainer, isClosing, urlMix } = await loadModules({
|
|
...Object.fromEntries(['beginLifecycle', 'destroyInstance', 'duringTemplateMount', 'finishLifecycle', 'getScope', 'getFinalizationScope', 'ownContainer', 'isClosing'].map(name => [name, { file, name }])),
|
|
urlMix: 'packages/artplayer/src/player/urlMix',
|
|
})
|
|
|
|
test('container reservations prevent concurrent mounts and release after failed rollback', () => {
|
|
const container = {}
|
|
const calls = []
|
|
const first = { isDestroy: false, reset() {}, emit() {
|
|
calls.push('destroy')
|
|
} }
|
|
const second = { isDestroy: false, reset() {}, emit() {} }
|
|
assert.throws(() => getScope(first), /has not been initialized/)
|
|
beginLifecycle(first)
|
|
beginLifecycle(second)
|
|
assert.equal(isClosing(first), false)
|
|
ownContainer(first, container, () => {
|
|
assert(isClosing(first))
|
|
assert.throws(() => ownContainer(second, container, () => {}), /multiple instances/)
|
|
calls.push('rollback')
|
|
})
|
|
assert.throws(() => ownContainer(second, container, () => {}), /multiple instances/)
|
|
destroyInstance(first, [], false, false, true)
|
|
assert.deepEqual(calls, ['destroy', 'rollback'])
|
|
ownContainer(second, container, () => {
|
|
throw new Error('Successful mounting must discard rollback')
|
|
})
|
|
assert(finishLifecycle(second))
|
|
destroyInstance(second, [], true, false)
|
|
assert.equal(finishLifecycle(second), false)
|
|
})
|
|
|
|
test('direct scope disposal finalizes retained resources once without a destroy phase', () => {
|
|
const owner = {}
|
|
const calls = []
|
|
beginLifecycle(owner)
|
|
getFinalizationScope(owner).add(() => {
|
|
calls.push('finalized')
|
|
})
|
|
getScope(owner).dispose()
|
|
assert(isClosing(owner))
|
|
assert(getFinalizationScope(owner).closed)
|
|
getScope(owner).dispose()
|
|
assert.deepEqual(calls, ['finalized'])
|
|
})
|
|
|
|
test('cleanup failures preserve the first value and still finalize, roll back and release reservations', (context) => {
|
|
const failures = [new Error('scope'), new Error('finalizer'), new Error('rollback')]
|
|
const warnings = []
|
|
context.mock.method(console, 'warn', (...args) => warnings.push(args))
|
|
const container = {}
|
|
const owner = { isDestroy: false, reset() {}, emit() {} }
|
|
beginLifecycle(owner)
|
|
getScope(owner).add(() => {
|
|
throw failures[0]
|
|
})
|
|
getFinalizationScope(owner).add(() => {
|
|
throw failures[1]
|
|
})
|
|
ownContainer(owner, container, () => {
|
|
throw failures[2]
|
|
})
|
|
assert.throws(() => destroyInstance(owner, [owner], true, false, true), error => error === failures[0])
|
|
assert.deepEqual(warnings, failures.slice(1).map(error => ['Additional ArtPlayer cleanup failure:', error]))
|
|
assert(owner.isDestroy && getScope(owner).closed && getFinalizationScope(owner).closed)
|
|
const replacement = { isDestroy: false, reset() {}, emit() {} }
|
|
beginLifecycle(replacement)
|
|
ownContainer(replacement, container, () => {})
|
|
finishLifecycle(replacement)
|
|
destroyInstance(replacement, [], true, false)
|
|
})
|
|
|
|
test('mounting template cleanup precedes destroy without exposing an early public property', () => {
|
|
const calls = []
|
|
const owner = { isDestroy: false, reset() {
|
|
calls.push('reset')
|
|
}, emit() {
|
|
calls.push('destroy')
|
|
} }
|
|
beginLifecycle(owner)
|
|
const template = { $video: {}, destroy(removeHtml) {
|
|
calls.push(['template', removeHtml])
|
|
} }
|
|
duringTemplateMount(owner, template, () => {
|
|
assert.equal(Object.hasOwn(owner, 'template'), false)
|
|
destroyInstance(owner, [], false, true)
|
|
})
|
|
assert.deepEqual(calls, [['template', false], 'destroy'])
|
|
assert(owner.isDestroy)
|
|
assert.equal(Object.hasOwn(owner, 'template'), false)
|
|
})
|
|
|
|
test('temporary mounting ownership is cleared after failures and completed mounting', () => {
|
|
for (const fail of [false, true]) {
|
|
let cleanups = 0
|
|
const owner = { isDestroy: false, reset() {}, emit() {} }
|
|
beginLifecycle(owner)
|
|
const template = { destroy() {
|
|
cleanups++
|
|
} }
|
|
const failure = new Error('mount failed')
|
|
const mount = () => duringTemplateMount(owner, template, () => {
|
|
if (fail)
|
|
throw failure
|
|
})
|
|
if (fail)
|
|
assert.throws(mount, error => error === failure)
|
|
else
|
|
mount()
|
|
destroyInstance(owner, [], true, true)
|
|
assert.equal(cleanups, 0)
|
|
}
|
|
})
|
|
|
|
test('instance teardown preserves order, public state and first-call removeHtml across reentry', () => {
|
|
const calls = []
|
|
const instances = []
|
|
const owner = {
|
|
isDestroy: false,
|
|
reset() {
|
|
calls.push(['reset', this.isDestroy])
|
|
destroyInstance(this, instances, true, true)
|
|
},
|
|
template: { $video: {}, destroy(removeHtml) {
|
|
calls.push(['template', removeHtml])
|
|
} },
|
|
emit(name) {
|
|
calls.push([name, this.isDestroy, instances.includes(this)])
|
|
},
|
|
}
|
|
const keys = Object.keys(owner)
|
|
beginLifecycle(owner)
|
|
assert.deepEqual(Object.keys(owner), keys)
|
|
getScope(owner).add(() => {
|
|
calls.push(['resources', owner.isDestroy])
|
|
})
|
|
assert(finishLifecycle(owner))
|
|
instances.push(owner)
|
|
destroyInstance(owner, instances, false, true)
|
|
destroyInstance(owner, instances, true, true)
|
|
assert.deepEqual(calls, [['reset', false], ['resources', false], ['template', false], ['destroy', true, false]])
|
|
})
|
|
|
|
test('reset failure still releases resources, removes the instance and dispatches destroy', () => {
|
|
const error = new Error('reset failed')
|
|
let released = false
|
|
let emitted = false
|
|
const owner = { isDestroy: false, reset() {
|
|
throw error
|
|
}, template: { $video: {}, destroy() {} }, emit() { emitted = true } }
|
|
const instances = [owner]
|
|
beginLifecycle(owner)
|
|
getScope(owner).add(() => {
|
|
released = true
|
|
})
|
|
assert.throws(() => destroyInstance(owner, instances, true, true), value => value === error)
|
|
assert(released && emitted && owner.isDestroy)
|
|
assert.deepEqual(instances, [])
|
|
destroyInstance(owner, instances, true, true)
|
|
})
|
|
|
|
test('destroy listener error propagates unchanged after internal cleanup and remains consumed', () => {
|
|
const error = new Error('listener failed')
|
|
const sibling = {}
|
|
const owner = { isDestroy: false, reset() {}, emit() {
|
|
throw error
|
|
} }
|
|
const instances = [owner, sibling]
|
|
beginLifecycle(owner)
|
|
assert.throws(() => destroyInstance(owner, instances, true, false), value => value === error)
|
|
assert(getScope(owner).closed)
|
|
destroyInstance(owner, instances, true, false)
|
|
assert.deepEqual(instances, [sibling])
|
|
})
|
|
|
|
test('custom-type continuation cannot start after its timer resolves but before its owner is destroyed', async (context) => {
|
|
context.mock.timers.enable({ apis: ['setTimeout'] })
|
|
let calls = 0
|
|
const owner = {
|
|
isDestroy: false,
|
|
reset() {},
|
|
emit() {},
|
|
option: { type: 'custom', customType: { custom() { calls++ } } },
|
|
template: { $video: { src: '' }, destroy() {} },
|
|
loading: { show: false },
|
|
}
|
|
beginLifecycle(owner)
|
|
urlMix(owner)
|
|
owner.url = 'test.custom'
|
|
context.mock.timers.tick(0)
|
|
destroyInstance(owner, [], true, false)
|
|
await Promise.resolve()
|
|
assert.equal(calls, 0)
|
|
assert.equal(owner.loading.show, false)
|
|
})
|