test(vast): [PKG-VAST-02] cover SDK failures and lifecycle races

This commit is contained in:
Harvey Zhao committed 2026-09-12 09:19:01 +08:00
1 parent 4f39d6f2b9
commit d5e1feadf5
16 files changed
+1402 -12

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
// Controlled wrapper boundary, not an implementation of Google IMA or media playback.
// Self-contained so the same recorder can run in Node and a real browser realm.
export function createVastSdk(options = {}) {
const state = { players: [], loads: 0, errors: [], ...options }
let resolveLoad
let rejectLoad
const pending = new Promise((resolve, reject) => {
resolveLoad = resolve
rejectLoad = reject
})
const ima = { AdsRenderingSettings: class {}, AdsRequest: class {} }
const sdk = {
loadImaSdk() {
state.loads++
return state.deferLoad ? pending : Promise.resolve(ima)
},
PlayerOptions: class {},
Player: class {
constructor(...args) {
if (state.constructError)
throw state.constructError
this.args = args
this.requests = []
this.listeners = new Map()
this.destroyCalls = 0
state.players.push(this)
}
addEventListener(name, callback) {
if (state.listenError)
throw state.listenError
if (!this.listeners.has(name))
this.listeners.set(name, new Set())
this.listeners.get(name).add(callback)
}
removeEventListener(name, callback) {
this.listeners.get(name)?.delete(callback)
}
emit(name, detail) {
for (const callback of [...(this.listeners.get(name) || [])]) callback({ type: name, detail })
}
playAds(request) {
if (state.playError)
throw state.playError
this.requests.push(request)
}
destroy() {
this.destroyCalls++
if (state.destroyError)
throw state.destroyError
this.listeners.clear()
}
},
}
return { state, sdk, ima, resolveLoad: () => resolveLoad(ima), rejectLoad }
}
+107
View File
@@ -0,0 +1,107 @@
import assert from 'node:assert/strict'
import vm from 'node:vm'
import { build, transform } from 'esbuild'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { verifyVastContract } from '../../refactor/scripts/vast-contract.mjs'
import { resolveSource } from './load.js'
import { createVastSdk } from './vast-sdk.js'
export async function vastImplementations() {
const contract = await verifyVastContract()
const archive = contract.archives.get('artplayer-plugin-vast@1.0.0')
const { outputFiles } = await build({ entryPoints: [resolveSource('packages/artplayer-plugin-vast/src/index')], bundle: true, write: false, format: 'cjs', platform: 'browser', external: ['@glomex/vast-ima-player'] })
const frozen = async source => (await transform(source, { loader: 'js', format: 'cjs', target: 'es2020' })).code
return [
{ name: 'source', code: outputFiles[0].text },
{ name: 'published-1.0.0-source', historical: true, published: true, code: await frozen(readMember(archive, 'package/src/index.js').toString()) },
{ name: 'frozen-workspace-1.2.0', historical: true, code: await frozen(contract.sources.get('packages/artplayer-plugin-vast/src/index.js')) },
]
}
export function vastEnvironment(implementation, options = {}) {
const fixture = createVastSdk(options)
const module = { exports: {} }
class FixedDate extends Date {
static now() { return 1000 }
}
vm.runInNewContext(implementation.code, {
module,
exports: module.exports,
window: { google: { ima: fixture.ima } },
Date: FixedDate,
console: { error: (...args) => fixture.state.errors.push(args) },
require(name) {
assert.equal(name, '@glomex/vast-ima-player', 'Only the external SDK may be substituted')
return fixture.sdk
},
}, { timeout: 5000 })
const factory = module.exports.default || module.exports
function host() {
const listeners = new Map()
const calls = []
const node = () => ({ style: {}, parentNode: null })
const parent = {
children: [],
appendChild(child) {
if (child.parentNode)
child.parentNode.removeChild(child)
child.parentNode = this
this.children.push(child)
return child
},
removeChild(child) {
const index = this.children.indexOf(child)
assert(index !== -1, 'removeChild requires an owned node')
this.children.splice(index, 1)
child.parentNode = null
return child
},
}
const art = {
template: { $player: parent, $video: {} },
constructor: { utils: { createElement: node, setStyles: (target, styles) => Object.assign(target.style, styles), append: (target, child) => target.appendChild(child) } },
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
},
once(name, callback) {
const once = (...args) => {
art.off(name, once)
callback(...args)
}
return art.on(name, once)
},
emit(name, ...args) {
calls.push([name, ...args])
for (const callback of [...(listeners.get(name) || [])]) callback(...args)
return art
},
destroy() {
if (art.isDestroy)
return
art.isDestroy = true
art.emit('destroy')
listeners.clear()
},
}
return { art, parent, calls, listeners }
}
return { ...fixture, factory, host }
}
export function deferred() {
let resolve
let reject
const promise = new Promise((yes, no) => {
resolve = yes
reject = no
})
return { promise, resolve, reject }
}