Files
ArtPlayer/test/helpers/canvas.js
T

153 lines
4.5 KiB
JavaScript

import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyCanvasContract } from '../../refactor/scripts/canvas-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
export async function canvasHistorical() {
const contract = await verifyCanvasContract()
return [
...[contract.baseline.release, ...contract.baseline.previous].map(release => ({ name: `published-${release.version}`, source: readMember(contract.archives.get(release.version), `package/${release.manifest.main.replace(/^\.\//, '')}`).toString(), format: 'artifact' })),
{ name: 'frozen-workspace', source: contract.sources.get('packages/artplayer-proxy-canvas/src/index.js'), format: 'source' },
]
}
export async function canvasEnvironment(implementation, settings = {}) {
const calls = []
const draws = []
const listeners = new Map()
const proxies = []
const emitted = []
const timers = new Map()
const frames = new Map()
const cancelled = []
let nextTimer = 0
let nextFrame = settings.firstFrame ?? 0
const context = {
drawImage(...args) {
draws.push(args)
settings.onDraw?.(...args)
},
}
const canvas = {
nodeName: 'CANVAS',
width: 300,
height: 150,
title: 'canvas-title',
style: {},
getContext(kind) { return kind === '2d' && !settings.noContext ? context : null },
toDataURL(...args) {
calls.push({ name: 'toDataURL', receiver: this, args })
return 'data:canvas'
},
addEventListener(...args) { calls.push({ name: 'canvas-listener', receiver: this, args }) },
}
const video = {
nodeName: 'VIDEO',
videoWidth: 320,
videoHeight: 180,
width: 320,
height: 180,
title: 'video-title',
src: '',
currentTime: 0,
paused: true,
muted: false,
style: {},
play(...args) {
calls.push({ name: 'play', receiver: this, args })
this.paused = false
return Promise.resolve()
},
pause(...args) {
calls.push({ name: 'pause', receiver: this, args })
this.paused = true
},
addEventListener(...args) { calls.push({ name: 'video-listener', receiver: this, args }) },
}
const art = {
constructor: { config: { events: ['loadedmetadata', 'play', 'pause', 'seeked', 'error'] }, utils: { createElement: tag => tag === 'canvas' ? canvas : video } },
option: { autoSize: false },
template: { $player: { clientWidth: 640, clientHeight: 480 } },
isDestroy: false,
on(name, callback) {
if (!listeners.has(name))
listeners.set(name, new Set())
listeners.get(name).add(callback)
return art
},
off(name, callback) {
listeners.get(name)?.delete(callback)
return art
},
emit(name, ...args) {
emitted.push({ name, args })
for (const callback of [...(listeners.get(name) || [])]) callback(...args)
return art
},
proxy(target, name, callback) {
const entry = { target, name, callback }
proxies.push(entry)
return () => proxies.splice(proxies.indexOf(entry), 1)
},
destroy() {
art.isDestroy = true
art.emit('destroy')
},
}
const source = implementation.format === 'source' ? (await transform(implementation.source, { format: 'cjs', target: 'es2020' })).code : implementation.source
const module = { exports: {} }
vm.runInNewContext(source, {
module,
exports: module.exports,
window: {},
createImageBitmap: settings.createImageBitmap,
setTimeout(callback) {
const id = nextTimer++
timers.set(id, callback)
return id
},
clearTimeout(id) { timers.delete(id) },
requestAnimationFrame(callback) {
const id = nextFrame++
frames.set(id, callback)
return id
},
cancelAnimationFrame(id) {
cancelled.push(id)
frames.delete(id)
},
}, { timeout: 5000 })
return {
factory: module.exports.default || module.exports,
art,
canvas,
video,
context,
calls,
draws,
listeners,
proxies,
emitted,
timers,
frames,
cancelled,
flushTimers() {
const pending = [...timers.values()]
timers.clear()
for (const callback of pending) callback()
},
emitVideo(event) {
for (const entry of [...proxies]) {
if (entry.target === video && entry.name === event.type)
entry.callback(event)
}
},
async flush() {
// Resolve only the controlled draw/animation promise chain, without wall-clock sleeps.
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
},
}
}