mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-12 23:36:15 -08:00
test(mediabunny): [PKG-MB-01] freeze published proxy and dependency contracts
This commit is contained in:
1 parent
1f93bb9ba1
commit
1cfd2e17f3
18 files changed
+1357
-7
No files matched your search
@@ -44,7 +44,8 @@ function fixture(t) {
|
||||
test('Actual contract model assigns all 264 family rows and separates exact published versions from workspace snapshots', () => {
|
||||
assert.equal(actual.policy.packages.length, 22)
|
||||
assert.equal(actual.families.length, 12)
|
||||
assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 15)
|
||||
assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 17)
|
||||
assert.deepEqual(actual.policy.versions.filter(version => version.kind === 'published-baseline' && version.package === 'artplayer-proxy-mediabunny').map(version => version.version).sort(), ['1.0.0', '1.2.0'])
|
||||
assert.equal(actual.policy.versions.filter(version => version.kind === 'workspace-baseline').length, 22)
|
||||
assert.equal(actual.policy.cases.length, 10)
|
||||
const markdown = renderContracts(actual)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import lockfile from '@yarnpkg/lockfile'
|
||||
import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
||||
|
||||
export async function verifyMbContract() {
|
||||
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/mb-release.json'), 'utf8'))
|
||||
const archives = new Map()
|
||||
for (const release of [baseline.release, ...baseline.previous]) {
|
||||
const archive = await ensureArchive(release)
|
||||
assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort())
|
||||
for (const [file, expected] of Object.entries(release.files))
|
||||
assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`)
|
||||
assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), release.manifest)
|
||||
for (const field of ['main', 'module', 'types', 'legacy'])
|
||||
assert(Object.hasOwn(release.files, `package/${release.manifest[field].replace(/^\.\//, '')}`))
|
||||
const core = release.historicalCore
|
||||
const source = execFileSync('git', ['show', `${core.commit}:${core.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(source), core.sha256LF)
|
||||
assert.equal(JSON.parse(source).version, core.version)
|
||||
archives.set(release.version, archive)
|
||||
}
|
||||
const sources = new Map()
|
||||
for (const [file, expected] of Object.entries(baseline.source)) {
|
||||
const source = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(source), expected, `MediaBunny frozen source: ${file}`)
|
||||
sources.set(file, source)
|
||||
}
|
||||
const sdk = baseline.dependency
|
||||
for (const [file, expected] of Object.entries(sdk.files))
|
||||
assert.equal(hash(fs.readFileSync(path.join(refactorDir, '../node_modules', sdk.name, file))), expected, `Installed SDK provenance changed: ${file}`)
|
||||
const lock = lockfile.parse(fs.readFileSync(path.join(refactorDir, '../yarn.lock'), 'utf8'))
|
||||
assert.equal(lock.type, 'success')
|
||||
assert.equal(lock.object[`${sdk.name}@${sdk.declaredRange}`].version, sdk.version)
|
||||
return { baseline, archives, sources }
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const { baseline } = await verifyMbContract()
|
||||
console.log(`MediaBunny proxy verified: two releases, twelve archive members, ${Object.keys(baseline.source).length} frozen inputs; installed SDK ${baseline.dependency.version} provenance is separate from historical bundles`)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Node frozen contract runner.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { mbEnvironment, mbHistorical } from '../../test/helpers/mediabunny.js'
|
||||
import { diagnostics } from './factory-assignability.mjs'
|
||||
import { verifyMbContract } from './mb-contract.mjs'
|
||||
import { readMember } from './releases.mjs'
|
||||
|
||||
const contract = await verifyMbContract()
|
||||
const { baseline, archives } = contract
|
||||
const surface = JSON.parse(fs.readFileSync('refactor/baselines/mb-surface.json', 'utf8'))
|
||||
|
||||
test('MediaBunny freezes two real releases, twelve members, seventeen Git inputs and separately locked SDK provenance', () => {
|
||||
assert.deepEqual([baseline.release.version, ...baseline.previous.map(item => item.version)], ['1.2.0', '1.0.0'])
|
||||
assert.equal([baseline.release, ...baseline.previous].reduce((sum, item) => sum + Object.keys(item.files).length, 0), 12)
|
||||
assert.equal(Object.keys(baseline.source).length, 17)
|
||||
assert.equal(baseline.dependency.version, '1.56.1')
|
||||
assert.equal(baseline.dependency.manifestLicense, 'MPL-2.0')
|
||||
assert.deepEqual([baseline.release, ...baseline.previous].map(item => item.bundledDependencyVersion), [null, null])
|
||||
})
|
||||
|
||||
test('Historical proxy archives embed SDK code without separate license/notice members or MPL markers', () => {
|
||||
for (const release of [baseline.release, ...baseline.previous]) {
|
||||
const files = Object.keys(release.files)
|
||||
assert(!files.some(file => /license|notice|\.wasm$|worker/i.test(file)))
|
||||
assert.equal(release.manifest.license, 'MIT')
|
||||
const archive = archives.get(release.version)
|
||||
for (const field of ['main', 'legacy', 'module']) {
|
||||
const code = readMember(archive, `package/${release.manifest[field].replace(/^\.\//, '')}`).toString()
|
||||
assert.doesNotMatch(code, /Mozilla Public License|MPL-2\.0|This Source Code Form/)
|
||||
if (field === 'module') {
|
||||
assert.match(code, /AudioBufferSink/)
|
||||
assert.match(code, /CanvasSink/)
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.match(fs.readFileSync('node_modules/mediabunny/LICENSE', 'utf8'), /^Mozilla Public License Version 2\.0/)
|
||||
})
|
||||
|
||||
test('Published declarations preserve optional factory replacements and exact Canvas results in both supported compiler generations', () => {
|
||||
const source = `import Artplayer from 'artplayer'; import factory from './factory-history';
|
||||
type Option = Parameters<typeof factory>[0];
|
||||
const replacement: typeof factory = (_option?: Option) => (_art: Artplayer) => document.createElement('canvas');
|
||||
const result: ReturnType<ReturnType<typeof factory>> = document.createElement('canvas');
|
||||
factory(); factory({ volume: 0.7, source: new Blob([]), loadTimeout: 1000 }); void [replacement, result];`
|
||||
for (const release of [baseline.release, ...baseline.previous]) {
|
||||
const types = readMember(archives.get(release.version), `package/${release.manifest.types.replace(/^\.\//, '')}`).toString()
|
||||
for (const compiler of [ts, compat]) {
|
||||
assert.deepEqual(diagnostics(compiler, source, types), [])
|
||||
assert.deepEqual(diagnostics(compiler, `${source} factory({ volume: 'loud' });`, types).map(item => item.code), [2322])
|
||||
assert.deepEqual(diagnostics(compiler, `${source} factory({ m3u8: {} });`, types).map(item => item.code), release.version === '1.0.0' ? [compiler === compat ? 2345 : 2353] : [])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Actual main, legacy, browser globals and native ESM retain historical export shapes', async () => {
|
||||
for (const release of [baseline.release, ...baseline.previous]) {
|
||||
for (const field of ['main', 'legacy']) {
|
||||
const code = readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString()
|
||||
const env = mbEnvironment({ code })
|
||||
assert.equal(typeof env.exported, release.version === '1.0.0' ? 'object' : 'function')
|
||||
assert.equal(typeof env.factory(), 'function')
|
||||
const global = { ...env.globals }
|
||||
delete global.module
|
||||
delete global.exports
|
||||
vm.runInNewContext(code, global)
|
||||
const value = global.artplayerProxyMediabunny || global.window.artplayerProxyMediabunny
|
||||
assert.equal(typeof (value.default || value), 'function')
|
||||
}
|
||||
const code = readMember(archives.get(release.version), `package/${release.manifest.module.replace(/^\.\//, '')}`).toString()
|
||||
const module = await import(`data:text/javascript,${encodeURIComponent(code)}`)
|
||||
assert.deepEqual(Object.keys(module), ['default'])
|
||||
assert.equal(typeof module.default(), 'function')
|
||||
}
|
||||
})
|
||||
|
||||
for (const implementation of await mbHistorical()) {
|
||||
test(`MediaBunny ${implementation.name}: exact observable canvas surface and media defaults`, () => {
|
||||
const env = mbEnvironment(implementation)
|
||||
const canvas = env.factory()(env.art)
|
||||
const expected = surface.records.find(item => item.name === implementation.name)
|
||||
assert.equal(canvas, env.canvas)
|
||||
assert.deepEqual(Object.keys(canvas), expected.canvasKeys)
|
||||
assert.equal(Boolean(env.art.mediabunny), expected.artMediabunny)
|
||||
if (env.art.mediabunny) {
|
||||
assert.deepEqual(Object.getOwnPropertyNames(env.art.mediabunny), expected.shimOwn)
|
||||
assert.deepEqual(Object.getOwnPropertyNames(Object.getPrototypeOf(env.art.mediabunny)), expected.shimPrototype)
|
||||
}
|
||||
for (const [name, value] of Object.entries(expected.defaults)) assert.equal(canvas[name], value, name)
|
||||
for (const name of ['src', 'currentTime', 'play', 'duration', 'engine']) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(canvas, name)
|
||||
assert.equal(descriptor.enumerable, true)
|
||||
assert.equal(descriptor.configurable, true)
|
||||
assert.equal(typeof descriptor.get, 'function')
|
||||
assert.equal(typeof descriptor.set, 'function')
|
||||
}
|
||||
if (implementation.name === 'published-1.0.0') {
|
||||
assert.equal(Reflect.set(canvas, 'duration', 12), true, 'Older non-strict forwarding silently ignores readonly writes')
|
||||
assert(Number.isNaN(canvas.duration))
|
||||
}
|
||||
else {
|
||||
assert.throws(() => Reflect.set(canvas, 'duration', 12), { name: 'TypeError' })
|
||||
}
|
||||
assert.notEqual(canvas.play, canvas.play, 'Forwarded methods are rebound on every read')
|
||||
})
|
||||
|
||||
test(`MediaBunny ${implementation.name}: option reference, coercion and historical no-op setters`, () => {
|
||||
const env = mbEnvironment(implementation)
|
||||
const option = { volume: 2, muted: true, poster: 'old.png', autoplay: true, loop: true, crossOrigin: 'anonymous' }
|
||||
const canvas = env.factory(option)(env.art)
|
||||
assert.equal(canvas.option, option)
|
||||
assert.equal(canvas.volume, 2, 'Initial volume bypasses setter clamp')
|
||||
canvas.volume = '0.4'
|
||||
assert.equal(canvas.volume, 0.4)
|
||||
assert.equal(canvas.muted, false)
|
||||
canvas.volume = -1
|
||||
assert.equal(canvas.volume, 0)
|
||||
canvas.volume = 2
|
||||
assert.equal(canvas.volume, 1)
|
||||
canvas.muted = 'yes'
|
||||
assert.equal(canvas.muted, true)
|
||||
canvas.playbackRate = 2
|
||||
canvas.playbackRate = 0
|
||||
canvas.playbackRate = Number.NaN
|
||||
assert.equal(canvas.playbackRate, 2)
|
||||
canvas.poster = 'new.png'
|
||||
assert.equal(option.poster, 'new.png')
|
||||
canvas.autoplay = false
|
||||
canvas.loop = false
|
||||
canvas.crossOrigin = 'use-credentials'
|
||||
assert.equal(canvas.autoplay, true)
|
||||
assert.equal(canvas.loop, true)
|
||||
assert.equal(canvas.crossOrigin, 'anonymous')
|
||||
assert.deepEqual(env.emitted.map(item => item.name), ['video:volumechange', 'video:volumechange', 'video:volumechange', 'video:volumechange', 'video:ratechange'])
|
||||
})
|
||||
|
||||
test(`MediaBunny ${implementation.name}: native canvas methods win and configured shim events forward through ArtPlayer`, () => {
|
||||
const env = mbEnvironment(implementation)
|
||||
const canvas = env.factory()(env.art)
|
||||
assert.equal(canvas.getContext('2d'), env.context)
|
||||
assert.deepEqual(env.nativeCalls.at(-1), ['context', '2d', true])
|
||||
canvas.setAttribute('src', 'media.mp4')
|
||||
assert.equal(canvas.src, null, 'Canvas native setAttribute is not shim setAttribute')
|
||||
assert.deepEqual(env.nativeCalls.at(-1), ['attribute', 'src', 'media.mp4'])
|
||||
let nativeEvents = 0
|
||||
canvas.addEventListener('volumechange', () => nativeEvents++)
|
||||
for (const name of env.eventNames) {
|
||||
const detail = { name }
|
||||
canvas.events.emit(name, detail)
|
||||
const event = env.emitted.at(-1)
|
||||
assert.equal(event.name, `video:${name}`)
|
||||
assert.equal(event.args[0].type, name)
|
||||
assert.equal(event.args[0].detail, detail)
|
||||
}
|
||||
assert.equal(nativeEvents, 0)
|
||||
env.art.emit('resize')
|
||||
assert.deepEqual({ ...canvas.style }, { width: '100%', height: '100%', objectFit: 'contain' })
|
||||
})
|
||||
|
||||
test(`MediaBunny ${implementation.name}: sync and async forwarding, synthetic ranges and factory teardown`, async () => {
|
||||
const env = mbEnvironment(implementation)
|
||||
const canvas = env.factory()(env.art)
|
||||
const calls = []
|
||||
const promise = Promise.resolve()
|
||||
canvas.engine.play = () => promise
|
||||
canvas.engine.pause = () => calls.push('pause')
|
||||
canvas.engine.load = source => calls.push(['load', source])
|
||||
canvas.engine.seek = time => calls.push(['seek', time])
|
||||
canvas.engine.destroy = () => calls.push('destroy')
|
||||
assert.equal(canvas.play(), promise)
|
||||
assert.equal(canvas.pause(), undefined)
|
||||
canvas.src = 'first.mp4'
|
||||
assert.equal(canvas.load(), undefined)
|
||||
canvas.currentTime = '3.5'
|
||||
assert.equal(canvas.currentSrc, 'first.mp4')
|
||||
assert.equal(canvas.buffered.length, 0)
|
||||
Object.defineProperty(canvas.engine, 'duration', { value: 12, configurable: true })
|
||||
Object.defineProperty(canvas.engine, 'currentTime', { value: 3.5, configurable: true })
|
||||
assert.equal(canvas.buffered.end(99), 12, 'Synthetic range does not validate indices or measure downloaded bytes')
|
||||
assert.equal(canvas.played.end(0), 3.5)
|
||||
assert.equal(canvas.seekable.start(0), 0)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.art.mediabunny, undefined)
|
||||
assert.deepEqual(calls, ['pause', ['load', 'first.mp4'], ['load', 'first.mp4'], ['seek', 3.5], 'destroy'])
|
||||
await promise
|
||||
})
|
||||
|
||||
test(`MediaBunny ${implementation.name}: frame callback is one cancellable RAF, not decoded-frame evidence`, () => {
|
||||
const env = mbEnvironment(implementation)
|
||||
const canvas = env.factory()(env.art)
|
||||
const calls = []
|
||||
const first = canvas.requestVideoFrameCallback((...args) => calls.push(args))
|
||||
assert.equal(first, 0)
|
||||
canvas.cancelVideoFrameCallback(first)
|
||||
assert.equal(env.frames.size, 0)
|
||||
const second = canvas.requestVideoFrameCallback((...args) => calls.push(args))
|
||||
env.frames.get(second)(100)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.equal(calls[0][0], 100)
|
||||
assert.equal(calls[0][1].presentedFrames, 0)
|
||||
assert.equal(calls[0][1].mediaTime, 0)
|
||||
assert.equal(canvas.canPlayType('not/a-real-codec'), 'maybe')
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user