test(BASE-04): freeze published DOM input and demo baselines

This commit is contained in:
Harvey Zhao committed 2026-09-10 20:16:58 +08:00
1 parent 9489392905
commit 2b7054d8a5
16 files changed
+3193 -17

No files matched your search

+3 -2
View File
@@ -37,7 +37,7 @@ 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}`)
const reportName = { '/reports/api': 'api', '/reports/lifecycle': 'lifecycle' }[url.pathname]
const reportName = { '/reports/api': 'api', '/reports/lifecycle': 'lifecycle', '/reports/dom': 'dom' }[url.pathname]
if (req.method === 'POST' && reportName) {
assert.equal(req.headers.origin, `http://127.0.0.1:${port}`, 'Unexpected report origin')
const chunks = []
@@ -48,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, reportName === 'api' ? 'public-api' : 'lifecycle')
assert.equal(report.kind, reportName === 'api' ? 'public-api' : reportName)
report.capture = {
capturedAt: new Date().toISOString(),
releases: baseline.releases.map(release => ({
@@ -62,6 +62,7 @@ const server = http.createServer(async (req, res) => {
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)))]))
}
if (reportName === 'dom') report.capture.media = { 'video.mp4': hash(fs.readFileSync(path.join(root, 'docs/assets/sample/video.mp4'))) }
const output = path.join(refactorDir, '.cache/reports')
fs.mkdirSync(output, { recursive: true })
fs.writeFileSync(path.join(output, `${reportName}.json`), `${JSON.stringify(report, null, 2)}\n`)
+70
View File
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { hash, refactorDir } from './releases.mjs'
const root = path.resolve(refactorDir, '..')
const read = name => fs.readFileSync(path.join(root, name), 'utf8').replaceAll('\r\n', '\n')
function htmlFiles(dir = 'docs') {
return fs.readdirSync(path.join(root, dir), { withFileTypes: true }).flatMap(entry => {
const name = `${dir}/${entry.name}`
if (entry.isDirectory()) return ['compiled', 'uncompiled', 'node_modules'].includes(entry.name) ? [] : htmlFiles(name)
return entry.name.endsWith('.html') ? [name] : []
}).sort()
}
function menuLinks() {
return [...read('docs/index.html').matchAll(/href="([^"\n]*[?&]example=[^"\n]*)"/g)].map(match => match[1].replaceAll('&', '&'))
}
export function captureDemoInventory() {
const links = menuLinks()
const inventory = JSON.parse(read('refactor/package-inventory.json'))
const examples = fs.readdirSync(path.join(root, 'docs/assets/example')).filter(name => name.endsWith('.js')).sort().map(file => {
const name = file.slice(0, -3)
const source = `docs/assets/example/${file}`
const linked = links.filter(link => new URL(link, 'http://localhost:8082').searchParams.get('example') === name)
const libs = [...new Set(linked.flatMap(link => (new URL(link, 'http://localhost:8082').searchParams.get('libs') || '').split('\n').filter(Boolean)))]
return { name, source, sha256Lf: hash(read(source)), menuLinks: linked, route: linked[0] || `/?example=${name}`, libs,
status: 'not-run', owner: 'EX-03',
note: name === 'thumbnail' ? 'References artplayer-plugin-thumbnail, which is absent from the 22 current workspace packages; dependency and route need reconciliation.' : linked.length ? 'Existing menu route; SDK and media loading have not been validated.' : 'No menu link; example route follows common.js loader. Required globals are not automatically loaded.' }
})
const pages = htmlFiles().map(source => {
const text = read(source)
const uncommented = text.replace(/<!--[\s\S]*?-->/g, '')
return { source, route: `/${source.slice(5)}`.replace(/index\.html$/, ''), sha256Lf: hash(text),
kind: source.startsWith('docs/document/') ? 'generated-docs' : source.includes('googlebc') ? 'site-verification' : source === 'docs/upscaler/index.html' ? 'standalone-tool' : 'demo-or-editor',
scriptSources: [...uncommented.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["']/g)].map(match => match[1]),
status: 'not-run', owner: source.includes('googlebc') ? 'SITE-01' : 'EX-03' }
})
return { schemaVersion: 1, task: 'BASE-04', sourceCommit: '94893929', capturedAt: new Date().toISOString(),
note: 'Historical source inventory, not browser or network evidence. ScriptSources excludes comments but does not resolve inline scripts, import maps or dynamic SDK loading. Hashes use LF and describe this capture; check validates coverage, not current content identity.',
examples, pages,
packages: inventory.packages.map(pkg => ({ name: pkg.name, examples: examples.filter(example => example.libs.some(lib => lib.includes(`/${pkg.name}/`))).map(example => example.name), status: 'not-run', owner: pkg.name === 'artplayer-vitepress' ? 'SITE-01' : 'EX-03' })),
editor: { source: 'docs/assets/js/common.js', sha256Lf: hash(read('docs/assets/js/common.js')), storage: ['prod', 'ts', 'code', 'log'], isolation: 'Use separate test origin; record keys, do not erase user browser data.', core: { prodTrue: './compiled/artplayer.js', otherwise: './uncompiled/artplayer/index.js' }, pluginSources: 'URL libs parameter; independent of prod toggle', execution: 'Run evaluates editor text; TS mode alone is not strict tsc validation.', runtimeStatus: 'not-run' },
}
}
export function validateDemoInventory(inventory) {
assert.equal(inventory.schemaVersion, 1)
assert.deepEqual(inventory.examples.map(item => item.source).sort(), fs.readdirSync(path.join(root, 'docs/assets/example')).filter(name => name.endsWith('.js')).map(name => `docs/assets/example/${name}`).sort(), 'Demo file coverage drift')
assert.deepEqual(inventory.pages.map(item => item.source).sort(), htmlFiles(), 'HTML route coverage drift')
assert.deepEqual(inventory.examples.flatMap(item => item.menuLinks).sort(), menuLinks().sort(), 'Editor link coverage drift')
assert.deepEqual(inventory.packages.map(item => item.name).sort(), JSON.parse(read('refactor/package-inventory.json')).packages.map(item => item.name).sort(), 'Package demo coverage drift')
const names = new Set(inventory.examples.map(item => item.name))
for (const pkg of inventory.packages) for (const name of pkg.examples) assert(names.has(name), `Unknown example: ${name}`)
assert(inventory.examples.find(item => item.name === 'thumbnail').note.includes('absent'), 'Missing historical plugin exception')
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const filename = path.join(refactorDir, 'baselines/demo-inventory.json')
if (process.argv.includes('--capture')) {
assert(!fs.existsSync(filename), 'Do not overwrite the historical inventory; reconcile changes explicitly')
const inventory = captureDemoInventory()
validateDemoInventory(inventory)
fs.writeFileSync(filename, `${JSON.stringify(inventory, null, 2)}\n`)
}
else {
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --capture or --check')
const inventory = JSON.parse(fs.readFileSync(filename, 'utf8'))
validateDemoInventory(inventory)
console.log(`Demo coverage: ${inventory.examples.length} examples, ${inventory.pages.length} HTML paths, ${inventory.packages.length} packages; no browser pass implied`)
}
}
+21
View File
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import test from 'node:test'
import { validateDemoInventory } from './demos.mjs'
import { refactorDir } from './releases.mjs'
test('Demo inventory rejects missing files, routes and ecosystem packages', () => {
const inventory = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/demo-inventory.json'), 'utf8'))
validateDemoInventory(inventory)
for (const mutate of [
value => value.examples.pop(),
value => value.pages.pop(),
value => value.examples.find(item => item.menuLinks.length).menuLinks.pop(),
value => value.packages.pop(),
]) {
const changed = structuredClone(inventory)
mutate(changed)
assert.throws(() => validateDemoInventory(changed))
}
})
+78
View File
@@ -0,0 +1,78 @@
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 domChecks = [
'DOM.chapter-markup', 'DOM.control-click', 'DOM.control-remove', 'DOM.control-update',
'DOM.destroy', 'DOM.fullscreen-web-enter', 'DOM.fullscreen-web-exit', 'DOM.input-ignores-hotkey',
'DOM.keyboard-seek', 'DOM.layer-remove', 'DOM.narrow-container', 'DOM.setting-remove',
'DOM.setting-select', 'DOM.setting-update', 'DOM.user-style', 'DOM.version-template',
]
export function validateDomReport(report) {
assert.equal(report.kind, 'dom')
assert.deepEqual(report.errors, [], 'DOM capture failed')
assert.deepEqual(report.unhandled, [], 'Unexpected DOM rejection')
assert.deepEqual(report.checks, domChecks.map(id => ({ id, passed: true })), 'Missing DOM checks')
validatePublishedCapture(report, 'dom')
assert.equal(report.capture.media['video.mp4'], hash(fs.readFileSync(path.resolve(refactorDir, '../docs/assets/sample/video.mp4'))), 'DOM media changed')
const s = report.snapshot
assert(s.template.includes('art-video-player') && s.tree.children.length && s.cssVariables.length > 0 && s.cssClasses.length > 0, 'Incomplete DOM/CSS snapshot')
assert.equal(s.chapterStylePresent, true)
assert(s.controlAttributes.length >= 8)
assert.deepEqual(report.observations.selected, { html: 'High', defaults: [false, true] })
for (const [name, width, height] of [['wide', 640, 360], ['narrow', 320, 180]]) {
assert.equal(report.observations[name].width, width)
assert.equal(report.observations[name].height, height)
assert(report.observations[name].controls.length >= 8, 'Missing layout measurement')
}
const keys = report.interactions.filter(item => item.code)
assert(keys.every(item => item.trusted), 'Input was not trusted browser interaction')
const seek = keys.find(item => item.code === 'ArrowRight' && item.target !== 'INPUT')
assert(seek?.focused && seek.afterTime === seek.time + 5 && seek.afterHotkeys === seek.hotkeys + 1, 'Missing real keyboard seek')
const input = keys.find(item => item.code === 'ArrowRight' && item.target === 'INPUT')
assert(input?.focused && input.afterTime === input.time && input.afterHotkeys === input.hotkeys, 'Input consumed a player hotkey')
assert(keys.some(item => item.code === 'Escape' && item.focused && item.afterHotkeys === item.hotkeys + 1), 'Missing Escape interaction')
assert.equal(keys.filter(item => item.code === 'Tab').length, 2, 'Missing Tab traversal')
const focus = report.interactions.filter(item => item.focus).slice(0, 3)
assert.deepEqual(focus, [{ focus: 'BUTTON', label: null, id: 'tab-start' }, { focus: 'INPUT', label: 'Typing probe', id: '' }, { focus: 'BUTTON', label: null, id: 'narrow' }], 'Published focus traversal changed; assess accessibility improvements explicitly')
}
export function domProfile(report) {
return {
snapshot: report.snapshot,
checks: report.checks,
selected: report.observations.selected,
layouts: Object.fromEntries(['wide', 'narrow'].map(name => {
const { width, height, controls } = report.observations[name]
return [name, { width, height, controls: controls.map(({ class: className, withinPlayer, display }) => ({ class: className, withinPlayer, display })) }]
})),
}
}
export function verifyFrozenDom() {
const frozen = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/dom.json'), 'utf8'))
assert.equal(frozen.schemaVersion, 1)
validateDomReport(frozen.report)
assert.equal(hash(JSON.stringify(domProfile(frozen.report))), frozen.verification.profileSha256, 'Frozen DOM profile changed')
return frozen
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const frozen = verifyFrozenDom()
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'))
validateDomReport(captured)
assert.deepEqual(domProfile(captured), domProfile(frozen.report), 'Published DOM profile differs; review the change')
console.log('Published DOM capture matches baseline; this comparison is not a candidate accessibility gate')
}
else {
assert(process.argv.slice(2).every(arg => arg === '--check'), 'Use --check or --compare <report>')
console.log(`Frozen DOM verified: ${domChecks.length} checks; this command does not rerun the browser`)
}
}
+37
View File
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { domProfile, validateDomReport, verifyFrozenDom } from './dom.mjs'
test('DOM baseline rejects absent interaction, fake input and wrong media', () => {
const frozen = verifyFrozenDom().report
for (const mutate of [
report => report.checks.pop(),
report => report.interactions.find(item => item.code === 'ArrowRight').trusted = false,
report => report.interactions.find(item => item.code === 'ArrowRight').afterTime = 0,
report => report.interactions.find(item => item.code === 'ArrowRight' && item.target === 'INPUT').afterHotkeys++,
report => report.capture.media['video.mp4'] = 'wrong',
report => report.unhandled.push('unexpected'),
]) {
const changed = structuredClone(frozen)
mutate(changed)
assert.throws(() => validateDomReport(changed))
}
})
test('DOM comparison detects class, variable and layout changes without freezing subpixel widths', () => {
const frozen = verifyFrozenDom().report
const expected = domProfile(frozen)
const jitter = structuredClone(frozen)
jitter.observations.wide.controls[0].width += 0.01
assert.deepEqual(domProfile(jitter), expected)
for (const mutate of [
report => report.snapshot.tree.attributes.class = 'renamed',
report => report.snapshot.cssVariables.pop(),
report => report.snapshot.controlAttributes[0].role = 'button',
report => report.observations.narrow.controls[0].withinPlayer = false,
]) {
const changed = structuredClone(frozen)
mutate(changed)
assert.notDeepEqual(domProfile(changed), expected)
}
})