refactor(dpip): [PKG-DPIP-03] own windows and DOM resources in strict TypeScript

This commit is contained in:
Harvey Zhao committed 2026-09-12 14:22:09 +08:00
1 parent 202351934b
commit 80d37d66f8
31 files changed
+2335 -597

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import { hash } from '../../refactor/scripts/releases.mjs'
import { expect } from './fixtures.js'
export async function setupDpip(page, core, implementation, testInfo) {
await page.goto(`/test/player.html?core=${core}`)
await page.evaluate(() => {
window.dpipEvidence = { nativeCapability: typeof window.documentPictureInPicture?.requestWindow === 'function', scope: 'real DOM iframe with controlled requestWindow; not native Document PiP', windows: [], requests: [], events: [] }
Object.defineProperty(window, 'documentPictureInPicture', { configurable: true, value: {
requestWindow(option) {
return new Promise((resolve) => {
window.dpipEvidence.requests.push({ option, resolve })
})
},
} })
window.resolveDpip = (index) => {
const frame = document.createElement('iframe')
frame.width = '640'
frame.height = '360'
frame.style.cssText = 'display:block;border:0;width:640px;height:360px'
document.body.appendChild(frame)
const popup = frame.contentWindow
const record = { frame, popup, closed: false }
Object.defineProperty(popup, 'close', { configurable: true, value: () => {
record.closed = true
} })
window.dpipEvidence.windows.push(record)
window.dpipEvidence.requests[index].resolve(popup)
}
})
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.dpipFactory = module.exports.default || module.exports; })();` })
await page.evaluate(() => {
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, plugins: [window.dpipFactory()] })
window.dpip = window.art.plugins.artplayerPluginDocumentPip
window.dpipPlayer = window.art.template.$player
window.dpipVideo = window.art.video
window.dpipParent = window.dpipPlayer.parentNode
window.art.on('document-pip', active => window.dpipEvidence.events.push(active))
})
await expect.poll(() => page.evaluate(() => window.art.video.readyState)).toBeGreaterThanOrEqual(2)
await testInfo.attach('dpip-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, ...(await page.evaluate(() => ({ capability: window.dpipEvidence.nativeCapability, scope: window.dpipEvidence.scope }))) }) })
}
+65
View File
@@ -0,0 +1,65 @@
import { dpipCandidate } from '../helpers/dpip.js'
import { setupDpip } from './dpip-helpers.js'
import { expect, test } from './fixtures.js'
const implementation = await dpipCandidate()
for (const core of ['published', 'candidate']) {
test(`Document PiP candidate/${core}: concurrent opens coalesce and a moved placeholder restores the native node`, async ({ page }, testInfo) => {
await setupDpip(page, core, implementation, testInfo)
await page.evaluate(() => {
window.firstDpipOpen = window.dpip.open()
window.secondDpipOpen = window.dpip.open()
})
expect(await page.evaluate(() => window.dpipEvidence.requests.length)).toBe(1)
await page.evaluate(async () => {
window.resolveDpip(0)
await Promise.all([window.firstDpipOpen, window.secondDpipOpen])
})
expect(await page.evaluate(() => ({ active: window.dpip.isActive, adopted: window.dpipPlayer.ownerDocument === window.dpipEvidence.windows[0].popup.document, sameVideo: window.art.video === window.dpipVideo }))).toEqual({ active: true, adopted: true, sameVideo: true })
await page.evaluate(async () => {
document.body.appendChild(document.querySelector('.artplayer-document-pip-placeholder'))
await window.dpip.close()
})
expect(await page.evaluate(() => ({ active: window.dpip.isActive, restored: window.dpipPlayer.parentNode === window.dpipParent && window.dpipPlayer.ownerDocument === document, closed: window.dpipEvidence.windows[0].closed, placeholder: document.querySelectorAll('.artplayer-document-pip-placeholder').length, styles: window.dpipEvidence.windows[0].popup.document.head.children.length, events: window.dpipEvidence.events }))).toEqual({ active: false, restored: true, closed: true, placeholder: 0, styles: 0, events: [true, false] })
await page.evaluate(() => window.art.destroy())
})
test(`Document PiP candidate/${core}: close and destroy revoke pending window requests before they resolve`, async ({ page }, testInfo) => {
await setupDpip(page, core, implementation, testInfo)
await page.evaluate(async () => {
const opening = window.dpip.open()
await window.dpip.close()
await opening
window.resolveDpip(0)
})
await expect.poll(() => page.evaluate(() => window.dpipEvidence.windows[0].closed)).toBe(true)
expect(await page.evaluate(() => ({ active: window.dpip.isActive, restored: window.dpipPlayer.parentNode === window.dpipParent, events: window.dpipEvidence.events }))).toEqual({ active: false, restored: true, events: [] })
await page.evaluate(async () => {
const opening = window.dpip.open()
window.art.destroy()
await opening
window.resolveDpip(1)
})
await expect.poll(() => page.evaluate(() => window.dpipEvidence.windows[1].closed)).toBe(true)
await page.evaluate(async () => {
await window.dpip.open()
window.dpip.toggle()
})
expect(await page.evaluate(() => ({ active: window.dpip.isActive, requests: window.dpipEvidence.requests.length, placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length }))).toEqual({ active: false, requests: 2, placeholders: 0 })
})
test(`Document PiP candidate/${core}: interrupted native adoption rolls back without leaving a root or style nodes`, async ({ page }, testInfo) => {
await setupDpip(page, core, implementation, testInfo)
await page.evaluate(async () => {
const opening = window.dpip.open()
window.resolveDpip(0)
window.dpipEvidence.windows[0].popup.document.adoptNode = () => {
throw new DOMException('controlled adoption failure', 'NotSupportedError')
}
await opening
})
expect(await page.evaluate(() => ({ active: window.dpip.isActive, restored: window.dpipPlayer.parentNode === window.dpipParent, closed: window.dpipEvidence.windows[0].closed, placeholder: document.querySelectorAll('.artplayer-document-pip-placeholder').length, roots: window.dpipEvidence.windows[0].popup.document.body.children.length, styles: window.dpipEvidence.windows[0].popup.document.head.children.length }))).toEqual({ active: false, restored: true, closed: true, placeholder: 0, roots: 0, styles: 0 })
await page.evaluate(() => window.art.destroy())
})
}
+1 -40
View File
@@ -1,48 +1,9 @@
import { hash } from '../../refactor/scripts/releases.mjs'
import { dpipHistorical } from '../helpers/dpip.js'
import { setupDpip as setup } from './dpip-helpers.js'
import { expect, test } from './fixtures.js'
const implementations = await dpipHistorical()
async function setup(page, core, implementation, testInfo) {
await page.goto(`/test/player.html?core=${core}`)
await page.evaluate(() => {
window.dpipEvidence = { nativeCapability: typeof window.documentPictureInPicture?.requestWindow === 'function', scope: 'real DOM iframe with controlled requestWindow; not native Document PiP', windows: [], requests: [], events: [] }
Object.defineProperty(window, 'documentPictureInPicture', { configurable: true, value: {
requestWindow(option) {
return new Promise((resolve) => {
window.dpipEvidence.requests.push({ option, resolve })
})
},
} })
window.resolveDpip = (index) => {
const frame = document.createElement('iframe')
frame.width = '640'
frame.height = '360'
frame.style.cssText = 'display:block;border:0;width:640px;height:360px'
document.body.appendChild(frame)
const popup = frame.contentWindow
const record = { frame, popup, closed: false }
Object.defineProperty(popup, 'close', { configurable: true, value: () => {
record.closed = true
} })
window.dpipEvidence.windows.push(record)
window.dpipEvidence.requests[index].resolve(popup)
}
})
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.dpipFactory = module.exports.default || module.exports; })();` })
await page.evaluate(() => {
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, plugins: [window.dpipFactory()] })
window.dpip = window.art.plugins.artplayerPluginDocumentPip
window.dpipPlayer = window.art.template.$player
window.dpipVideo = window.art.video
window.dpipParent = window.dpipPlayer.parentNode
window.art.on('document-pip', active => window.dpipEvidence.events.push(active))
})
await expect.poll(() => page.evaluate(() => window.art.video.readyState)).toBeGreaterThanOrEqual(2)
await testInfo.attach('dpip-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, ...(await page.evaluate(() => ({ capability: window.dpipEvidence.nativeCapability, scope: window.dpipEvidence.scope }))) }) })
}
for (const implementation of implementations) {
for (const core of ['published', 'candidate']) {
test(`Document PiP ${implementation.name}/${core}: real DOM migration and pagehide restore preserve media and controls`, async ({ page }, testInfo) => {