mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:17:46 -08:00
90 lines
5.1 KiB
JavaScript
90 lines
5.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { validatePublishedCapture } from './api.mjs'
|
|
import { hash, refactorDir } from './releases.mjs'
|
|
|
|
export const lifecycleChecks = [
|
|
'LIFE.version', 'EVENT.chain', 'EVENT.context-arguments', 'EVENT.once-reentry',
|
|
'EVENT.dispatch-snapshot', 'EVENT.off-original-once', 'EVENT.throw-propagation',
|
|
'LIFE.ready-callback', 'MEDIA.metadata', 'MEDIA.play-promise', 'MEDIA.play-result',
|
|
'MEDIA.time-advances', 'MEDIA.pause-sync-stable', 'MEDIA.seek',
|
|
'MEDIA.quality-preserves-time', 'MEDIA.url-resets-resumes',
|
|
'LIFE.ready-once-after-switches', 'LIFE.restart-url', 'MEDIA.play-rejects-original',
|
|
'PLUGIN.async-promise', 'PLUGIN.async-registry', 'PLUGIN.duplicate-throws',
|
|
'MEDIA.latest-source', 'LIFE.destroy-retains-markup', 'LIFE.destroy-cleans-proxy',
|
|
'LIFE.destroy-removes-markup', 'LIFE.constructor-plugin-error',
|
|
]
|
|
|
|
export function validateLifecycleReport(report) {
|
|
assert.equal(report.kind, 'lifecycle')
|
|
assert.deepEqual(report.errors, [], 'Lifecycle capture failed')
|
|
assert.deepEqual(report.unhandled, [], 'Unexpected unhandled rejection')
|
|
assert.deepEqual(report.checks, lifecycleChecks.map(id => ({ id, passed: true })), 'Missing or failed lifecycle checks')
|
|
validatePublishedCapture(report, 'lifecycle')
|
|
for (const name of ['video.mp4', 'video2.mp4']) {
|
|
assert.equal(report.capture.media[name], hash(fs.readFileSync(path.resolve(refactorDir, '../docs/assets/sample', name))), `Media fixture changed: ${name}`)
|
|
}
|
|
const o = report.observations
|
|
assert(o.playback.progressed >= o.playback.start + 0.1 && o.playback.width > 0 && o.playback.duration > 2, 'No real-media advancement evidence')
|
|
assert(o.pause.observedMs >= 250 && o.pause.maxDelta < 0.02, 'No stable pause evidence')
|
|
assert.equal(o.readyCallbacks, 1, 'Ready callback repeated')
|
|
assert.equal(o.remainingInstances, 0, 'Probe leaked registered instances')
|
|
const order = (phase, earlier, later) => {
|
|
const events = report.trace.filter(item => item.phase === phase).map(item => item.event)
|
|
assert(events.indexOf(earlier) >= 0 && events.indexOf(earlier) < events.indexOf(later), `Unexpected ${phase} order: ${earlier} -> ${later}`)
|
|
}
|
|
order('initial', 'video:loadedmetadata', 'ready')
|
|
order('play', 'video:play', 'video:playing')
|
|
order('play', 'video:playing', 'play')
|
|
order('pause', 'pause', 'video:pause')
|
|
order('quality-paused', 'video:canplay', 'restart')
|
|
for (const item of o.expectedUnhandled) {
|
|
assert.deepEqual(item, { phase: 'controlled-resume-rejection', message: 'controlled switch resume rejection' }, 'Unexpected controlled rejection')
|
|
}
|
|
const tasks = JSON.parse(fs.readFileSync(path.join(refactorDir, 'tasks.json'), 'utf8')).tasks
|
|
assert.equal(new Set(report.findings.map(item => item.id)).size, report.findings.length, 'Duplicate finding IDs')
|
|
for (const finding of report.findings) assert(tasks.some(task => task.id === finding.owner), `Unknown finding owner: ${finding.owner}`)
|
|
}
|
|
|
|
export function lifecycleProfile(report) {
|
|
const o = report.observations
|
|
return {
|
|
checks: report.checks,
|
|
emitterOrder: o.emitterOrder,
|
|
readyCallbacks: o.readyCallbacks,
|
|
restarts: o.restarts,
|
|
concurrentSwitch: { outcomes: o.concurrentSwitch.outcomes, finalSource: o.concurrentSwitch.finalSource },
|
|
destroyPending: { switchState: o.destroyPending.switchState, latePluginRegistered: o.destroyPending.latePluginRegistered },
|
|
repeatedDestroy: o.repeatedDestroy,
|
|
failedConstructor: { hasMarkup: o.failedConstructor.hasMarkup, retainedListeners: o.failedConstructor.listeners > 0, registered: o.failedConstructor.registered },
|
|
rejectedResume: { switchState: o.rejectedResume.switchState, expectedUnhandled: o.rejectedResume.expectedUnhandled },
|
|
findings: report.findings.map(({ id, owner }) => ({ id, owner })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
}
|
|
}
|
|
|
|
export function verifyFrozenLifecycle() {
|
|
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/lifecycle.json'), 'utf8'))
|
|
assert.equal(frozen.schemaVersion, 1)
|
|
validateLifecycleReport(frozen.report)
|
|
assert.equal(hash(JSON.stringify(lifecycleProfile(frozen.report))), frozen.verification.profileSha256, 'Frozen lifecycle profile changed')
|
|
return frozen
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const frozen = verifyFrozenLifecycle()
|
|
const index = process.argv.indexOf('--compare')
|
|
if (index !== -1) {
|
|
assert(process.argv[index + 1], '--compare requires a report path')
|
|
const captured = JSON.parse(fs.readFileSync(process.argv[index + 1], 'utf8'))
|
|
validateLifecycleReport(captured)
|
|
assert.deepEqual(lifecycleProfile(captured), lifecycleProfile(frozen.report), 'Published lifecycle profile differs')
|
|
console.log('Published lifecycle capture matches semantic baseline; historical findings remain recorded')
|
|
}
|
|
else {
|
|
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --check or --compare <report>')
|
|
console.log(`Frozen lifecycle report verified: ${lifecycleChecks.length} checks; this command does not rerun the browser`)
|
|
}
|
|
}
|