mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { getEventListeners } from 'node:events'
|
|
// eslint-disable-next-line test/no-import-node-test -- Controlled lifecycle contracts use Node's runner.
|
|
import test from 'node:test'
|
|
import { setImmediate as settle } from 'node:timers/promises'
|
|
import { adsEnvironment, adsImplementations } from './helpers/ads.js'
|
|
|
|
const implementations = (await adsImplementations()).filter(item => !item.historical)
|
|
const skips = host => host.calls.filter(call => call[0] === 'emit' && call[1] === 'artplayerPluginAds:skip')
|
|
const contentPlays = host => host.calls.filter(call => call[0] === 'content.play')
|
|
|
|
for (const { name, factory } of implementations) {
|
|
test(`${name}: destroy before initialization preserves caller-owned undefined template property`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
Object.defineProperty(host.art.template, '$ads', { configurable: true, enumerable: true, writable: true, value: undefined })
|
|
const before = Object.getOwnPropertyDescriptor(host.art.template, '$ads')
|
|
factory({ html: 'ad' })(host.art)
|
|
host.art.destroy()
|
|
assert.deepEqual(Object.getOwnPropertyDescriptor(host.art.template, '$ads'), before)
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: repeated play owns one timer and pause clears all countdown work`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad', totalDuration: 3 })(host.art)
|
|
host.start()
|
|
plugin.play()
|
|
plugin.play()
|
|
assert.equal(env.timers.size, 1)
|
|
env.tick(1000)
|
|
assert.equal(host.node('countdown').innerHTML, '2秒')
|
|
plugin.pause()
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: skip clears timers and becomes inert under repeated methods/events`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad', totalDuration: 1 })(host.art)
|
|
host.start()
|
|
plugin.skip()
|
|
plugin.skip()
|
|
plugin.play()
|
|
host.visibility(false)
|
|
env.tick(5000)
|
|
assert.equal(env.timers.size, 0)
|
|
assert.equal(skips(host).length, 1)
|
|
assert.equal(contentPlays(host).length, 1)
|
|
})
|
|
|
|
test(`${name}: destroy cancels countdown and future public calls never resume content`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad', totalDuration: 1 })(host.art)
|
|
host.start()
|
|
host.art.destroy()
|
|
plugin.play()
|
|
plugin.skip()
|
|
env.tick(5000)
|
|
assert.equal(env.timers.size, 0)
|
|
assert.equal(contentPlays(host).length, 0)
|
|
assert.equal(skips(host).length, 0)
|
|
})
|
|
|
|
test(`${name}: one factory retains per-instance normalized event payloads`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const first = env.host()
|
|
const second = env.host()
|
|
const attach = factory({ html: 'shared' })
|
|
const one = attach(first.art)
|
|
first.start()
|
|
attach(second.art)
|
|
second.start()
|
|
one.skip()
|
|
assert.equal(skips(first)[0][2], first.normalized)
|
|
assert.notEqual(skips(first)[0][2], second.normalized)
|
|
})
|
|
|
|
test(`${name}: zero threshold close works before the first timer tick`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad', playDuration: 0 })(host.art)
|
|
host.start()
|
|
host.fire('close', 'click')
|
|
assert.equal(skips(host).length, 1)
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: play before initialization is inert and skip cancels pending preroll`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad' })(host.art)
|
|
plugin.play()
|
|
assert.equal(env.timers.size, 0)
|
|
plugin.skip()
|
|
host.start()
|
|
assert.equal(host.node(), undefined)
|
|
assert.equal(skips(host).length, 1)
|
|
assert.equal(contentPlays(host).length, 0)
|
|
})
|
|
|
|
test(`${name}: duplicate metadata starts the ad once`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ video: 'ad.mp4' })(host.art)
|
|
host.start()
|
|
host.fire('video', 'loadedmetadata')
|
|
host.fire('video', 'loadedmetadata')
|
|
assert.equal(env.timers.size, 1)
|
|
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
|
|
})
|
|
|
|
test(`${name}: core version numbers do not reject hosts providing the required capabilities`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
host.art.constructor.version = '4.5.5'
|
|
factory({ html: 'ad', totalDuration: 2 })(host.art)
|
|
host.start()
|
|
env.tick(1000)
|
|
host.visibility(true)
|
|
env.tick(5000)
|
|
assert.equal(host.node('countdown').innerHTML, '1秒')
|
|
host.visibility(false)
|
|
env.tick(1000)
|
|
assert.equal(skips(host).length, 1)
|
|
})
|
|
|
|
test(`${name}: synchronous content-play failure still hides ad and completes exactly once`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const warning = []
|
|
t.mock.method(console, 'warn', (...args) => warning.push(args))
|
|
const plugin = factory({ html: 'ad' })(host.art)
|
|
host.start()
|
|
const error = new Error('Content play failed')
|
|
host.art.play = () => {
|
|
throw error
|
|
}
|
|
assert.doesNotThrow(() => plugin.skip())
|
|
assert.equal(host.node().style.display, 'none')
|
|
assert.equal(skips(host).length, 1)
|
|
assert(warning.some(args => args.includes(error)))
|
|
})
|
|
|
|
test(`${name}: destroy during content restoration suppresses subsequent skip dispatch`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad' })(host.art)
|
|
host.start()
|
|
host.art.on('play', () => host.art.destroy())
|
|
plugin.skip()
|
|
assert.equal(skips(host).length, 0)
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: metadata emitted by the src setter is observed after listeners are installed`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const append = host.art.constructor.utils.append
|
|
host.art.constructor.utils.append = (...args) => {
|
|
const node = append(...args)
|
|
if (node.tagName === 'VIDEO') {
|
|
let source
|
|
Object.defineProperty(node, 'src', {
|
|
configurable: true,
|
|
get: () => source,
|
|
set(value) {
|
|
source = value
|
|
node.dispatchEvent(new Event('loadedmetadata'))
|
|
},
|
|
})
|
|
}
|
|
return node
|
|
}
|
|
factory({ video: 'cached.mp4' })(host.art)
|
|
host.start()
|
|
assert.equal(env.timers.size, 1)
|
|
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
|
|
})
|
|
|
|
test(`${name}: rejected ad play restores content once and observes the original error`, async (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const warnings = []
|
|
t.mock.method(console, 'warn', (...args) => warnings.push(args))
|
|
factory({ video: 'ad.mp4' })(host.art)
|
|
host.start()
|
|
const error = new Error('Ad play rejected')
|
|
host.node('video').play = () => Promise.reject(error)
|
|
host.fire('video', 'loadedmetadata')
|
|
await settle()
|
|
assert.equal(skips(host).length, 1)
|
|
assert.equal(contentPlays(host).length, 1)
|
|
assert.equal(env.timers.size, 0)
|
|
assert(warnings.some(args => args.includes(error)))
|
|
})
|
|
|
|
test(`${name}: late ad play fulfillment after destroy pauses owned media without restoring content`, async (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ video: 'ad.mp4' })(host.art)
|
|
host.start()
|
|
let resolve
|
|
const video = host.node('video')
|
|
video.play = () => new Promise((done) => {
|
|
resolve = done
|
|
})
|
|
host.fire('video', 'loadedmetadata')
|
|
host.art.destroy()
|
|
const pauses = host.calls.filter(call => call[0] === 'ad.pause').length
|
|
video.paused = false
|
|
resolve()
|
|
await settle()
|
|
assert.equal(video.paused, true)
|
|
assert.equal(host.calls.filter(call => call[0] === 'ad.pause').length, pauses + 1)
|
|
assert.equal(contentPlays(host).length, 0)
|
|
assert.equal(video.src, undefined)
|
|
assert.equal(host.art.template.$ads, undefined)
|
|
})
|
|
|
|
test(`${name}: destroy releases own DOM/document listeners and leaves foreign listeners intact`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ video: 'ad.mp4' })(host.art)
|
|
host.start()
|
|
const video = host.node('video')
|
|
const foreign = () => {}
|
|
video.addEventListener('loadedmetadata', foreign)
|
|
assert.equal(getEventListeners(video, 'loadedmetadata').length, 2)
|
|
assert.equal(getEventListeners(env.document, 'visibilitychange').length, 1)
|
|
host.art.destroy()
|
|
assert.deepEqual(getEventListeners(video, 'loadedmetadata'), [foreign])
|
|
assert.equal(getEventListeners(video, 'error').length, 0)
|
|
assert.equal(getEventListeners(env.document, 'visibilitychange').length, 0)
|
|
assert.equal(host.node().parentNode, null)
|
|
assert.equal(host.art.template.$ads, undefined)
|
|
host.fire('video', 'loadedmetadata')
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: partial view failure removes owned root and subscriptions before propagating`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const append = host.art.constructor.utils.append
|
|
const error = new Error('View failed')
|
|
host.art.constructor.utils.append = (...args) => {
|
|
if (host.node('loading'))
|
|
throw error
|
|
return append(...args)
|
|
}
|
|
factory({ html: 'ad' })(host.art)
|
|
assert.throws(() => host.start(), candidate => candidate === error)
|
|
assert.equal(host.node().parentNode, null)
|
|
assert.equal(host.art.template.$ads, undefined)
|
|
assert.equal([...host.listeners.values()].flatMap(set => [...set]).length, 0)
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: synchronous destroy during root creation leaves no newly appended overlay`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const append = host.art.constructor.utils.append
|
|
host.art.constructor.utils.append = (...args) => {
|
|
const node = append(...args)
|
|
if (node.className === 'artplayer-plugin-ads')
|
|
host.art.destroy()
|
|
return node
|
|
}
|
|
factory({ html: 'ad' })(host.art)
|
|
host.start()
|
|
assert.equal(host.node().parentNode, null)
|
|
assert.equal(host.art.template.$ads, undefined)
|
|
assert.equal(env.timers.size, 0)
|
|
assert.equal(host.calls.filter(call => call[0] === 'content.pause').length, 0)
|
|
})
|
|
|
|
test(`${name}: fullscreen icons follow initial and externally changed state`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
host.art.fullscreen = true
|
|
factory({ html: 'ad' })(host.art)
|
|
host.start()
|
|
assert.equal(host.nodes.get('fullscreenOn').style.display, 'none')
|
|
assert.equal(host.nodes.get('fullscreenOff').style.display, 'inline-flex')
|
|
host.art.fullscreen = false
|
|
host.art.emit('fullscreen', false)
|
|
assert.equal(host.nodes.get('fullscreenOn').style.display, 'inline-flex')
|
|
assert.equal(host.nodes.get('fullscreenOff').style.display, 'none')
|
|
})
|
|
|
|
test(`${name}: destroy during ad pause prevents late UI writes and skip notification`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ video: 'ad.mp4' })(host.art)
|
|
host.start()
|
|
const video = host.node('video')
|
|
let once = false
|
|
video.pause = () => {
|
|
if (!once) {
|
|
once = true
|
|
host.art.destroy()
|
|
}
|
|
}
|
|
plugin.skip()
|
|
assert.equal(skips(host).length, 0)
|
|
assert.equal(host.node().style.display, undefined)
|
|
})
|
|
|
|
test(`${name}: destroy preserves a caller replacement of template.$ads`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad' })(host.art)
|
|
host.start()
|
|
const foreign = {}
|
|
host.art.template.$ads = foreign
|
|
host.art.destroy()
|
|
assert.equal(host.art.template.$ads, foreign)
|
|
assert.equal(host.node().parentNode, null)
|
|
})
|
|
|
|
test(`${name}: rejected play after destroy is observed without restoring or reporting cancellation`, async (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const warnings = []
|
|
t.mock.method(console, 'warn', (...args) => warnings.push(args))
|
|
factory({ video: 'ad.mp4' })(host.art)
|
|
host.start()
|
|
let reject
|
|
host.node('video').play = () => new Promise((_, fail) => {
|
|
reject = fail
|
|
})
|
|
host.fire('video', 'loadedmetadata')
|
|
host.art.destroy()
|
|
reject(new Error('Cancelled by destruction'))
|
|
await settle()
|
|
assert.equal(warnings.length, 0)
|
|
assert.equal(contentPlays(host).length, 0)
|
|
assert.equal(skips(host).length, 0)
|
|
})
|
|
}
|