mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
380 lines
15 KiB
JavaScript
380 lines
15 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
// eslint-disable-next-line test/no-import-node-test -- Controlled clock/host contracts use Node's runner.
|
|
import test from 'node:test'
|
|
import { adsEnvironment, adsImplementations } from './helpers/ads.js'
|
|
|
|
const implementations = await adsImplementations()
|
|
const skips = host => host.calls.filter(call => call[0] === 'emit' && call[1] === 'artplayerPluginAds:skip')
|
|
const clicks = host => host.calls.filter(call => call[0] === 'emit' && call[1] === 'artplayerPluginAds:click')
|
|
const contentPlays = host => host.calls.filter(call => call[0] === 'content.play')
|
|
|
|
for (const { name, factory } of implementations.filter(item => !item.historical)) {
|
|
test(`${name}: historical default and callable entry share one factory and synchronous contract`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
assert.equal(factory.default, factory)
|
|
const host = env.host()
|
|
const result = factory.default({ html: 'ad' })(host.art)
|
|
assert.equal(result.name, 'artplayerPluginAds')
|
|
assert.equal(result.skip(), undefined)
|
|
host.start()
|
|
assert.equal(host.node(), undefined)
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
}
|
|
|
|
for (const { name, factory } of implementations) {
|
|
test(`${name}: lazy synchronous factory starts once after ready and either playback event`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const result = factory({ html: '<b>ad</b>' })(host.art)
|
|
assert.deepEqual(Object.keys(result), ['name', 'skip', 'pause', 'play'])
|
|
assert.equal(result.name, 'artplayerPluginAds')
|
|
assert.equal(host.node(), undefined)
|
|
assert.equal(env.timers.size, 0)
|
|
host.art.emit('play')
|
|
assert.equal(host.node(), undefined)
|
|
host.art.emit('ready').emit('video:playing')
|
|
const node = host.node()
|
|
assert(node)
|
|
assert.equal(host.art.template.$ads, node)
|
|
assert.equal(host.node('html').innerHTML, '<b>ad</b>')
|
|
assert.equal(host.art.playing, false)
|
|
host.art.emit('play').emit('video:playing')
|
|
assert.equal(host.node(), node)
|
|
assert.equal(env.timers.size, 1)
|
|
assert.equal(host.calls.filter(call => call[0] === 'content.pause').length, 1)
|
|
})
|
|
|
|
test(`${name}: actual validator keeps numeric durations and complete shallow i18n`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
assert.throws(() => factory({ totalDuration: '10' })(env.host().art), /totalDuration/)
|
|
assert.throws(() => factory({ playDuration: '5' })(env.host().art), /playDuration/)
|
|
assert.throws(() => factory({ html: 1 })(env.host().art), /html/)
|
|
assert.throws(() => factory({ i18n: { close: 'close' } })(env.host().art), /i18n/)
|
|
const host = env.host()
|
|
factory()(host.art)
|
|
assert.equal(host.normalized.totalDuration, 10)
|
|
assert.equal(host.normalized.playDuration, 5)
|
|
assert.equal(host.normalized.muted, false)
|
|
assert.equal(host.normalized.html, '')
|
|
assert.equal(host.normalized.video, '')
|
|
})
|
|
|
|
test(`${name}: video wins over HTML and metadata starts media and countdown`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: '<b>fallback</b>', video: 'ad.mp4', muted: true })(host.art)
|
|
host.start()
|
|
assert.equal(host.node('html'), undefined)
|
|
assert.equal(host.node('video').tagName, 'VIDEO')
|
|
assert.equal(host.node('video').muted, true)
|
|
assert.equal(env.timers.size, 0)
|
|
host.fire('video', 'loadedmetadata')
|
|
assert.equal(env.timers.size, 1)
|
|
assert.equal(host.node('video').paused, false)
|
|
assert.equal(host.node('timer').style.display, 'flex')
|
|
assert.equal(host.node('control').style.display, 'flex')
|
|
assert.equal(host.node('loading').style.display, 'none')
|
|
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
|
|
})
|
|
|
|
test(`${name}: each active second counts down and total duration restores content`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad', playDuration: 2, totalDuration: 3 })(host.art)
|
|
host.start()
|
|
assert.equal(host.node('close').innerHTML, '2秒后可关闭广告')
|
|
assert.equal(host.node('countdown').innerHTML, '3秒')
|
|
env.tick(999)
|
|
assert.equal(host.node('countdown').innerHTML, '3秒')
|
|
env.tick(1)
|
|
assert.equal(host.node('close').innerHTML, '1秒后可关闭广告')
|
|
assert.equal(host.node('countdown').innerHTML, '2秒')
|
|
env.tick(1000)
|
|
assert.equal(host.node('close').innerHTML, '关闭广告')
|
|
assert.equal(contentPlays(host).length, 0)
|
|
env.tick(1000)
|
|
assert.equal(host.node('countdown').innerHTML, '0秒')
|
|
assert.equal(host.node().style.display, 'none')
|
|
assert.equal(skips(host).length, 1)
|
|
assert.equal(skips(host)[0][2], host.normalized)
|
|
assert.equal(contentPlays(host).length, 1)
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: pause/play control the countdown without changing ad video state`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ video: 'ad.mp4', totalDuration: 4 })(host.art)
|
|
host.start()
|
|
host.fire('video', 'loadedmetadata')
|
|
env.tick(1000)
|
|
assert.equal(plugin.pause(), undefined)
|
|
env.tick(5000)
|
|
assert.equal(host.node('countdown').innerHTML, '3秒')
|
|
assert.equal(host.node('video').paused, false)
|
|
assert.equal(plugin.play(), undefined)
|
|
env.tick(1000)
|
|
assert.equal(host.node('countdown').innerHTML, '2秒')
|
|
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
|
|
})
|
|
|
|
test(`${name}: button obeys positive threshold while public skip bypasses it`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad', playDuration: 2, totalDuration: 5 })(host.art)
|
|
host.start()
|
|
host.fire('close', 'click')
|
|
env.tick(1000)
|
|
host.fire('close', 'click')
|
|
assert.equal(skips(host).length, 0)
|
|
assert.equal(plugin.skip(), undefined)
|
|
assert.equal(skips(host).length, 1)
|
|
const second = env.host()
|
|
factory({ html: 'second', playDuration: 2, totalDuration: 5 })(second.art)
|
|
second.start()
|
|
env.tick(2000)
|
|
second.fire('close', 'click')
|
|
assert.equal(skips(second).length, 1)
|
|
})
|
|
|
|
test(`${name}: non-skippable button stays hidden and automatic completion still occurs`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad', playDuration: 2, totalDuration: 2 })(host.art)
|
|
host.start()
|
|
assert.equal(host.node('close').style.display, 'none')
|
|
env.tick(2000)
|
|
assert.equal(skips(host).length, 1)
|
|
})
|
|
|
|
test(`${name}: translations preserve first-placeholder substitution and normalized payload`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const i18n = { close: 'skip', countdown: '%s seconds %s', detail: 'visit', canBeClosed: 'wait %s' }
|
|
factory({ html: 'ad', playDuration: 1, totalDuration: 3, i18n })(host.art)
|
|
host.start()
|
|
assert.equal(host.node('countdown').innerHTML, '3 seconds %s')
|
|
assert.equal(host.node('close').innerHTML, 'wait 1')
|
|
env.tick(1000)
|
|
assert.equal(host.node('close').innerHTML, 'skip')
|
|
host.fire('close', 'click')
|
|
assert.equal(skips(host)[0][2].i18n, i18n)
|
|
})
|
|
|
|
test(`${name}: ad/detail clicks preserve URL and option payload; unlinked ad still emits`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: '<img src="poster.png">', url: 'https://example.invalid/ad' })(host.art)
|
|
host.start()
|
|
host.fire('html', 'click')
|
|
host.fire('detail', 'click')
|
|
assert.deepEqual(env.opened, ['https://example.invalid/ad', 'https://example.invalid/ad'])
|
|
assert.equal(clicks(host).length, 2)
|
|
assert(clicks(host).every(call => call[2] === host.normalized))
|
|
const second = env.host()
|
|
factory({ html: 'unlinked' })(second.art)
|
|
second.start()
|
|
assert.equal(second.node('detail').style.display, 'none')
|
|
second.fire('html', 'click')
|
|
assert.equal(clicks(second).length, 1)
|
|
assert.equal(env.opened.length, 2)
|
|
})
|
|
|
|
test(`${name}: mute applies only to the ad and toggles existing icons`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ video: 'ad.mp4', muted: true })(host.art)
|
|
host.start()
|
|
assert.equal(host.nodes.get('volume').style.display, 'none')
|
|
assert.equal(host.nodes.get('volumeClose').style.display, 'inline-flex')
|
|
host.fire('muted', 'click')
|
|
assert.equal(host.node('video').muted, false)
|
|
assert.equal(host.nodes.get('volume').style.display, 'inline-flex')
|
|
assert.equal(host.nodes.get('volumeClose').style.display, 'none')
|
|
host.fire('muted', 'click')
|
|
assert.equal(host.node('video').muted, true)
|
|
})
|
|
|
|
test(`${name}: document visibility pauses and resumes only the countdown`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad', totalDuration: 4 })(host.art)
|
|
host.start()
|
|
env.tick(1000)
|
|
host.visibility(true)
|
|
env.tick(5000)
|
|
assert.equal(host.node('countdown').innerHTML, '3秒')
|
|
host.visibility(false)
|
|
env.tick(1000)
|
|
assert.equal(host.node('countdown').innerHTML, '2秒')
|
|
})
|
|
|
|
test(`${name}: media error before metadata restores content and retains skip order`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ video: 'missing.mp4' })(host.art)
|
|
host.start()
|
|
host.calls.length = 0
|
|
host.fire('video', 'error')
|
|
assert.deepEqual(host.calls.map(call => call.slice(0, 2)), [
|
|
['content.play', false],
|
|
['emit', 'play'],
|
|
['ad.pause'],
|
|
['style', 'artplayer-plugin-ads'],
|
|
['emit', 'artplayerPluginAds:skip'],
|
|
])
|
|
assert.equal(host.node().style.display, 'none')
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: separate factories keep instance timers and event options independent`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const first = env.host()
|
|
const second = env.host()
|
|
const firstPlugin = factory({ html: 'first', totalDuration: 2 })(first.art)
|
|
factory({ html: 'second', totalDuration: 3 })(second.art)
|
|
first.start()
|
|
second.start()
|
|
firstPlugin.pause()
|
|
env.tick(3000)
|
|
assert.equal(skips(first).length, 0)
|
|
assert.equal(first.node('countdown').innerHTML, '2秒')
|
|
assert.equal(skips(second).length, 1)
|
|
assert.equal(skips(second)[0][2], second.normalized)
|
|
firstPlugin.play()
|
|
env.tick(2000)
|
|
assert.equal(skips(first).length, 1)
|
|
assert.equal(skips(first)[0][2], first.normalized)
|
|
})
|
|
|
|
test(`${name}: initial input is read at attach and completed preroll stays hidden after restart`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const option = { html: 'before', totalDuration: 1 }
|
|
const attach = factory(option)
|
|
option.html = 'at attach'
|
|
attach(host.art)
|
|
option.html = 'after attach'
|
|
host.start()
|
|
assert.equal(host.node('html').innerHTML, 'at attach')
|
|
env.tick(1000)
|
|
const root = host.node()
|
|
host.art.emit('restart', 'second.mp4').emit('ready').emit('play').emit('video:playing')
|
|
assert.equal(host.node(), root)
|
|
assert.equal(root.style.display, 'none')
|
|
assert.equal(skips(host).length, 1)
|
|
assert.equal(env.timers.size, 0)
|
|
})
|
|
|
|
test(`${name}: normalized event options remain live for subsequent countdown ticks`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad', totalDuration: 10 })(host.art)
|
|
host.art.on('artplayerPluginAds:click', (option) => {
|
|
option.totalDuration = 2
|
|
})
|
|
host.start()
|
|
host.fire('html', 'click')
|
|
env.tick(2000)
|
|
assert.equal(skips(host).length, 1)
|
|
assert.equal(host.node('countdown').innerHTML, '0秒')
|
|
})
|
|
}
|
|
|
|
// Freeze legacy defects separately; migrated candidates must not inherit these assertions.
|
|
for (const { name, factory } of implementations.filter(item => item.historical)) {
|
|
test(`${name} historical: duplicate play starts parallel countdown chains`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad', totalDuration: 10 })(host.art)
|
|
host.start()
|
|
plugin.play()
|
|
assert.equal(env.timers.size, 2)
|
|
env.tick(1000)
|
|
assert.equal(host.node('countdown').innerHTML, '8秒')
|
|
plugin.pause()
|
|
assert.equal(env.timers.size, 1)
|
|
})
|
|
|
|
test(`${name} historical: skip leaves a queued callback and emits completion again`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad', totalDuration: 1 })(host.art)
|
|
host.start()
|
|
plugin.skip()
|
|
assert.equal(env.timers.size, 1)
|
|
env.tick(1000)
|
|
assert.equal(skips(host).length, 2)
|
|
plugin.skip()
|
|
assert.equal(skips(host).length, 3)
|
|
})
|
|
|
|
test(`${name} historical: destroy leaves countdown intent that requests content play`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad', totalDuration: 1 })(host.art)
|
|
host.start()
|
|
host.art.destroy()
|
|
assert.equal(env.timers.size, 1)
|
|
env.tick(1000)
|
|
assert.deepEqual(contentPlays(host), [['content.play', true]])
|
|
})
|
|
|
|
test(`${name} historical: zero wait displays close immediately but rejects the first click`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
factory({ html: 'ad', playDuration: 0, totalDuration: 3 })(host.art)
|
|
host.start()
|
|
assert.equal(host.node('close').innerHTML, '关闭广告')
|
|
host.fire('close', 'click')
|
|
assert.equal(skips(host).length, 0)
|
|
env.tick(1000)
|
|
host.fire('close', 'click')
|
|
assert.equal(skips(host).length, 1)
|
|
})
|
|
|
|
test(`${name} historical: skip before initialization plays content then throws`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad' })(host.art)
|
|
assert.throws(() => plugin.skip(), /undefined/)
|
|
assert.equal(contentPlays(host).length, 1)
|
|
})
|
|
|
|
test(`${name} historical: reusing one factory changes the first instance event payload identity`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const attach = factory({ html: 'shared' })
|
|
const first = env.host()
|
|
const firstPlugin = attach(first.art)
|
|
first.start()
|
|
const firstOption = first.normalized
|
|
const second = env.host()
|
|
attach(second.art)
|
|
second.start()
|
|
firstPlugin.skip()
|
|
assert.notEqual(firstOption, second.normalized)
|
|
assert.equal(skips(first)[0][2], second.normalized)
|
|
})
|
|
|
|
test(`${name} historical: repeated metadata schedules and starts the ad twice`, (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, 2)
|
|
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 2)
|
|
})
|
|
|
|
test(`${name} historical: play before initialization schedules a callback against missing DOM`, (t) => {
|
|
const env = adsEnvironment(t)
|
|
const host = env.host()
|
|
const plugin = factory({ html: 'ad' })(host.art)
|
|
plugin.play()
|
|
assert.equal(env.timers.size, 1)
|
|
assert.throws(() => env.tick(1000), /null|undefined/)
|
|
})
|
|
}
|