Files
ArtPlayer/test/browser/danmuku-dpip.spec.js

189 lines
11 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { observeDanmukuLoad } from '../helpers/danmuku-combination-load.js'
import { dpipCandidate } from '../helpers/dpip.js'
import { expect, test } from './fixtures.js'
const dpip = await dpipCandidate()
const implementations = new Map()
test.beforeAll(async () => {
const release = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-release.json', 'utf8')).release
const member = 'package/dist/artplayer-plugin-danmuku.js'
const published = readMember(await ensureArchive(release), member)
assert.equal(hash(published), release.files[member])
implementations.set('published', { input: `npm ${release.version}`, code: published.toString() })
const candidate = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT)
implementations.set('candidate', { input: candidate.provenance.file || candidate.provenance.kind, ...candidate })
})
for (const core of ['published', 'candidate']) {
for (const implementation of ['published', 'candidate']) {
for (const ending of ['close', 'native-close', 'destroy', 'load']) {
test(`${core}: ${implementation} Danmuku, heatmap and Worker survive native Document PiP ${ending}`, async ({ page, context }, testInfo) => {
if (ending === 'load')
test.setTimeout(45000) // Two native PiP loads add 16 seconds of actual media progression.
const input = implementations.get(implementation)
const danmuku = input.code
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ implementation, provenance: input.provenance || { kind: 'published', sha256: hash(danmuku) }, dpip: dpip.provenance || { kind: dpip.name, sha256: hash(dpip.code) } }) })
await page.goto(`/test/player.html?core=${core}`)
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
const evidence = { core, implementation, ending, supported, danmuku: { input: input.input, provenance: input.provenance, sha256: hash(danmuku) }, dpip: { input: dpip.name, provenance: dpip.provenance, sha256: hash(dpip.code) }, outcome: 'incomplete', rounds: [], loads: [] }
if (!supported) {
evidence.outcome = 'native-document-pip-unavailable'
await testInfo.attach('danmuku-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
return
}
await page.evaluate(() => {
window.combinationWorkers = []
const NativeWorker = window.Worker
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args)
this.record = { terminated: 0, replies: 0 }
window.combinationWorkers.push(this.record)
this.addEventListener('message', () => this.record.replies++)
}
terminate() {
this.record.terminated++
return super.terminate()
}
}
})
await page.addScriptTag({ content: danmuku })
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${dpip.code}; window.dpipFactory = module.exports.default || module.exports; })();` })
await page.evaluate(() => {
window.combinationVisible = []
window.combinationErrors = []
window.combinationEvents = []
window.art = new window.Artplayer({
container: '.player',
url: '/assets/sample/video.mp4',
muted: true,
plugins: [window.artplayerPluginDanmuku({ danmuku: [], heatmap: true, emitter: true, speed: 4 }), window.dpipFactory({ fallbackToVideoPiP: false })],
})
window.danmuku = window.art.plugins.artplayerPluginDanmuku
window.danmukuOwner = window.danmuku.config({})
window.pip = window.art.plugins.artplayerPluginDocumentPip
window.combinationPlayer = window.art.template.$player
window.combinationLayer = window.art.template.$danmuku
window.combinationParent = window.combinationPlayer.parentNode
window.art.on('document-pip', value => window.combinationEvents.push(value))
window.art.on('artplayerPluginDanmuku:error', error => window.combinationErrors.push(String(error)))
window.art.on('artplayerPluginDanmuku:visible', row => window.combinationVisible.push({ text: row.text, ref: row.$ref, time: window.art.currentTime }))
document.querySelector('#play').onclick = () => window.art.play()
const open = document.createElement('button')
open.id = 'danmuku-open-pip'
open.textContent = 'Open Danmuku PiP'
open.onclick = () => {
window.openingDanmukuPip = window.pip.open()
}
document.body.append(open)
})
try {
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
for (let round = 0; round < 2; round++) {
const popupPromise = context.waitForEvent('page', { timeout: 8000 })
await page.click('#danmuku-open-pip')
const popup = await popupPromise
await page.evaluate(() => window.openingDanmukuPip)
await expect(popup.locator('video.art-video')).toBeVisible()
await expect(popup.locator('.artplayer-plugin-danmuku')).toHaveCount(1)
const text = `PiP row ${round}`
await page.evaluate(async (text) => {
await window.danmuku.load([{ text, time: window.art.currentTime + 0.5, mode: 1 }])
}, text)
await expect.poll(() => page.evaluate(text => window.combinationVisible.some(row => row.text === text), text)).toBe(true)
await expect(popup.getByText(text, { exact: true }).and(popup.locator(':visible'))).toBeVisible()
const snapshot = await page.evaluate((text) => {
const row = window.combinationVisible.find(row => row.text === text)
const rect = row.ref.getBoundingClientRect()
const popup = window.documentPictureInPicture.window
return {
time: window.art.currentTime,
rowTime: row.time,
width: rect.width,
height: rect.height,
adopted: row.ref.ownerDocument === popup.document && window.combinationLayer.ownerDocument === popup.document,
inLayer: window.combinationLayer.contains(row.ref),
position: popup.getComputedStyle(row.ref).position,
player: window.combinationPlayer.ownerDocument === popup.document,
heatmap: window.art.controls.heatmap.ownerDocument === popup.document && !!window.art.controls.heatmap.querySelector('svg'),
workers: window.combinationWorkers.map(worker => ({ ...worker })),
}
}, text)
evidence.rounds.push(snapshot)
expect(snapshot.adopted && snapshot.inLayer && snapshot.player && snapshot.heatmap).toBe(true)
expect(snapshot.width).toBeGreaterThan(0)
expect(snapshot.height).toBeGreaterThan(0)
expect(snapshot.position).toBe('absolute')
expect(snapshot.workers).toHaveLength(1)
expect(snapshot.workers[0].replies).toBeGreaterThan(0)
if (round === 0)
await testInfo.attach('native-danmuku-page', { contentType: 'image/png', body: await popup.screenshot() })
if (ending === 'load')
evidence.loads.push(await observeDanmukuLoad(page, { label: `PiP ${round}`, requireComplete: implementation === 'candidate' }))
evidence.beforeDestroy = await page.evaluate(() => ({ nodes: window.combinationLayer.children.length }))
if (round === 1 && ending === 'destroy') {
await page.evaluate(() => window.art.destroy(false))
}
else {
await page.evaluate(ending => ending === 'native-close' ? window.documentPictureInPicture.window.close() : window.pip.close(), ending)
await expect.poll(() => page.evaluate(() => window.pip.isActive)).toBe(false)
expect(await page.evaluate(() => window.combinationPlayer.parentNode === window.combinationParent && window.combinationLayer.ownerDocument === document)).toBe(true)
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(snapshot.time + 0.1)
}
}
if (ending !== 'destroy')
await page.evaluate(() => window.art.destroy(false))
await expect.poll(() => page.evaluate(() => !window.documentPictureInPicture.window || window.documentPictureInPicture.window.closed)).toBe(true)
evidence.final = await page.evaluate(() => ({
active: window.pip.isActive,
workers: window.combinationWorkers,
errors: window.combinationErrors,
events: window.combinationEvents,
frame: window.danmukuOwner.scheduler?.frame,
operation: window.danmukuOwner.scheduler?.operation,
nodes: window.combinationLayer.children.length,
settings: document.querySelectorAll('.artplayer-plugin-danmuku').length,
placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length,
}))
expect(evidence.final.active).toBe(false)
expect(evidence.final.workers).toHaveLength(1)
expect(evidence.final.workers[0].terminated).toBe(1)
expect(evidence.final.errors).toEqual([])
// Destroy suppresses further public events, as in the existing native PiP contract.
expect(evidence.final.events).toEqual(ending === 'destroy' ? [true, false, true] : [true, false, true, false])
if (implementation === 'candidate') {
expect(evidence.final.frame).toBeNull()
expect(evidence.final.operation).toBeNull()
expect(evidence.final.nodes + evidence.final.settings + evidence.final.placeholders).toBe(0)
evidence.outcome = 'native-playback-danmuku-and-cleanup-passed'
}
else {
// Frozen npm 5.3.0 destroy(false) retains its renderer and Setting nodes.
expect(evidence.final.nodes).toBe(ending === 'load' ? evidence.beforeDestroy.nodes : 3)
expect(evidence.final.nodes).toBeGreaterThan(0)
expect(evidence.final.settings).toBe(1)
expect(evidence.final.placeholders).toBe(0)
evidence.outcome = 'native-playback-with-published-retained-dom'
}
}
finally {
evidence.incompleteLoad = await page.evaluate(() => window.danmukuCombinationLoadResult)
await testInfo.attach('danmuku-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy(false)
})
}
})
}
}
}