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() test('VAST source default alias preserves deferred registration, requests and cleanup', async () => { const env = vastEnvironment(implementations.find(item => item.name === 'source'), { deferLoad: true }) const host = env.host() assert.equal(env.factory.default, env.factory) const gate = deferred() const started = deferred() const pending = env.factory.default(async (context) => { started.resolve(context) await gate.promise context.playUrl('/alias.xml') })(host.art) assert.equal(typeof pending.then, 'function') assert.equal(env.state.players.length, 0) env.resolveLoad() await started.promise gate.resolve() const result = await pending assert.equal(result.name, 'artplayerPluginVast') assert.equal(env.state.players[0].requests[0].adTagUrl, '/alias.xml') host.art.destroy() assert.equal(host.parent.children.length, 0) assert.equal(env.state.players[0].destroyCalls, 1) }) 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 }, implementation.options)(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++, implementation.options)(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(undefined, implementation.options)(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 }, implementation.options)(env.host().art) assert.equal(context.playUrl('https://example.test/ad.xml'), undefined) assert.equal(context.playRes(''), undefined) const [player] = env.state.players assert.equal(player.requests[0].adTagUrl, 'https://example.test/ad.xml') assert.equal(player.requests[1].adsResponse, '') 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), implementation.options) 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('') 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 }, implementation.options)(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 }, implementation.options)(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 }, implementation.options)(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 }, implementation.options)(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 }, implementation.options)(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 }, implementation.options)(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), implementation.options)(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) }, implementation.options)(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) }, implementation.options)(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') }, implementation.options)(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), implementation.options)(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), implementation.options) 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 }, implementation.options)(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 }, implementation.options)(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 }, implementation.options)(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 }, implementation.options)(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) }) }