Files
ArtPlayer/test/plugins.test.js

151 lines
6.0 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node runner.
import { test } from 'node:test'
import vm from 'node:vm'
import { loadModules } from './helpers/load.js'
const lifecycle = 'packages/artplayer/src/lifecycle/instance'
const { Plugins, beginLifecycle, getScope, destroyInstance } = await loadModules({
Plugins: 'packages/artplayer/src/plugins/index',
...Object.fromEntries(['beginLifecycle', 'getScope', 'destroyInstance'].map(name => [name, { file: lifecycle, name }])),
})
function create(plugins = []) {
const art = { option: { miniProgressBar: false, isLive: false, lock: false, autoPlayback: false, autoOrientation: false, fastForward: false, plugins } }
beginLifecycle(art)
return { art, registry: new Plugins(art) }
}
test('plugin registration keeps receiver, arguments, id, descriptors and naming precedence', () => {
const { art, registry } = create()
const result = { name: 'returnedName' }
function factory(value) {
assert.equal(this, art)
assert.equal(value, art)
assert.equal(arguments.length, 1)
return result
}
assert.equal(registry.add(factory), registry)
assert.equal(registry.returnedName, result)
assert.equal(registry.id, 1)
assert.deepEqual(Object.getOwnPropertyDescriptor(registry, 'returnedName'), { value: result, writable: false, enumerable: false, configurable: false })
function named() {}
assert.equal(registry.add(named), registry)
assert(Object.hasOwn(registry, 'named'))
registry.add((0, () => null))
assert.equal(registry.plugin3, null)
assert.throws(() => registry.add(() => result), /Cannot add a plugin that already has the same name: returnedName/)
assert.equal(registry.id, 4)
assert.deepEqual(Object.keys(registry), ['art', 'id'])
})
test('async factories preserve pending visibility, completion-time fallback and registry fulfillment', async () => {
const { registry } = create()
let resolve
const result = registry.add((0, () => new Promise(done => resolve = done)))
assert(result instanceof Promise)
assert(!Object.hasOwn(registry, 'plugin1'))
registry.add(() => ({ name: 'second' }))
resolve(undefined)
assert.equal(await result, registry)
assert(Object.hasOwn(registry, 'plugin2'))
assert(!Object.hasOwn(registry, 'plugin1'))
const failure = new Error('factory failure')
await assert.rejects(registry.add(() => Promise.reject(failure)), error => error === failure)
assert.throws(() => registry.add(() => {
throw failure
}), error => error === failure)
})
test('only same-realm Promises are awaited, preserving catch-less thenables and foreign promises', () => {
const { registry } = create()
const thenable = { name: 'thenable', then() {
throw new Error('must not assimilate')
} }
assert.equal(registry.add(() => thenable), registry)
assert.equal(registry.thenable, thenable)
const foreign = vm.runInNewContext('Promise.resolve({ name: "foreignResult" })')
const foreignFactory = () => foreign
assert.equal(registry.add(foreignFactory), registry)
assert.equal(registry.foreignFactory, foreign)
})
test('closed registries ignore late results without inspecting or destroying them and reject new factories', async () => {
const { art, registry } = create()
let resolve
const pending = registry.add(() => new Promise(done => resolve = done))
getScope(art).dispose()
resolve({ get name() {
throw new Error('late result must not be inspected')
}, destroy() {
throw new Error('not a lifecycle hook')
} })
assert.equal(await pending, registry)
assert.deepEqual(Object.getOwnPropertyNames(registry), ['art', 'id'])
assert.throws(() => registry.add(() => {
throw new Error('factory must not run')
}), /Cannot add a plugin after ArtPlayer is destroyed/)
assert.equal(registry.id, 1)
})
test('reentrant destruction in a name getter or property-key coercion cannot publish a result', () => {
for (const closeAt of [0, 1, 2, 3]) {
const { art, registry } = create()
const coercions = []
const name = { [Symbol.toPrimitive](hint) {
coercions.push(hint)
if (coercions.length === closeAt)
getScope(art).dispose()
return 'closing'
} }
const result = closeAt > 0
? { name }
: { get name() {
getScope(art).dispose()
return 'closing'
} }
assert.equal(registry.add(() => result), registry)
assert(!Object.hasOwn(registry, 'closing'))
assert.deepEqual(coercions, Array.from({ length: closeAt }, () => 'string'))
}
})
test('live key coercion order and symbol-name failure retain native behavior', () => {
const { registry } = create()
const calls = []
const result = { name: { [Symbol.toPrimitive](hint) {
calls.push(hint)
return `key${calls.length}`
} } }
registry.add(() => result)
assert.equal(registry.key3, result)
assert.deepEqual(calls, ['string', 'string', 'string'])
assert.throws(() => registry.add(() => ({ name: Symbol('name') })), TypeError)
})
test('constructor stops after a synchronous close and observes its own async rejection', async (context) => {
const failure = new Error('initial async failure')
const warnings = []
context.mock.method(console, 'warn', (...args) => warnings.push(args))
const { art } = create([() => Promise.reject(failure), function close() {
getScope(this).dispose()
}, () => {
throw new Error('must not start')
}])
assert(getScope(art).closed)
await new Promise(resolve => setImmediate(resolve))
assert.deepEqual(warnings, [['Failed to initialize ArtPlayer plugin:', failure]])
})
test('registration during reset is rejected before resource scope disposal', () => {
const { art, registry } = create()
Object.assign(art, { isDestroy: false, reset() {
assert.equal(getScope(art).closed, false)
assert.throws(() => registry.add(() => {
throw new Error('must not execute during reset')
}), /Cannot add a plugin after ArtPlayer is destroyed/)
}, template: { $video: {}, destroy() {} }, emit() {} })
destroyInstance(art, [art], true, true)
assert.equal(registry.id, 0)
})