test(vast): [PKG-VAST-05] checkpoint native tag requests and skip controls

This commit is contained in:
Harvey Zhao committed 2026-09-14 23:57:20 +08:00
1 parent 75da50a454
commit 09459955c6
13 files changed
+1789 -9

No files matched your search

+8 -1
View File
@@ -11,6 +11,7 @@ import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases
import { performanceHtml, performanceScript } from '../../scripts/performance-fixture.mjs'
import { getEntryFile } from '../../scripts/projects.js'
import { getGlobalName, getViteBuildConfig } from '../../scripts/utils.js'
import { skippableVast } from './vast-fixture.mjs'
const workspace = fileURLToPath(new URL('../../', import.meta.url))
@@ -94,7 +95,8 @@ add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/m
add('/test/thumbnail-pattern.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-pattern.mp4')), { kind: 'generated-media', file: 'test/browser/media/thumbnail-pattern.mp4' })
add('/test/auto-thumbnail-timeline.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/auto-thumbnail-timeline.mp4')), { kind: 'generated-media', file: 'test/browser/media/auto-thumbnail-timeline.mp4' })
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', '.wasm': 'application/wasm', '.woff2': 'font/woff2' }
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.xml': 'application/xml', '.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', '.wasm': 'application/wasm', '.woff2': 'font/woff2' }
add('/test/vast-skippable.xml', Buffer.from(skippableVast(`http://127.0.0.1:${port}`)), { kind: 'local-vast-fixture', file: 'test/browser/vast-fixture.mjs' })
add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' })
add('/test/thumbnail-grid.svg', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-grid.svg')), { kind: 'test-thumbnail-grid', file: 'test/browser/media/thumbnail-grid.svg' })
add('/test/legacy-safe-area.js', fs.readFileSync(path.join(workspace, 'test/helpers/legacy-safe-area.js')), { kind: 'frozen-own-source', file: 'test/helpers/legacy-safe-area.js', commit: 'ccf77c4e' })
@@ -170,6 +172,11 @@ const server = http.createServer((req, res) => {
return
}
if (files.has(url.pathname)) {
if (url.pathname === '/test/vast-skippable.xml' && ['http://imasdk.googleapis.com', 'https://imasdk.googleapis.com'].includes(req.headers.origin)) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Vary', 'Origin')
}
send(req, res, files.get(url.pathname), url.pathname)
return
}
+10
View File
@@ -0,0 +1,10 @@
export function skippableVast(origin) {
return `<?xml version="1.0" encoding="UTF-8"?>
<VAST version="3.0"><Ad id="artplayer-skippable"><InLine>
<AdSystem version="1">ArtPlayer test fixture</AdSystem><AdTitle>Local skippable ad</AdTitle>
<Impression><![CDATA[${origin}/assets/vast/nonlinear-320x50.png?tracking=impression]]></Impression>
<Creatives><Creative><Linear skipoffset="00:00:05"><Duration>00:00:08</Duration>
<TrackingEvents><Tracking event="skip"><![CDATA[${origin}/assets/vast/nonlinear-320x50.png?tracking=skip]]></Tracking></TrackingEvents>
<MediaFiles><MediaFile delivery="progressive" type="video/mp4" width="320" height="180"><![CDATA[${origin}/test/pattern.mp4?skippable=1]]></MediaFile></MediaFiles>
</Linear></Creative></Creatives></InLine></Ad></VAST>`
}
+78
View File
@@ -0,0 +1,78 @@
import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
import { skippableVast } from './vast-fixture.mjs'
// eslint-disable-next-line antfu/no-top-level-await -- Native SDK bundle is shared by test discovery.
const code = await compilePackage('artplayer-plugin-vast', 'umd')
for (const compatibility of [undefined, 'workspace-1.2']) {
test(`real IMA ${compatibility || 'npm-default'} requests playUrl and skips through the SDK button`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=candidate')
await page.addScriptTag({ content: code })
await page.evaluate(async (compatibility) => {
window.createPlayer('/assets/sample/video.mp4')
window.skipEvents = []
await window.art.plugins.add(window.artplayerPluginVast((context) => {
window.skipContext = context
const player = context.init()
for (const type of ['AdStarted', 'AdComplete', 'AdSkipped', 'AdSkippableStateChanged', 'AdContentPauseRequested', 'AdContentResumeRequested', 'AdError']) {
player.addEventListener(type, event => window.skipEvents.push({ type, at: performance.now(), time: window.art.currentTime, paused: window.art.template.$video.paused, skipOffset: event.detail?.ad?.getSkipTimeOffset(), error: event.detail?.error?.message }))
}
}, compatibility ? { compatibility } : undefined))
}, compatibility)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.5)
await page.evaluate(() => {
document.querySelector('#play').onclick = () => window.skipContext.playUrl(`${location.origin}/test/vast-skippable.xml`)
})
const beforeAd = await page.evaluate(() => window.art.currentTime)
const responsePromise = page.waitForResponse(response => response.url().endsWith('/test/vast-skippable.xml'), { timeout: 7000 })
await page.locator('#play').click()
const response = await responsePromise
expect(response.status()).toBe(200)
expect(response.headers()['content-type']).toBe('application/xml')
expect(['http://imasdk.googleapis.com', 'https://imasdk.googleapis.com']).toContain(response.headers()['access-control-allow-origin'])
const xml = await response.text()
expect(xml).toBe(skippableVast(new URL(page.url()).origin))
await testInfo.attach('native-vast-skip.xml', { contentType: 'application/xml', body: xml })
await expect.poll(() => page.evaluate(() => window.skipEvents.some(event => event.type === 'AdStarted'))).toBe(true)
await expect.poll(() => page.evaluate(() => [...document.querySelectorAll('video')].some(video => video.currentSrc.includes('/test/pattern.mp4?skippable=1') && video.videoWidth > 0 && video.currentTime > 0))).toBe(true)
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
await testInfo.attach('native-vast-before-skip.png', { contentType: 'image/png', body: await page.screenshot() })
await expect.poll(() => page.evaluate(() => window.skipEvents.some(event => event.type === 'AdSkippableStateChanged'))).toBe(true)
let skipButton
await expect.poll(async () => {
for (const frame of page.frames()) {
const button = frame.getByRole('button', { name: /skip ad/i })
if (await button.count() === 1 && await button.isVisible() && await button.isEnabled()) {
skipButton = button
return true
}
}
return false
}).toBe(true)
await testInfo.attach('native-vast-skip-button.png', { contentType: 'image/png', body: await page.screenshot() })
await skipButton.click()
await expect.poll(() => page.evaluate(() => window.skipEvents.filter(event => event.type === 'AdSkipped').length)).toBe(1)
await expect.poll(() => page.evaluate(() => window.skipEvents.some(event => event.type === 'AdContentResumeRequested'))).toBe(true)
await expect(page.locator('[id^="art-"]')).toBeHidden()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(beforeAd + 0.2)
const state = await page.evaluate(() => ({ events: window.skipEvents, time: window.art.currentTime, paused: window.art.template.$video.paused, imaVersion: window.google.ima.VERSION }))
expect(state.paused).toBe(false)
expect(state.events.filter(event => event.type === 'AdError' || event.type === 'AdComplete')).toEqual([])
await testInfo.attach('native-vast-skip', { contentType: 'application/json', body: JSON.stringify({ sourceSha256: hash(code), xmlSha256: hash(xml), mediaSha256: hash(fs.readFileSync('test/browser/media/pattern.mp4')), compatibility: compatibility || 'npm-default', beforeAd, ...state }) })
await page.evaluate(() => window.art.destroy())
await expect(page.locator('[id^="art-"]')).toHaveCount(0)
})
}
test.afterEach(async ({ page }, testInfo) => {
await testInfo.attach('native-vast-skip-final', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ events: window.skipEvents, imaVersion: window.google?.ima?.VERSION, videos: [...document.querySelectorAll('video')].map(video => ({ src: video.currentSrc, time: video.currentTime, width: video.videoWidth, paused: video.paused })) }))) })
for (const [index, frame] of page.frames().entries()) {
if (frame !== page.mainFrame())
await testInfo.attach(`native-vast-frame-${index}`, { contentType: 'text/plain', body: await frame.locator('body').ariaSnapshot().catch(error => String(error)) })
}
await page.evaluate(() => window.art?.destroy())
})
+7 -1
View File
@@ -17,6 +17,7 @@ test.afterEach(async ({ page }, testInfo) => {
error: window.nativeVastError,
imaVersion: window.google?.ima?.VERSION,
events: window.nativeVastEvents,
mediaSamples: window.nativeVastMediaSamples,
videos: [...document.querySelectorAll('video')].map(video => ({ src: video.currentSrc, time: video.currentTime, duration: video.duration, width: video.videoWidth, height: video.videoHeight, paused: video.paused, ended: video.ended })),
containerConnected: window.nativeVastContext?.container?.isConnected,
coreVersion: window.Artplayer?.version,
@@ -41,6 +42,7 @@ for (const core of ['published-5.1.7', 'published', 'candidate']) {
await page.evaluate(({ compatibility, historical }) => {
window.nativeVastHistorical = historical
window.nativeVastEvents = []
window.nativeVastMediaSamples = []
window.nativeVastRegistration = 'pending'
window.createPlayer('/assets/sample/video.mp4')
const factory = window.artplayerPluginVast.default || window.artplayerPluginVast
@@ -81,7 +83,11 @@ for (const core of ['published-5.1.7', 'published', 'candidate']) {
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.nativeVastEvents.some(event => event.type === 'AdStarted'))).toBe(true)
await expect(page.locator('[id^="art-"]')).toBeVisible()
await expect.poll(() => page.evaluate(() => [...document.querySelectorAll('video')].some(video => video.currentSrc.includes('/assets/vast/linear-video.mp4') && video.videoWidth > 0 && video.videoHeight > 0 && video.currentTime > 0))).toBe(true)
await expect.poll(() => page.evaluate(() => {
const videos = [...document.querySelectorAll('video')].map(video => ({ src: video.currentSrc, declaredSrc: video.src, width: video.videoWidth, height: video.videoHeight, time: video.currentTime, readyState: video.readyState, paused: video.paused }))
window.nativeVastMediaSamples.push({ at: performance.now(), videos })
return videos.some(video => video.src.includes('/assets/vast/linear-video.mp4') && video.width > 0 && video.height > 0 && video.time > 0)
})).toBe(true)
const decodedAd = await page.evaluate(() => [...document.querySelectorAll('video')].map(video => ({ src: video.currentSrc, width: video.videoWidth, height: video.videoHeight, time: video.currentTime })).filter(video => video.src.includes('/assets/vast/linear-video.mp4')))
expect(decodedAd.some(video => video.width > 0 && video.height > 0)).toBe(true)
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)