mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 01:26:16 -08:00
test(refactor): [BASE-03] capture media and lifecycle behavior
This commit is contained in:
1 parent
b382ec08f7
commit
9489392905
15 files changed
+1333
-20
No files matched your search
@@ -12,10 +12,7 @@ export const checkIds = [
|
||||
'API-05.destroy-return', 'API-05.instance-cleanup',
|
||||
]
|
||||
|
||||
export function validateApiReport(report) {
|
||||
assert.equal(report.kind, 'public-api')
|
||||
assert.deepEqual(report.errors, [], 'Browser capture failed')
|
||||
assert.deepEqual(report.checks, checkIds.map(id => ({ id, passed: true })), 'Missing or failed API checks')
|
||||
export function validatePublishedCapture(report, fixture) {
|
||||
assert(report.environment.userAgent && report.environment.language, 'Missing browser environment')
|
||||
assert(Number.isFinite(Date.parse(report.capture.capturedAt)), 'Missing capture timestamp')
|
||||
const releases = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases
|
||||
@@ -24,12 +21,19 @@ export function validateApiReport(report) {
|
||||
return { name: release.name, version: release.version, integrity: release.integrity, member, sha256: release.files[member] }
|
||||
})
|
||||
assert.deepEqual(report.capture.releases, expectedSources, 'Published sources differ')
|
||||
assert.deepEqual(report.scripts, [...expectedSources.map(release => `/releases/${release.name}/${release.member.slice('package/'.length)}`), '/fixtures/api.js'], 'Unexpected loaded scripts')
|
||||
assert.deepEqual(report.scripts, [...expectedSources.map(release => `/releases/${release.name}/${release.member.slice('package/'.length)}`), `/fixtures/${fixture}.js`], 'Unexpected loaded scripts')
|
||||
assert.equal(report.capture.fixtureHashAlgorithm, 'sha256-lf')
|
||||
for (const name of ['api.html', 'api.js']) {
|
||||
for (const name of [`${fixture}.html`, `${fixture}.js`]) {
|
||||
const source = fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n')
|
||||
assert.equal(report.capture.fixtures[name], hash(source), `Fixture changed: ${name}; review the baseline explicitly`)
|
||||
}
|
||||
}
|
||||
|
||||
export function validateApiReport(report) {
|
||||
assert.equal(report.kind, 'public-api')
|
||||
assert.deepEqual(report.errors, [], 'Browser capture failed')
|
||||
assert.deepEqual(report.checks, checkIds.map(id => ({ id, passed: true })), 'Missing or failed API checks')
|
||||
validatePublishedCapture(report, 'api')
|
||||
for (const section of ['static', 'prototype', 'emitterPrototype', 'defaults', 'constants', 'config', 'utilityDescriptors', 'instance', 'instanceResolved', 'chapterFactory', 'chapterResult', 'integrations']) {
|
||||
assert(report.snapshot?.[section] && Object.keys(report.snapshot[section]).length, `Missing API section: ${section}`)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ function sendBytes(req, res, bytes, filename) {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, `http://127.0.0.1:${port}`)
|
||||
if (req.method === 'POST' && url.pathname === '/reports/api') {
|
||||
const reportName = { '/reports/api': 'api', '/reports/lifecycle': 'lifecycle' }[url.pathname]
|
||||
if (req.method === 'POST' && reportName) {
|
||||
assert.equal(req.headers.origin, `http://127.0.0.1:${port}`, 'Unexpected report origin')
|
||||
const chunks = []
|
||||
let size = 0
|
||||
@@ -47,7 +48,7 @@ const server = http.createServer(async (req, res) => {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
const report = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
assert.equal(report.kind, 'public-api')
|
||||
assert.equal(report.kind, reportName === 'api' ? 'public-api' : 'lifecycle')
|
||||
report.capture = {
|
||||
capturedAt: new Date().toISOString(),
|
||||
releases: baseline.releases.map(release => ({
|
||||
@@ -56,11 +57,14 @@ const server = http.createServer(async (req, res) => {
|
||||
sha256: release.files[`package/${release.manifest.main.replace(/^\.\//, '')}`],
|
||||
})),
|
||||
fixtureHashAlgorithm: 'sha256-lf',
|
||||
fixtures: Object.fromEntries(['api.html', 'api.js'].map(name => [name, hash(fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n'))])),
|
||||
fixtures: Object.fromEntries([`${reportName}.html`, `${reportName}.js`].map(name => [name, hash(fs.readFileSync(path.join(refactorDir, 'fixtures', name), 'utf8').replaceAll('\r\n', '\n'))])),
|
||||
}
|
||||
if (reportName === 'lifecycle') {
|
||||
report.capture.media = Object.fromEntries(['video.mp4', 'video2.mp4'].map(name => [name, hash(fs.readFileSync(path.join(root, 'docs/assets/sample', name)))]))
|
||||
}
|
||||
const output = path.join(refactorDir, '.cache/reports')
|
||||
fs.mkdirSync(output, { recursive: true })
|
||||
fs.writeFileSync(path.join(output, 'api.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
fs.writeFileSync(path.join(output, `${reportName}.json`), `${JSON.stringify(report, null, 2)}\n`)
|
||||
res.writeHead(204).end()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { lifecycleProfile, validateLifecycleReport, verifyFrozenLifecycle } from './lifecycle.mjs'
|
||||
|
||||
test('lifecycle evidence rejects fake progress, unexpected errors and event reordering', () => {
|
||||
const { report } = verifyFrozenLifecycle()
|
||||
const stopped = structuredClone(report)
|
||||
stopped.observations.playback.progressed = stopped.observations.playback.start
|
||||
assert.throws(() => validateLifecycleReport(stopped), /No real-media advancement/)
|
||||
const error = structuredClone(report)
|
||||
error.unhandled.push('unexpected error')
|
||||
assert.throws(() => validateLifecycleReport(error), /Unexpected unhandled/)
|
||||
const reordered = structuredClone(report)
|
||||
const phase = reordered.trace.filter(item => item.phase === 'play')
|
||||
const native = phase.find(item => item.event === 'video:playing')
|
||||
const publicEvent = phase.find(item => item.event === 'play')
|
||||
native.event = 'play'
|
||||
publicEvent.event = 'video:playing'
|
||||
assert.throws(() => validateLifecycleReport(reordered), /Unexpected play order/)
|
||||
})
|
||||
|
||||
test('lifecycle profile ignores media clock jitter but retains lifecycle findings', () => {
|
||||
const { report } = verifyFrozenLifecycle()
|
||||
const jitter = structuredClone(report)
|
||||
jitter.observations.playback.progressed += 0.03
|
||||
jitter.observations.pause.observedMs += 10
|
||||
assert.deepEqual(lifecycleProfile(jitter), lifecycleProfile(report))
|
||||
const changed = structuredClone(report)
|
||||
changed.observations.repeatedDestroy.survivorAfter = true
|
||||
assert.notDeepEqual(lifecycleProfile(changed), lifecycleProfile(report))
|
||||
})
|
||||
Reference in new issue
Block a user