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

170 lines
9.3 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 { expect, test } from './fixtures.js'
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 plugin of ['published', 'candidate']) {
for (const mode of ['web', 'native']) {
test(`${core} core / ${plugin} Danmuku: ${mode} fullscreen across three player lifetimes`, async ({ page }, testInfo) => {
test.setTimeout(45000) // Three independent players, native entry/exit and timestamp-delivered rows.
await page.goto(`/test/player.html?core=${core}`)
const input = implementations.get(plugin)
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ plugin, provenance: input.provenance || { kind: 'published', sha256: hash(input.code) } }) })
const evidence = { core, plugin, mode, input: input.input, sha256: hash(input.code), cycles: [], outcome: 'incomplete' }
await page.evaluate(() => {
window.fullscreenWorkers = []
window.fullscreenLifetimes = []
const NativeWorker = window.Worker
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args)
this.record = { replies: 0, terminated: 0 }
window.fullscreenWorkers.push(this.record)
this.addEventListener('message', () => this.record.replies++)
}
terminate() {
this.record.terminated++
return super.terminate()
}
}
})
await page.addScriptTag({ content: input.code })
try {
for (let cycle = 0; cycle < 3; cycle++) {
await page.evaluate(({ cycle, mode }) => {
window.fullscreenRows = []
window.fullscreenErrors = []
window.fullscreenModes = []
window.art = new window.Artplayer({
container: '.player',
url: '/assets/sample/video.mp4',
muted: true,
fullscreen: true,
fullscreenWeb: true,
plugins: [window.artplayerPluginDanmuku({ danmuku: [], heatmap: true, emitter: true, speed: 5 })],
})
window.fullscreenPlugin = window.art.plugins.artplayerPluginDanmuku
window.fullscreenOwner = window.fullscreenPlugin.config({})
const { art, fullscreenModes: modes, fullscreenRows: rows, fullscreenErrors: errors } = window
window.fullscreenLifetimes.push({ modes, rows, errors })
art.on(mode === 'native' ? 'fullscreen' : 'fullscreenWeb', active => modes.push(active))
art.on('artplayerPluginDanmuku:visible', row => rows.push({ text: row.text, time: art.currentTime }))
art.on('artplayerPluginDanmuku:error', error => errors.push(String(error)))
const toggle = document.createElement('button')
toggle.id = 'danmuku-fullscreen-toggle'
toggle.textContent = `Fullscreen ${cycle}`
toggle.style.cssText = 'position:absolute;top:10px;left:10px;z-index:9999'
toggle.onclick = () => {
if (mode === 'native')
window.art.fullscreen = !window.art.fullscreen
else
window.art.fullscreenWeb = !window.art.fullscreenWeb
}
window.art.template.$player.append(toggle)
document.querySelector('#play').onclick = () => window.art.play()
}, { cycle, mode })
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)
const width = await page.evaluate(() => window.art.template.$player.clientWidth)
await page.click('#danmuku-fullscreen-toggle')
if (mode === 'native')
await expect.poll(() => page.evaluate(() => (document.fullscreenElement || document.webkitFullscreenElement) === window.art.template.$player)).toBe(true)
else
await expect.poll(() => page.evaluate(() => window.art.fullscreenWeb)).toBe(true)
const text = `Fullscreen row ${cycle}`
await page.evaluate(async (text) => {
await window.fullscreenPlugin.load([{ text, time: window.art.currentTime + 0.5, mode: 1 }])
}, text)
await expect.poll(() => page.evaluate(text => window.fullscreenRows.some(row => row.text === text), text)).toBe(true)
// The published renderer retains a hidden measurement node with the same text.
await expect(page.getByText(text, { exact: true }).and(page.locator(':visible'))).toBeVisible()
const entered = await page.evaluate(() => ({
width: window.art.template.$player.clientWidth,
layerWidth: window.art.template.$danmuku.clientWidth,
settings: document.querySelectorAll('.artplayer-plugin-danmuku').length,
heatmaps: document.querySelectorAll('.art-control-heatmap').length,
time: window.art.currentTime,
}))
expect(entered.width).toBeGreaterThan(width)
expect(entered.layerWidth).toBe(entered.width)
expect(entered.settings).toBe(1)
expect(entered.heatmaps).toBe(1)
await page.evaluate(() => window.art.pause())
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
await page.click('#danmuku-fullscreen-toggle')
if (mode === 'native')
await expect.poll(() => page.evaluate(() => Boolean(document.fullscreenElement || document.webkitFullscreenElement))).toBe(false)
else
await expect.poll(() => page.evaluate(() => window.art.fullscreenWeb)).toBe(false)
await expect.poll(() => page.evaluate(() => window.art.template.$player.clientWidth)).toBe(width)
// Native exit can clear fullscreenElement before its change event is delivered.
await expect.poll(() => page.evaluate(() => window.fullscreenModes)).toEqual([true, false])
const after = await page.evaluate(() => {
const art = window.art
const owner = window.fullscreenOwner
const modes = [...window.fullscreenModes]
art.destroy()
return {
modes,
errors: window.fullscreenErrors,
roots: document.querySelectorAll('.art-video-player').length,
settings: document.querySelectorAll('.artplayer-plugin-danmuku').length,
heatmaps: document.querySelectorAll('.art-control-heatmap').length,
styles: document.querySelectorAll('style#artplayer-plugin-danmuku').length,
workers: window.fullscreenWorkers.map(worker => ({ ...worker })),
frame: owner.scheduler?.frame,
operation: owner.scheduler?.operation,
}
})
evidence.cycles.push({ cycle, entered, after })
expect(after.roots + after.settings + after.heatmaps).toBe(0)
expect(after.styles).toBe(1)
expect(after.errors).toEqual([])
expect(after.modes).toEqual([true, false])
expect(after.workers).toHaveLength(cycle + 1)
for (const worker of after.workers) {
expect(worker.replies).toBeGreaterThan(0)
expect(worker.terminated).toBe(1)
}
if (plugin === 'candidate') {
expect(after.frame).toBeNull()
expect(after.operation).toBeNull()
}
}
evidence.lifetimes = await page.evaluate(() => window.fullscreenLifetimes)
for (const [index, lifetime] of evidence.lifetimes.entries()) {
// BASE-LIFE-18: the frozen core keeps native listeners after destroy.
const changes = core === 'published' && mode === 'native' ? 3 - index : 1
expect(lifetime.modes).toEqual(Array.from({ length: changes }, () => [true, false]).flat())
expect(lifetime.errors).toEqual([])
}
evidence.outcome = 'three-lifetimes-fullscreen-playback-cleanup-passed'
}
finally {
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
})
await testInfo.attach('danmuku-fullscreen', { contentType: 'application/json', body: JSON.stringify(evidence) })
}
})
}
}
}