import fs from 'node:fs' import process from 'node:process' import { hash } from '../../refactor/scripts/releases.mjs' import { dpipCandidate } from '../helpers/dpip.js' import { compilePackage } from '../helpers/load.js' import { expect, test } from './fixtures.js' const dpip = await dpipCandidate() let danmuku test.beforeAll(async () => { danmuku = process.env.ARTPLAYER_DANMUKU_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-danmuku', 'umd') }) for (const core of ['published', 'candidate']) { for (const ending of ['close', 'native-close', 'destroy']) { test(`${core}: Danmuku, heatmap and Worker survive native Document PiP ${ending}`, async ({ page, context }, testInfo) => { await page.goto(`/test/player.html?core=${core}`) const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function') const evidence = { core, ending, supported, danmuku: { input: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', sha256: hash(danmuku) }, dpip: { input: dpip.name, sha256: hash(dpip.code) }, outcome: 'incomplete', rounds: [] } 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 })).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 (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]) 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' } finally { 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) }) } }) } }