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 vm from 'node:vm' import validator from 'option-validator' import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs' import { loadPackage } from './load.js' function commonJS(bytes) { const module = { exports: {} } vm.runInNewContext(bytes.toString(), { module, exports: module.exports, get document() { return globalThis.document }, get window() { return globalThis.window }, setTimeout: (...args) => globalThis.setTimeout(...args), clearTimeout: id => globalThis.clearTimeout(id), console, }, { timeout: 5000 }) return module.exports.default || module.exports } export async function adsImplementations() { const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/ads-release.json', import.meta.url))) const archive = await ensureArchive(baseline.release) const member = `package/${baseline.release.manifest.main}` const bytes = readMember(archive, member) assert.equal(hash(bytes), baseline.release.files[member]) const file = 'packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js' const historical = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`]) assert.equal(hash(historical.toString().replaceAll('\r\n', '\n')), baseline.source[file]) const implementations = [ { name: 'source', factory: (await loadPackage('artplayer-plugin-ads')).default }, { name: 'published-1.0.6', historical: true, factory: commonJS(bytes) }, { name: 'frozen-workspace-2.1.0', historical: true, factory: commonJS(historical) }, ] for (const file of (process.env.ARTPLAYER_TEST_ADS || '').split(path.delimiter).filter(Boolean)) { const bytes = fs.readFileSync(file) const factory = file.endsWith('.mjs') ? (await import(`data:text/javascript;base64,${bytes.toString('base64')}`)).default : commonJS(bytes) implementations.push({ name: `artifact-${path.basename(file)}`, factory }) } return implementations } // Intent fixture only: these nodes do not model layout, HTML parsing or media decoding. export function adsEnvironment(t) { const document = Object.assign(new EventTarget(), { hidden: false }) const opened = [] for (const [name, value] of Object.entries({ document, window: { open: url => opened.push(url) } })) { const previous = Object.getOwnPropertyDescriptor(globalThis, name) Object.defineProperty(globalThis, name, { configurable: true, value }) t.after(() => previous ? Object.defineProperty(globalThis, name, previous) : Reflect.deleteProperty(globalThis, name)) } const timers = new Map() let nextId = 0 let now = 0 t.mock.method(globalThis, 'setTimeout', (callback, delay) => { const id = ++nextId timers.set(id, { callback, at: now + Number(delay) }) return id }) t.mock.method(globalThis, 'clearTimeout', id => timers.delete(id)) t.mock.method(Date, 'now', () => now) function tick(milliseconds) { const end = now + milliseconds let steps = 0 while (true) { const entry = [...timers].sort((a, b) => a[1].at - b[1].at)[0] if (!entry || entry[1].at > end) break assert(++steps < 10000, 'Unexpected endless timer chain') const [id, timer] = entry timers.delete(id) now = timer.at timer.callback() } now = end } function host() { const calls = [] const listeners = new Map() const proxies = [] const nodes = new Map() let normalized class Node extends EventTarget { constructor(tag = 'div', className = '', html = '') { super() this.tagName = tag.toUpperCase() this.className = className this.innerHTML = html this.style = {} this.children = [] this.muted = false this.paused = true this.ownerDocument = document if (className) nodes.set(className, this) } play() { calls.push(['ad.play']) this.paused = false return Promise.resolve() } pause() { calls.push(['ad.pause']) this.paused = true } remove() { if (this.parentNode) this.parentNode.children = this.parentNode.children.filter(child => child !== this) this.parentNode = null calls.push(['remove', this.className]) } removeAttribute(name) { calls.push(['removeAttribute', this.className, name]) delete this[name] } load() { calls.push(['ad.load']) } } const prefix = 'artplayer-plugin-ads' function append(parent, value) { if (typeof value !== 'string') { parent.children.push(value) value.parentNode = parent return value } const match = value.match(/^<(\w+)\s[^>]*class="([^"]*)"[^>]*>([\s\S]*)<\/\w+>$/) assert(match, 'Extend the fixture for a new wrapper; do not claim a real HTML parser') const [, tag, className, inner] = match const node = new Node(tag, className, inner) parent.children.push(node) node.parentNode = parent for (const child of inner.matchAll(/