Files
ArtPlayer/test/vconsole-lifecycle.test.js

142 lines
5.8 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Exercise scheduling ownership and cleanup failure semantics.
import test from 'node:test'
import { generateVconsole, hooks } from '../scripts/site-vendor/vconsole/build.ts'
import { enqueue, renderTab, unbind } from '../scripts/site-vendor/vconsole/lifecycle.ts'
function fixture() {
const queued = new Map()
const cancelled = []
const flushed = []
let sequence = 0
let restored = 0
const host = {
ADDED_LOG_PLUGIN_ID: ['default'],
logQueue: [],
flushLogScheduled: false,
_flushLogs() {
flushed.push(this.logQueue)
this.logQueue = []
},
unmockConsole() { restored++ },
}
const frames = {
requestAnimationFrame(callback) {
queued.set(++sequence, callback)
return sequence
},
cancelAnimationFrame(id) { cancelled.push(id) },
}
return { host, frames, queued, cancelled, flushed, restored: () => restored }
}
test('vConsole batches in original order and releases the scheduling flag before flushing', () => {
const { host, frames, queued, flushed } = fixture()
enqueue(host, 'one', frames)
enqueue(host, 'two', frames)
assert.equal(queued.size, 1)
queued.get(1)()
assert.equal(host.flushLogScheduled, false)
assert.deepEqual(flushed, [['one', 'two']])
enqueue(host, 'three', frames)
queued.get(2)()
assert.deepEqual(flushed, [['one', 'two'], ['three']])
})
test('vConsole cancelled callbacks cannot flush or consume a new instance queue', () => {
const { host, frames, queued, cancelled, flushed, restored } = fixture()
enqueue(host, 'old', frames)
host.ADDED_LOG_PLUGIN_ID = []
unbind(host, frames)
enqueue(host, 'captured old logger after destroy', frames)
assert.deepEqual(host.logQueue, [])
assert.deepEqual(cancelled, [1])
assert.equal(restored(), 1)
host.ADDED_LOG_PLUGIN_ID = ['default']
enqueue(host, 'new', frames)
queued.get(1)()
assert.equal(host.flushLogScheduled, true)
assert.deepEqual(host.logQueue, ['new'])
queued.get(2)()
assert.deepEqual(flushed, [['new']])
})
test('vConsole releases ownership and restores console even if native cancellation throws', () => {
const { host, frames, queued, restored, flushed } = fixture()
const failure = new Error('cancel failed')
enqueue(host, 'old', frames)
frames.cancelAnimationFrame = () => {
throw failure
}
assert.throws(() => unbind(host, frames), error => error === failure)
assert.equal(restored(), 1)
assert.equal(host.flushLogScheduled, false)
queued.get(1)()
assert.deepEqual(flushed, [])
})
test('vConsole allows reentrant logging while a prior batch is being flushed', () => {
const { host, frames, queued } = fixture()
const batches = []
host._flushLogs = () => {
batches.push(host.logQueue)
host.logQueue = []
if (batches.length === 1)
enqueue(host, 'reentrant', frames)
}
enqueue(host, 'initial', frames)
queued.get(1)()
queued.get(2)()
assert.deepEqual(batches, [['initial'], ['reentrant']])
})
test('vConsole delayed panels retain their original instance and plugin ownership', async () => {
const plugin = { id: 'custom' }
const host = { isInited: true, pluginList: { custom: plugin } }
const rendered = []
renderTab(host, plugin, () => rendered.push('removed'))
host.pluginList.custom = { id: 'custom' }
renderTab(host, host.pluginList.custom, () => rendered.push('replacement'))
const destroyed = { isInited: true, pluginList: { custom: plugin } }
renderTab(destroyed, plugin, () => rendered.push('destroyed'))
destroyed.isInited = false
await new Promise(resolve => setTimeout(resolve, 10))
assert.deepEqual(rendered, ['replacement'])
})
test('vConsole generation is deterministic and preserves all bytes outside pinned hooks', async () => {
const root = path.resolve('.')
const candidate = await generateVconsole(root)
assert.equal(candidate, await generateVconsole(root))
const upstream = fs.readFileSync('scripts/site-vendor/vconsole/upstream.js', 'utf8')
const original = upstream.split(hooks.factory)
assert.equal(original.length, 2)
const suffix = original[1]
.replace(hooks.signal, 'e._signalLog=function(t){__artplayerVConsoleLogLifecycle.enqueue(this,t,window)}')
.replace(hooks.unbind, '0===this.ADDED_LOG_PLUGIN_ID.length&&__artplayerVConsoleLogLifecycle.unbind(this,window)')
.replace(hooks.panel, 'o&&__artplayerVConsoleLogLifecycle.renderTab(e,t,(function(){var e=document.querySelector("#__vc_plug_"+t.id);n.HD(o)?e.innerHTML+=o:n.mf(o.appendTo)?o.appendTo(e):n.kK(o)&&e.insertAdjacentElement("beforeend",o)}))')
.replace(hooks.resizeResume, 'case 9:if(!s)return n.abrupt("return");F(p),X(p,W.getPosition(),k),0!==k&&H(N&&L),K();')
.replace(hooks.itemResume, 'case 12:if(!s)return o.abrupt("return");X(p,W.getPosition(),k),e(6,s.style.height=P+"px",s),K();')
assert(candidate.startsWith(`${original[0]}(this||self,(function(){var __artplayerVConsoleLogLifecycle=`))
assert(candidate.endsWith(`return function(){${suffix}`))
assert.equal(candidate, fs.readFileSync('docs/assets/js/vconsole.min.js', 'utf8'))
})
test('vConsole generator rejects changed upstream input before producing a candidate', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-vconsole-'))
try {
const directory = path.join(root, 'scripts/site-vendor/vconsole')
fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(path.join(directory, 'upstream.js'), 'unexpected source')
await assert.rejects(generateVconsole(root), /Frozen vConsole source changed/)
}
finally {
assert.equal(path.dirname(root), path.resolve(os.tmpdir()))
assert(path.basename(root).startsWith('artplayer-vconsole-'))
fs.rmSync(root, { recursive: true, force: true })
}
})