mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-15 00:56:16 -08:00
test(vast): [PKG-VAST-02] cover SDK failures and lifecycle races
This commit is contained in:
1 parent
4f39d6f2b9
commit
d5e1feadf5
16 files changed
+1402
-12
No files matched your search
@@ -64,6 +64,11 @@ const adsCoreMember = `package/${adsCore.manifest.main}`
|
||||
const adsCoreBytes = readMember(await ensureArchive(adsCore), adsCoreMember)
|
||||
assert.equal(hash(adsCoreBytes), adsCore.files[adsCoreMember], 'Historical Ads core differs from frozen release')
|
||||
add('/published-4.5.5/artplayer.js', adsCoreBytes, { kind: 'npm-release', version: adsCore.version, integrity: adsCore.integrity, member: adsCoreMember })
|
||||
const vastCore = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/vast-core.json'), 'utf8')).release
|
||||
const vastCoreMember = `package/${vastCore.manifest.main}`
|
||||
const vastCoreBytes = readMember(await ensureArchive(vastCore), vastCoreMember)
|
||||
assert.equal(hash(vastCoreBytes), vastCore.files[vastCoreMember], 'Historical VAST core differs from frozen release')
|
||||
add('/published-5.1.7/artplayer.js', vastCoreBytes, { kind: 'npm-release', version: vastCore.version, integrity: vastCore.integrity, member: vastCoreMember })
|
||||
add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/media/audio-tone.m4a')), { kind: 'generated-media', file: 'test/browser/media/audio-tone.m4a' })
|
||||
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
|
||||
@@ -128,7 +133,7 @@ const server = http.createServer((req, res) => {
|
||||
if (url.pathname === '/test/player.html') {
|
||||
const core = url.searchParams.get('core') || 'candidate'
|
||||
const chapter = url.searchParams.get('chapter') || 'candidate'
|
||||
assert(['candidate', 'published', 'published-4.5.5'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
|
||||
assert(['candidate', 'published', 'published-4.5.5', 'published-5.1.7'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
|
||||
const html = fs.readFileSync(path.join(workspace, 'test/browser/player.html'), 'utf8')
|
||||
.replace('__CORE__', core)
|
||||
.replace('__CHAPTER__', chapter)
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { createVastSdk } from '../helpers/vast-sdk.js'
|
||||
import { vastImplementations } from '../helpers/vast.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementations = await vastImplementations()
|
||||
|
||||
async function setup(page, core, implementation, testInfo, deferLoad = false) {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: `window.createVastSdk = ${createVastSdk.toString()};` })
|
||||
await page.evaluate((deferLoad) => {
|
||||
window.vastSdk = window.createVastSdk({ deferLoad })
|
||||
window.google = { ima: window.vastSdk.ima }
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true })
|
||||
}, deferLoad)
|
||||
await page.addScriptTag({ content: `(() => {
|
||||
const module = { exports: {} };
|
||||
const exports = module.exports;
|
||||
const require = name => {
|
||||
if (name !== '@glomex/vast-ima-player') throw new Error('Unexpected dependency: ' + name);
|
||||
return window.vastSdk.sdk;
|
||||
};
|
||||
${implementation.code}
|
||||
window.artplayerPluginVast = module.exports.default || module.exports;
|
||||
})();` })
|
||||
await testInfo.attach('vast-inputs', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, sha256: hash(implementation.code), sdk: 'controlled wrapper recorder, no IMA execution' }) })
|
||||
}
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
const state = await page.evaluate(() => ({
|
||||
destroyed: window.art?.isDestroy,
|
||||
callbackCalls: window.vastCalls,
|
||||
players: window.vastSdk?.state.players.map(player => ({ requests: player.requests, destroyCalls: player.destroyCalls, connected: player.args[2].isConnected, display: player.args[2].style.display })),
|
||||
coreVersion: window.Artplayer?.version,
|
||||
}))
|
||||
await testInfo.attach('vast-state', { contentType: 'application/json', body: JSON.stringify(state) })
|
||||
await page.evaluate(() => {
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
// Historical wrappers leak; explicit fixture cleanup runs only after assertions/evidence.
|
||||
for (const player of window.vastSdk?.state.players || []) player.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
for (const core of ['published-5.1.7', 'published', 'candidate']) {
|
||||
for (const implementation of implementations) {
|
||||
const label = `${core} / ${implementation.name}`
|
||||
test(`${label}: async SDK and callback gate real core registration and preserve requests`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, implementation, testInfo, true)
|
||||
await page.evaluate(() => {
|
||||
window.vastCalls = 0
|
||||
window.vastDone = false
|
||||
const gate = new Promise((resolve) => {
|
||||
window.finishVastCallback = resolve
|
||||
})
|
||||
window.vastRegistration = window.art.plugins.add(window.artplayerPluginVast(async (context) => {
|
||||
window.vastContext = context
|
||||
window.vastCalls++
|
||||
await gate
|
||||
context.playUrl('/test/ad-tag.xml')
|
||||
context.playRes('<VAST/>')
|
||||
})).then(() => {
|
||||
window.vastDone = true
|
||||
})
|
||||
})
|
||||
expect(await page.evaluate(() => ({ calls: window.vastCalls, players: window.vastSdk.state.players.length, registered: Object.hasOwn(window.art.plugins, 'artplayerPluginVast') }))).toEqual({ calls: 0, players: 0, registered: false })
|
||||
await page.evaluate(() => window.vastSdk.resolveLoad())
|
||||
await expect.poll(() => page.evaluate(() => window.vastCalls)).toBe(1)
|
||||
expect(await page.evaluate(() => window.vastDone)).toBe(false)
|
||||
expect(await page.evaluate(() => Object.hasOwn(window.art.plugins, 'artplayerPluginVast'))).toBe(false)
|
||||
await page.evaluate(() => window.finishVastCallback())
|
||||
await page.evaluate(() => window.vastRegistration)
|
||||
const result = await page.evaluate(() => ({
|
||||
done: window.vastDone,
|
||||
name: window.art.plugins.artplayerPluginVast.name,
|
||||
requests: window.vastSdk.state.players[0].requests,
|
||||
owned: window.vastSdk.state.players[0].args[2].parentNode === window.art.template.$player,
|
||||
video: window.vastSdk.state.players[0].args[1] === window.art.template.$video,
|
||||
}))
|
||||
expect(result).toEqual({ done: true, name: 'artplayerPluginVast', requests: [{ adTagUrl: '/test/ad-tag.xml' }, { adsResponse: '<VAST/>' }], owned: true, video: true })
|
||||
})
|
||||
|
||||
test(`${label}: SDK rejection stays observable and core can still play content`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, implementation, testInfo, true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const failure = new Error('controlled SDK failure')
|
||||
let calls = 0
|
||||
const registration = window.art.plugins.add(window.artplayerPluginVast(() => calls++))
|
||||
const observed = registration.catch(error => error === failure)
|
||||
window.vastSdk.rejectLoad(failure)
|
||||
return { sameFailure: await observed, calls, players: window.vastSdk.state.players.length, registered: Object.hasOwn(window.art.plugins, 'artplayerPluginVast') }
|
||||
})
|
||||
expect(result).toEqual({ sameFailure: true, calls: 0, players: 0, registered: false })
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
})
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.2)
|
||||
expect(await page.evaluate(() => window.art.template.$video.videoWidth)).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
if (!implementation.published) {
|
||||
test(`${label}: real overlay follows SDK events and explicit cleanup supports inactive recreation`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, implementation, testInfo)
|
||||
await page.evaluate(async () => {
|
||||
await window.art.plugins.add(window.artplayerPluginVast((context) => {
|
||||
window.vastContext = context
|
||||
}))
|
||||
window.vastContext.playUrl('/first.xml')
|
||||
})
|
||||
const container = page.locator('[id^="art-vast-"]')
|
||||
await expect(container).toHaveCount(1)
|
||||
await expect(container).toBeHidden()
|
||||
await page.evaluate(() => window.vastContext.imaPlayer.emit('AdStarted'))
|
||||
await expect(container).toBeVisible()
|
||||
await page.evaluate(() => {
|
||||
window.vastContext.playUrl('/suppressed.xml')
|
||||
window.vastContext.imaPlayer.emit('AdError', { code: 301 })
|
||||
})
|
||||
await expect(container).toBeHidden()
|
||||
expect(await page.evaluate(() => window.vastContext.imaPlayer.requests.length)).toBe(1)
|
||||
await page.evaluate(() => {
|
||||
window.vastContext.playRes('<VAST/>')
|
||||
window.vastContext.imaPlayer.emit('AdContentPauseRequested')
|
||||
})
|
||||
await expect(container).toBeVisible()
|
||||
await page.evaluate(() => window.vastContext.imaPlayer.emit('AdContentResumeRequested'))
|
||||
await expect(container).toBeHidden()
|
||||
await page.evaluate(() => {
|
||||
window.art.plugins.artplayerPluginVast.destroy()
|
||||
window.art.plugins.artplayerPluginVast.destroy()
|
||||
})
|
||||
await expect(container).toHaveCount(0)
|
||||
expect(await page.evaluate(() => window.vastSdk.state.players[0].destroyCalls)).toBe(1)
|
||||
await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4?source=next'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.currentSrc)).toContain('?source=next')
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.videoWidth)).toBeGreaterThan(0)
|
||||
await page.evaluate(() => window.vastContext.playUrl('/new-session.xml'))
|
||||
await expect(container).toHaveCount(1)
|
||||
expect(await page.evaluate(() => window.vastSdk.state.players[1].requests[0].adTagUrl)).toBe('/new-session.xml')
|
||||
expect(await page.evaluate(() => window.vastSdk.state.players[1].args[1] === window.art.template.$video)).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
if (implementation.historical) {
|
||||
test(`${label} historical: late SDK after destroy allocates into the detached player`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, implementation, testInfo, true)
|
||||
await page.evaluate(() => {
|
||||
window.vastCalls = 0
|
||||
window.vastRegistration = window.art.plugins.add(window.artplayerPluginVast((context) => {
|
||||
window.vastCalls++
|
||||
context.playUrl('/late.xml')
|
||||
}))
|
||||
window.art.destroy()
|
||||
window.vastSdk.resolveLoad()
|
||||
})
|
||||
await page.evaluate(() => window.vastRegistration)
|
||||
expect(await page.evaluate(() => ({
|
||||
calls: window.vastCalls,
|
||||
players: window.vastSdk.state.players.length,
|
||||
destroys: window.vastSdk.state.players[0].destroyCalls,
|
||||
connected: window.vastSdk.state.players[0].args[2].isConnected,
|
||||
url: window.vastSdk.state.players[0].requests[0].adTagUrl,
|
||||
}))).toEqual({ calls: 1, players: 1, destroys: 0, connected: false, url: '/late.xml' })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- The external SDK boundary is explicitly controlled.
|
||||
import test from 'node:test'
|
||||
import { deferred, vastEnvironment, vastImplementations } from './helpers/vast.js'
|
||||
|
||||
const implementations = await vastImplementations()
|
||||
|
||||
for (const implementation of implementations) {
|
||||
const { name, published } = implementation
|
||||
test(`${name}: SDK resolution precedes callback and result waits for callback completion`, async () => {
|
||||
const env = vastEnvironment(implementation, { deferLoad: true })
|
||||
const host = env.host()
|
||||
const started = deferred()
|
||||
const finish = deferred()
|
||||
let calls = 0
|
||||
let settled = false
|
||||
const attaching = env.factory(async (context) => {
|
||||
calls++
|
||||
started.resolve(context)
|
||||
await finish.promise
|
||||
})(host.art)
|
||||
attaching.then(() => {
|
||||
settled = true
|
||||
})
|
||||
assert.equal(env.state.loads, 1)
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(host.parent.children.length, 0)
|
||||
env.resolveLoad()
|
||||
const context = await started.promise
|
||||
assert.equal(context.art, host.art)
|
||||
assert.equal(context.ima, env.ima)
|
||||
assert.equal(settled, false)
|
||||
finish.resolve()
|
||||
assert.equal((await attaching).name, 'artplayerPluginVast')
|
||||
assert.equal(calls, 1)
|
||||
})
|
||||
|
||||
test(`${name}: SDK rejection propagates unchanged and does not invoke callback or allocate DOM`, async () => {
|
||||
const env = vastEnvironment(implementation, { deferLoad: true })
|
||||
const host = env.host()
|
||||
let calls = 0
|
||||
const attaching = env.factory(() => calls++)(host.art)
|
||||
const failure = new Error('SDK unavailable')
|
||||
const rejected = assert.rejects(attaching, error => error === failure)
|
||||
env.rejectLoad(failure)
|
||||
await rejected
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(env.state.players.length, 0)
|
||||
assert.equal(host.parent.children.length, 0)
|
||||
})
|
||||
|
||||
test(`${name}: absent callback remains accepted at runtime`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const result = await env.factory()(env.host().art)
|
||||
assert.equal(result.name, 'artplayerPluginVast')
|
||||
})
|
||||
|
||||
test(`${name}: playUrl and playRes preserve request content and return void`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
let context
|
||||
await env.factory((value) => {
|
||||
context = value
|
||||
})(env.host().art)
|
||||
assert.equal(context.playUrl('https://example.test/ad.xml'), undefined)
|
||||
assert.equal(context.playRes('<VAST version="3.0"/>'), undefined)
|
||||
const [player] = env.state.players
|
||||
assert.equal(player.requests[0].adTagUrl, 'https://example.test/ad.xml')
|
||||
assert.equal(player.requests[1].adsResponse, '<VAST version="3.0"/>')
|
||||
assert.equal(player.requests[0] instanceof env.ima.AdsRequest, true)
|
||||
assert.notEqual(player.requests[0], player.requests[1])
|
||||
})
|
||||
|
||||
test(`${name}: one attach factory creates independent contexts, players and requests`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const contexts = []
|
||||
const attach = env.factory(context => contexts.push(context))
|
||||
const one = env.host()
|
||||
const two = env.host()
|
||||
await Promise.all([attach(one.art), attach(two.art)])
|
||||
contexts[0].playUrl('/first.xml')
|
||||
contexts[1].playRes('<VAST/>')
|
||||
assert.notEqual(contexts[0], contexts[1])
|
||||
assert.equal(env.state.players.length, 2)
|
||||
assert.equal(env.state.players[0].args[1], one.art.template.$video)
|
||||
assert.equal(env.state.players[1].args[1], two.art.template.$video)
|
||||
assert.equal(env.state.players[0].requests.length, 1)
|
||||
assert.equal(env.state.players[1].requests.length, 1)
|
||||
})
|
||||
|
||||
test(`${name}: playAds exceptions remain synchronous and are not converted to silent success`, async () => {
|
||||
const failure = new Error('request failure')
|
||||
const env = vastEnvironment(implementation, { playError: failure })
|
||||
let context
|
||||
await env.factory((value) => {
|
||||
context = value
|
||||
})(env.host().art)
|
||||
assert.throws(() => context.playUrl('/ad.xml'), error => error === failure)
|
||||
env.state.playError = null
|
||||
context.playUrl('/retry.xml')
|
||||
assert.equal(env.state.players[0].requests[0].adTagUrl, '/retry.xml')
|
||||
})
|
||||
|
||||
test(`${name}: callback throws and rejected promises propagate their original value`, async () => {
|
||||
for (const asynchronous of [false, true]) {
|
||||
const env = vastEnvironment(implementation)
|
||||
const failure = new Error('callback failure')
|
||||
const attaching = env.factory(() => {
|
||||
if (asynchronous)
|
||||
return Promise.reject(failure)
|
||||
throw failure
|
||||
})(env.host().art)
|
||||
await assert.rejects(attaching, error => error === failure)
|
||||
}
|
||||
})
|
||||
|
||||
if (published)
|
||||
continue
|
||||
|
||||
test(`${name}: lazy init is idempotent and settings identities reach the Player constructor`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const host = env.host()
|
||||
let context
|
||||
await env.factory((value) => {
|
||||
context = value
|
||||
})(host.art)
|
||||
assert.equal(context.imaPlayer, null)
|
||||
assert.equal(context.container, null)
|
||||
context.playerOptions.custom = 'caller option'
|
||||
context.adsRenderingSettings.enablePreloading = false
|
||||
const player = context.init()
|
||||
assert.equal(context.init(), player)
|
||||
assert.equal(context.imaPlayer, player)
|
||||
assert.equal(context.container, player.args[2])
|
||||
assert.equal(player.args[3], context.adsRenderingSettings)
|
||||
assert.equal(player.args[4], context.playerOptions)
|
||||
assert.equal(player.args[3].enablePreloading, false)
|
||||
assert.equal(host.parent.children.length, 1)
|
||||
})
|
||||
|
||||
test(`${name}: request config is read live with own and inherited fields and primary overrides`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
let context
|
||||
await env.factory((value) => {
|
||||
context = value
|
||||
})(env.host().art)
|
||||
const config = Object.assign(Object.create({ inherited: 'legacy field' }), { adTagUrl: '/override.xml', adsResponse: 'override response' })
|
||||
context.playUrl('/ignored.xml', config)
|
||||
config.adTagUrl = '/changed.xml'
|
||||
context.playRes('ignored response', config)
|
||||
const [player] = env.state.players
|
||||
assert.equal(player.requests[0].adTagUrl, '/override.xml')
|
||||
assert.equal(player.requests[1].adTagUrl, '/changed.xml')
|
||||
assert.equal(player.requests[1].adsResponse, 'override response')
|
||||
assert.equal(player.requests[0].inherited, 'legacy field')
|
||||
})
|
||||
|
||||
test(`${name}: four SDK events update visibility and duplicate ad requests are suppressed only while active`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
let context
|
||||
await env.factory((value) => {
|
||||
context = value
|
||||
})(env.host().art)
|
||||
context.playUrl('/first.xml')
|
||||
const player = context.imaPlayer
|
||||
assert.equal(context.container.style.display, 'none')
|
||||
player.emit('AdContentPauseRequested')
|
||||
assert.equal(context.container.style.display, 'block')
|
||||
context.playUrl('/suppressed.xml')
|
||||
context.playRes('suppressed')
|
||||
assert.equal(player.requests.length, 1)
|
||||
player.emit('AdContentResumeRequested')
|
||||
assert.equal(context.container.style.display, 'none')
|
||||
context.playUrl('/second.xml')
|
||||
player.emit('AdStarted')
|
||||
assert.equal(context.container.style.display, 'block')
|
||||
const detail = { errorCode: 301 }
|
||||
player.emit('AdError', detail)
|
||||
assert.equal(context.container.style.display, 'none')
|
||||
assert.equal(env.state.errors[0][1], detail)
|
||||
context.playUrl('/after-error.xml')
|
||||
assert.equal(player.requests.length, 3)
|
||||
})
|
||||
|
||||
test(`${name}: explicit destroy is repeatable and permits a new inactive ad session`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const host = env.host()
|
||||
let context
|
||||
const result = await env.factory((value) => {
|
||||
context = value
|
||||
})(host.art)
|
||||
const first = context.init()
|
||||
assert.equal(result.destroy(), undefined)
|
||||
result.destroy()
|
||||
assert.equal(first.destroyCalls, 1)
|
||||
assert.equal(context.imaPlayer, null)
|
||||
assert.equal(context.container, null)
|
||||
assert.equal(host.parent.children.length, 0)
|
||||
context.playUrl('/next.xml')
|
||||
assert.equal(env.state.players.length, 2)
|
||||
assert.equal(context.imaPlayer.requests[0].adTagUrl, '/next.xml')
|
||||
})
|
||||
}
|
||||
|
||||
// Historical observations, never candidate acceptance requirements.
|
||||
for (const implementation of implementations.filter(item => item.historical)) {
|
||||
const { name, published } = implementation
|
||||
const initialize = context => context.init?.() || context.imaPlayer
|
||||
test(`${name} historical: core destroy leaves the SDK allocated`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const host = env.host()
|
||||
await env.factory(context => initialize(context))(host.art)
|
||||
const [player] = env.state.players
|
||||
host.art.destroy()
|
||||
assert.equal(player.destroyCalls, 0)
|
||||
assert.equal(host.parent.children.length, 1)
|
||||
})
|
||||
|
||||
test(`${name} historical: SDK resolution after core destruction still invokes callback and allocates`, async () => {
|
||||
const env = vastEnvironment(implementation, { deferLoad: true })
|
||||
const host = env.host()
|
||||
let calls = 0
|
||||
const attaching = env.factory((context) => {
|
||||
calls++
|
||||
initialize(context)
|
||||
})(host.art)
|
||||
host.art.destroy()
|
||||
env.resolveLoad()
|
||||
await attaching
|
||||
assert.equal(calls, 1)
|
||||
assert.equal(env.state.players.length, 1)
|
||||
assert.equal(env.state.players[0].destroyCalls, 0)
|
||||
})
|
||||
|
||||
test(`${name} historical: rejected callback leaks its already allocated SDK/container`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const host = env.host()
|
||||
const failure = new Error('after allocation')
|
||||
await assert.rejects(env.factory((context) => {
|
||||
initialize(context)
|
||||
return Promise.reject(failure)
|
||||
})(host.art), error => error === failure)
|
||||
assert.equal(env.state.players[0].destroyCalls, 0)
|
||||
assert.equal(host.parent.children.length, 1)
|
||||
})
|
||||
|
||||
test(`${name} historical: callback resumed after core destroy creates or reuses stale resources`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const host = env.host()
|
||||
const started = deferred()
|
||||
const finish = deferred()
|
||||
const attaching = env.factory(async (context) => {
|
||||
started.resolve()
|
||||
await finish.promise
|
||||
context.playUrl('/late.xml')
|
||||
})(host.art)
|
||||
await started.promise
|
||||
host.art.destroy()
|
||||
finish.resolve()
|
||||
await attaching
|
||||
assert.equal(env.state.players[0].requests[0].adTagUrl, '/late.xml')
|
||||
assert.equal(env.state.players[0].destroyCalls, 0)
|
||||
})
|
||||
|
||||
test(`${name} historical: SDK constructor failure leaves an attached container`, async () => {
|
||||
const failure = new Error('constructor failure')
|
||||
const env = vastEnvironment(implementation, { constructError: failure })
|
||||
const host = env.host()
|
||||
await assert.rejects(env.factory(context => initialize(context))(host.art), error => error === failure)
|
||||
assert.equal(host.parent.children.length, 1)
|
||||
assert.equal(env.state.players.length, 0)
|
||||
})
|
||||
|
||||
test(`${name} historical: simultaneous instances generate duplicate container IDs`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
const one = env.host()
|
||||
const two = env.host()
|
||||
const attach = env.factory(context => initialize(context))
|
||||
await Promise.all([attach(one.art), attach(two.art)])
|
||||
assert.equal(one.parent.children[0].id, two.parent.children[0].id)
|
||||
assert.notEqual(one.parent.children[0], two.parent.children[0])
|
||||
})
|
||||
|
||||
if (published)
|
||||
continue
|
||||
|
||||
test(`${name} historical: active-ad destroy keeps the request suppression flag stuck`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
let context
|
||||
const result = await env.factory((value) => {
|
||||
context = value
|
||||
})(env.host().art)
|
||||
context.playUrl('/first.xml')
|
||||
context.imaPlayer.emit('AdStarted')
|
||||
result.destroy()
|
||||
context.playUrl('/second.xml')
|
||||
assert.equal(context.imaPlayer, null)
|
||||
assert.equal(env.state.players.length, 1)
|
||||
})
|
||||
|
||||
test(`${name} historical: stale SDK callbacks throw after explicit destroy and corrupt a replacement`, async () => {
|
||||
const env = vastEnvironment(implementation)
|
||||
let context
|
||||
const result = await env.factory((value) => {
|
||||
context = value
|
||||
})(env.host().art)
|
||||
const first = context.init()
|
||||
const lateStart = [...first.listeners.get('AdStarted')][0]
|
||||
result.destroy()
|
||||
assert.throws(() => lateStart({}), /null/)
|
||||
const replacement = context.init()
|
||||
lateStart({})
|
||||
assert.equal(context.container.style.display, 'block')
|
||||
context.playUrl('/blocked-by-stale-event.xml')
|
||||
assert.equal(replacement.requests.length, 0)
|
||||
})
|
||||
|
||||
test(`${name} historical: SDK destroy exception prevents DOM and reference cleanup`, async () => {
|
||||
const failure = new Error('destroy failure')
|
||||
const env = vastEnvironment(implementation, { destroyError: failure })
|
||||
const host = env.host()
|
||||
let context
|
||||
const result = await env.factory((value) => {
|
||||
context = value
|
||||
})(host.art)
|
||||
const player = context.init()
|
||||
assert.throws(() => result.destroy(), error => error === failure)
|
||||
assert.equal(context.imaPlayer, player)
|
||||
assert.equal(host.parent.children.length, 1)
|
||||
})
|
||||
|
||||
test(`${name} historical: event registration failure leaves a partial SDK that init treats as ready`, async () => {
|
||||
const failure = new Error('listener failure')
|
||||
const env = vastEnvironment(implementation, { listenError: failure })
|
||||
const host = env.host()
|
||||
let context
|
||||
await env.factory((value) => {
|
||||
context = value
|
||||
})(host.art)
|
||||
assert.throws(() => context.init(), error => error === failure)
|
||||
env.state.listenError = null
|
||||
const player = context.init()
|
||||
assert.equal(player.listeners.size, 0)
|
||||
assert.equal(host.parent.children.length, 1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user