mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 01:26:16 -08:00
refactor(ads): [PKG-ADS-03] split TypeScript lifecycle and preserve legacy behavior
This commit is contained in:
1 parent
069ad4aae1
commit
39e2d4cdfb
37 files changed
+3083
-788
No files matched your search
@@ -7,6 +7,9 @@ Ads bundle against controlled clock/host contracts. It is included in `test:unit
|
||||
The helper uses the real option validator but does not simulate media decoding or layout.
|
||||
Historical defect assertions apply only to frozen implementations. See
|
||||
[Ads validation](../refactor/ads-validation.md) for artifact overrides and browser scope.
|
||||
`ads-lifecycle.test.js` adds candidate-only resource, reentry, early-method and Promise
|
||||
regressions. Async observations drain one event-loop turn, including VM Promise assimilation,
|
||||
rather than assuming a fixed number of microtasks. The published defect observations stay intact.
|
||||
|
||||
| Command | Scope |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
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}: 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)
|
||||
})
|
||||
}
|
||||
@@ -252,6 +252,20 @@ for (const { name, factory } of implementations) {
|
||||
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.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Browser regression entry
|
||||
|
||||
`yarn test:browser test/browser/ads.spec.js` tests actual Ads 1.0.6 and candidate code
|
||||
with published 5.4.1 and candidate cores using local image/video media. It covers actual
|
||||
with published 4.5.5/5.4.1 and candidate cores using local image/video media. It covers actual
|
||||
decoded pixels, countdown, skipping, content restoration, 404 and source replacement.
|
||||
Published-only rejection cases inject an exact play failure; they do not certify device
|
||||
autoplay policies. See [Ads validation](../../refactor/ads-validation.md).
|
||||
Candidate lifecycle cases check early/duplicate skip, real media release after destroy and
|
||||
observed internal play rejections. The server exposes the verified 4.5.5 bundle separately
|
||||
and records its fingerprint; the fixture uses the shared template.$video surface.
|
||||
|
||||
Audio Track tests use the source and frozen 1.1.0 plugin on both cores. The local AAC fixture
|
||||
has a pinned hash and generation command in media/audio-tone.json and uses the existing Range
|
||||
|
||||
@@ -26,8 +26,9 @@ test.afterEach(async ({ page }, testInfo) => {
|
||||
return {
|
||||
events: window.adsEvents,
|
||||
rejected: window.adsRejected,
|
||||
warnings: window.adsWarnings,
|
||||
ad: ad && { time: ad.currentTime, paused: ad.paused, width: ad.videoWidth, height: ad.videoHeight, error: ad.error?.code },
|
||||
content: window.art && { time: window.art.currentTime, paused: window.art.video.paused, destroyed: window.art.isDestroy },
|
||||
content: window.art && { time: window.art.currentTime, paused: window.art.template.$video.paused, destroyed: window.art.isDestroy },
|
||||
}
|
||||
})
|
||||
await testInfo.attach('ads-state', { contentType: 'application/json', body: JSON.stringify(state) })
|
||||
@@ -40,7 +41,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
||||
})
|
||||
})
|
||||
|
||||
async function openAds(page, core, plugin, option, testInfo) {
|
||||
async function openAds(page, core, plugin, option, testInfo, start = true) {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
const code = plugin === 'published' ? published : candidate
|
||||
await page.addScriptTag({ content: code })
|
||||
@@ -59,10 +60,96 @@ async function openAds(page, core, plugin, option, testInfo) {
|
||||
}, option)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(0)
|
||||
await page.locator('#play').click()
|
||||
if (start)
|
||||
await page.locator('#play').click()
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const core of ['published-4.5.5', 'published', 'candidate']) {
|
||||
test(`${core} core / candidate Ads: skip before play suppresses preroll without autoplay`, async ({ page }, testInfo) => {
|
||||
await openAds(page, core, 'candidate', { html: 'pending ad' }, testInfo, false)
|
||||
await page.evaluate(() => window.art.plugins.artplayerPluginAds.skip())
|
||||
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test(`${core} core / candidate Ads: immediate zero-threshold skip completes only once`, async ({ page }, testInfo) => {
|
||||
await openAds(page, core, 'candidate', { html: 'ad', playDuration: 0, totalDuration: 1 }, testInfo)
|
||||
await page.locator('.artplayer-plugin-ads-close').click()
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
await page.evaluate(() => {
|
||||
window.art.plugins.artplayerPluginAds.skip()
|
||||
window.art.plugins.artplayerPluginAds.play()
|
||||
})
|
||||
await page.waitForTimeout(1200)
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
})
|
||||
|
||||
test(`${core} core / candidate Ads: destroy releases actual ad media and prevents delayed playback`, async ({ page }, testInfo) => {
|
||||
await page.addInitScript(() => {
|
||||
window.adsPlayCalls = []
|
||||
const play = HTMLMediaElement.prototype.play
|
||||
HTMLMediaElement.prototype.play = function () {
|
||||
window.adsPlayCalls.push(this.className)
|
||||
return play.call(this)
|
||||
}
|
||||
})
|
||||
await openAds(page, core, 'candidate', { video: '/test/pattern.mp4', muted: true, totalDuration: 2 }, testInfo)
|
||||
await expect.poll(() => page.locator('.artplayer-plugin-ads-video').evaluate(video => video.currentTime)).toBeGreaterThan(0.2)
|
||||
const before = await page.evaluate(() => {
|
||||
window.retainedAd = document.querySelector('.artplayer-plugin-ads-video')
|
||||
window.art.destroy(false)
|
||||
window.retainedAd.dispatchEvent(new Event('loadedmetadata'))
|
||||
window.retainedAd.dispatchEvent(new Event('error'))
|
||||
return window.adsPlayCalls.length
|
||||
})
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(0)
|
||||
await page.waitForTimeout(2200)
|
||||
expect(await page.evaluate(() => ({
|
||||
paused: window.retainedAd.paused,
|
||||
src: window.retainedAd.getAttribute('src'),
|
||||
calls: window.adsPlayCalls.length,
|
||||
skips: window.adsEvents.filter(event => event.name === 'skip').length,
|
||||
hasTemplate: Boolean(window.art.template.$ads),
|
||||
}))).toEqual({ paused: true, src: null, calls: before, skips: 0, hasTemplate: false })
|
||||
})
|
||||
|
||||
for (const target of ['ad', 'content']) {
|
||||
test(`${core} core / candidate Ads: observes ${target} play rejection without an unhandled error`, async ({ page }, testInfo) => {
|
||||
await page.addInitScript((target) => {
|
||||
window.adsWarnings = []
|
||||
const warn = console.warn
|
||||
console.warn = function (...args) {
|
||||
window.adsWarnings.push(args.map(value => value?.message ? { name: value.name, message: value.message } : value))
|
||||
return warn.apply(this, args)
|
||||
}
|
||||
const play = HTMLMediaElement.prototype.play
|
||||
HTMLMediaElement.prototype.play = function () {
|
||||
const selected = target === 'ad'
|
||||
? this.classList.contains('artplayer-plugin-ads-video')
|
||||
: this === window.art?.template.$video && document.querySelector('.artplayer-plugin-ads')
|
||||
if (selected)
|
||||
return Promise.reject(new DOMException(`Ads candidate ${target} rejection`, 'NotAllowedError'))
|
||||
return play.call(this)
|
||||
}
|
||||
}, target)
|
||||
const option = target === 'ad' ? { video: '/test/pattern.mp4', muted: true, totalDuration: 10 } : { html: 'ad', totalDuration: 1 }
|
||||
await openAds(page, core, 'candidate', option, testInfo)
|
||||
await expect.poll(() => page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
await expect.poll(() => page.evaluate(() => window.adsWarnings)).toEqual([
|
||||
['Artplayer Ads:', { name: 'NotAllowedError', message: `Ads candidate ${target} rejection` }],
|
||||
])
|
||||
if (target === 'ad')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
else
|
||||
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
for (const plugin of ['published', 'candidate']) {
|
||||
const label = `${core} core / ${plugin} Ads`
|
||||
|
||||
@@ -71,7 +158,7 @@ for (const core of ['published', 'candidate']) {
|
||||
const root = page.locator('.artplayer-plugin-ads')
|
||||
await expect(root).toBeVisible()
|
||||
await expect.poll(() => page.locator('img[alt="Ad artwork"]').evaluate(image => image.complete && image.naturalWidth > 0)).toBe(true)
|
||||
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
|
||||
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
|
||||
await page.locator('.artplayer-plugin-ads-close').click()
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(0)
|
||||
await expect(page.locator('.artplayer-plugin-ads-close')).toHaveText('关闭广告')
|
||||
@@ -183,7 +270,7 @@ for (const core of ['published', 'candidate']) {
|
||||
})
|
||||
const play = HTMLMediaElement.prototype.play
|
||||
HTMLMediaElement.prototype.play = function () {
|
||||
if (this === window.art?.video && document.querySelector('.artplayer-plugin-ads'))
|
||||
if (this === window.art?.template.$video && document.querySelector('.artplayer-plugin-ads'))
|
||||
return Promise.reject(new DOMException('Ads test controlled content rejection', 'NotAllowedError'))
|
||||
return play.call(this)
|
||||
}
|
||||
@@ -191,7 +278,7 @@ for (const core of ['published', 'candidate']) {
|
||||
await openAds(page, core, 'published', { html: 'ad', totalDuration: 1 }, testInfo)
|
||||
await expect.poll(() => page.evaluate(() => window.adsRejected)).toEqual([{ name: 'NotAllowedError', message: 'Ads test controlled content rejection' }])
|
||||
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
|
||||
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
|
||||
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
|
||||
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
|
||||
})
|
||||
|
||||
|
||||
@@ -59,6 +59,11 @@ for (const extension of ['mp4', 'webm']) {
|
||||
const name = `test/browser/media/pattern.${extension}`
|
||||
add(`/test/pattern.${extension}`, fs.readFileSync(path.join(workspace, name)), { kind: 'generated-media', file: name })
|
||||
}
|
||||
const adsCore = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/ads-core.json'), 'utf8')).release
|
||||
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 })
|
||||
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' }
|
||||
@@ -123,7 +128,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'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
|
||||
assert(['candidate', 'published', 'published-4.5.5'].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)
|
||||
|
||||
@@ -109,11 +109,28 @@ export function adsEnvironment(t) {
|
||||
calls.push(['ad.pause'])
|
||||
this.paused = true
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.parentNode)
|
||||
this.parentNode.children = this.parentNode.children.filter(child => child !== this)
|
||||
this.parentNode = null
|
||||
calls.push(['remove', this.className])
|
||||
}
|
||||
|
||||
removeAttribute(name) {
|
||||
calls.push(['removeAttribute', this.className, name])
|
||||
delete this[name]
|
||||
}
|
||||
|
||||
load() {
|
||||
calls.push(['ad.load'])
|
||||
}
|
||||
}
|
||||
const prefix = 'artplayer-plugin-ads'
|
||||
function append(parent, value) {
|
||||
if (typeof value !== 'string') {
|
||||
parent.children.push(value)
|
||||
value.parentNode = parent
|
||||
return value
|
||||
}
|
||||
const match = value.match(/^<(\w+)\s[^>]*class="([^"]*)"[^>]*>([\s\S]*)<\/\w+>$/)
|
||||
@@ -121,6 +138,7 @@ export function adsEnvironment(t) {
|
||||
const [, tag, className, inner] = match
|
||||
const node = new Node(tag, className, inner)
|
||||
parent.children.push(node)
|
||||
node.parentNode = parent
|
||||
for (const child of inner.matchAll(/<div class="([^"]+)"[^>]*>([\s\S]*?)<\/div>/g))
|
||||
node.children.push(new Node('div', child[1], child[2]))
|
||||
return node
|
||||
|
||||
Reference in new issue
Block a user