mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-20 20:21:20 -08:00
refactor(dpip): [PKG-DPIP-03] own windows and DOM resources in strict TypeScript
This commit is contained in:
1 parent
202351934b
commit
80d37d66f8
31 files changed
+2335
-597
No files matched your search
@@ -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 }))) }) })
|
||||
}
|
||||
@@ -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,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) => {
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Candidate fixes are compared to the immutable workspace via ARTPLAYER_DPIP_BASELINE.
|
||||
import test from 'node:test'
|
||||
import { dpipCandidate, dpipEnvironment } from './helpers/dpip.js'
|
||||
|
||||
const implementation = await dpipCandidate()
|
||||
function deferred() {
|
||||
let resolve
|
||||
let reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
const setup = settings => dpipEnvironment(implementation, settings)
|
||||
|
||||
test('Document PiP candidate: public keys, snapshot/default options, controls and transition ordering remain stable', async () => {
|
||||
const env = setup()
|
||||
const options = { width: 720, placeholder: 'Active' }
|
||||
const plugin = env.factory(options)
|
||||
options.width = 1
|
||||
const result = plugin(env.art)
|
||||
assert.deepEqual(Object.keys(result), ['name', 'isSupported', 'isActive', 'open', 'close', 'toggle'])
|
||||
assert.equal(Reflect.set(result, 'isActive', true), false)
|
||||
assert.equal(result.name, 'artplayerPluginDocumentPip')
|
||||
assert.equal(env.controls.get('document-pip').spec.index, 40)
|
||||
assert.equal(await result.open(), undefined)
|
||||
assert.deepEqual({ ...env.requests[0] }, { width: 720, height: 270 })
|
||||
assert.deepEqual(env.emitted.map(event => [event.name, ...event.args]), [['document-pip', true], ['resize']])
|
||||
assert.equal(env.parent.children[0].textContent, 'Active')
|
||||
assert.equal(env.controls.get('document-pip').node.tooltip, 'Exit PIP Mode')
|
||||
assert.equal(await result.close(), undefined)
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
assert.deepEqual(env.emitted.map(event => [event.name, ...event.args]), [['document-pip', true], ['resize'], ['document-pip', false], ['resize']])
|
||||
assert.deepEqual(env.rebinds.map(item => item.document), [env.windows[0].document, env.document])
|
||||
})
|
||||
|
||||
test('Document PiP candidate: normal rejection and unsupported fallback preserve error and return boundaries', async () => {
|
||||
const error = new Error('denied')
|
||||
const env = setup({ requestWindow: () => Promise.reject(error) })
|
||||
const result = env.factory()(env.art)
|
||||
assert.equal(await result.open(), undefined)
|
||||
assert.equal(env.warnings[0][1], error)
|
||||
assert.equal(result.isActive, false)
|
||||
const fallback = setup({ supported: false })
|
||||
const other = fallback.factory()(fallback.art)
|
||||
await other.open()
|
||||
assert.equal(fallback.art.pip, true)
|
||||
assert.equal(other.isSupported, false)
|
||||
Object.defineProperty(fallback.art, 'pip', { get() {
|
||||
return false
|
||||
}, set() {
|
||||
throw error
|
||||
} })
|
||||
await assert.rejects(other.open(), caught => caught === error)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: overlapping opens issue one request and share one restored window', async () => {
|
||||
const pending = deferred()
|
||||
const env = setup({ requestWindow: () => pending.promise })
|
||||
const result = env.factory()(env.art)
|
||||
const first = result.open()
|
||||
const second = result.open()
|
||||
assert.equal(env.requests.length, 1)
|
||||
pending.resolve(env.createWindow())
|
||||
await Promise.all([first, second])
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
await result.close()
|
||||
assert.equal(env.windows[0].closed, true)
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
})
|
||||
|
||||
test('Document PiP candidate: close settles a pending open and closes the late window without adopting', async () => {
|
||||
const pending = deferred()
|
||||
const env = setup({ requestWindow: () => pending.promise })
|
||||
const result = env.factory()(env.art)
|
||||
let settled = false
|
||||
const opening = result.open().then(() => {
|
||||
settled = true
|
||||
})
|
||||
await result.close()
|
||||
await env.flush()
|
||||
assert.equal(settled, true)
|
||||
const win = env.createWindow()
|
||||
pending.resolve(win)
|
||||
await env.flush()
|
||||
await opening
|
||||
assert.equal(win.closed, true)
|
||||
assert.equal(result.isActive, false)
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
assert.deepEqual(env.emitted, [])
|
||||
})
|
||||
|
||||
test('Document PiP candidate: pending toggle cancels and a fresh request survives the old resolution', async () => {
|
||||
const requests = [deferred(), deferred()]
|
||||
let index = 0
|
||||
const env = setup({ requestWindow: () => requests[index++].promise })
|
||||
const result = env.factory()(env.art)
|
||||
const old = result.open()
|
||||
assert.equal(result.toggle(), undefined)
|
||||
const current = result.open()
|
||||
const newWin = env.createWindow()
|
||||
requests[1].resolve(newWin)
|
||||
await current
|
||||
const oldWin = env.createWindow()
|
||||
requests[0].resolve(oldWin)
|
||||
await old
|
||||
await env.flush()
|
||||
assert.equal(oldWin.closed, true)
|
||||
assert.equal(newWin.closed, false)
|
||||
assert.equal(env.player.ownerDocument, newWin.document)
|
||||
await result.close()
|
||||
})
|
||||
|
||||
test('Document PiP candidate: destroy cancels pending windows and makes escaped methods inert', async () => {
|
||||
const pending = deferred()
|
||||
const env = setup({ requestWindow: () => pending.promise })
|
||||
const result = env.factory()(env.art)
|
||||
const opening = result.open()
|
||||
env.art.destroy()
|
||||
const win = env.createWindow()
|
||||
pending.resolve(win)
|
||||
await opening
|
||||
await env.flush()
|
||||
assert.equal(win.closed, true)
|
||||
await result.open()
|
||||
result.toggle()
|
||||
await result.close()
|
||||
assert.equal(env.requests.length, 1)
|
||||
assert.equal(result.isActive, false)
|
||||
assert.equal(env.controls.size, 0)
|
||||
assert.equal(env.subscriptions.get('destroy').size, 0)
|
||||
assert.equal(env.subscriptions.get('document-pip').size, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: stale rejection after destroy does not write notices', async () => {
|
||||
const pending = deferred()
|
||||
const env = setup({ requestWindow: () => pending.promise })
|
||||
const result = env.factory()(env.art)
|
||||
const opening = result.open()
|
||||
env.art.destroy()
|
||||
pending.reject(new Error('late rejection'))
|
||||
await opening
|
||||
await env.flush()
|
||||
assert.equal(env.warnings.length, 0)
|
||||
assert.equal(env.art.notice.show, '')
|
||||
})
|
||||
|
||||
test('Document PiP candidate: destroy cancels owned transition timers and emits no delayed resize', async () => {
|
||||
const delay = deferred()
|
||||
const env = setup({ sleep: () => delay.promise })
|
||||
const result = env.factory()(env.art)
|
||||
const opening = result.open()
|
||||
await env.flush()
|
||||
env.art.destroy()
|
||||
assert.equal(env.timers.size, 0)
|
||||
const before = env.emitted.length
|
||||
delay.resolve()
|
||||
await opening
|
||||
await env.flush()
|
||||
assert.equal(env.emitted.length, before)
|
||||
assert.equal(result.isActive, false)
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
})
|
||||
|
||||
test('Document PiP candidate: destroy from the active event releases listeners and prevents subsequent effects', async () => {
|
||||
const env = setup()
|
||||
const result = env.factory()(env.art)
|
||||
env.art.on('document-pip', active => active && env.art.destroy())
|
||||
await result.open()
|
||||
assert.equal(result.isActive, false)
|
||||
assert.equal(env.windows[0].closed, true)
|
||||
assert.equal([...env.windows[0].listeners.values()].reduce((sum, value) => sum + value.size, 0), 0)
|
||||
assert.equal(env.emitted.filter(event => event.name === 'resize').length, 0)
|
||||
assert.equal(env.controls.size, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: failed adoption rolls back all window and DOM resources immediately', async () => {
|
||||
const env = setup({ requestWindow: (_, create) => {
|
||||
const win = create()
|
||||
win.document.adoptNode = () => {
|
||||
throw new Error('denied adoption')
|
||||
}
|
||||
return Promise.resolve(win)
|
||||
} })
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
assert.equal(result.isActive, false)
|
||||
assert.equal(env.windows[0].closed, true)
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
assert.equal(env.windows[0].document.head.children.length, 0)
|
||||
assert.equal(env.windows[0].document.body.children.length, 0)
|
||||
assert.match(env.art.notice.show, /open failed/)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: moved or removed placeholders restore to the original parent and sibling', async () => {
|
||||
for (const move of [true, false]) {
|
||||
const env = setup()
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
const placeholder = env.parent.children[0]
|
||||
if (move)
|
||||
env.document.body.appendChild(placeholder)
|
||||
else
|
||||
placeholder.remove()
|
||||
await result.close()
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
assert.equal(result.isActive, false)
|
||||
assert.equal(placeholder.parentNode, null)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('Document PiP candidate: cross-document styles follow the actual player owner and are released', async () => {
|
||||
const env = setup()
|
||||
const foreign = env.createWindow().document
|
||||
foreign.body.appendChild(env.parent)
|
||||
const style = foreign.createElement('style')
|
||||
style.textContent = '.foreign { color:red }'
|
||||
foreign.head.appendChild(style)
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
const target = env.windows[1].document
|
||||
assert.equal(target.querySelectorAll('style').some(node => node.textContent === style.textContent), true)
|
||||
await result.close()
|
||||
assert.equal(env.player.ownerDocument, foreign)
|
||||
assert.equal(target.head.children.length, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: duplicate loading evaluation injects one style and removes loading listeners', () => {
|
||||
const env = setup({ readyState: 'loading' })
|
||||
env.evaluate()
|
||||
env.document.dispatchEvent({ type: 'DOMContentLoaded' })
|
||||
assert.equal(env.document.querySelectorAll('style').filter(node => node.id === 'artplayer-plugin-document-pip').length, 1)
|
||||
assert.equal(env.document.listeners.get('DOMContentLoaded').size, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: registration after destroy creates no controls or listeners', async () => {
|
||||
const env = setup()
|
||||
env.art.isDestroy = true
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
assert.equal(env.controls.size, 0)
|
||||
assert.equal(env.requests.length, 0)
|
||||
assert.equal(env.subscriptions.size, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: partial control setup failure releases the click, control and destroy subscription', () => {
|
||||
const env = setup()
|
||||
const add = env.art.controls.add
|
||||
env.art.controls.add = (spec) => {
|
||||
add(spec)
|
||||
throw new Error('control failure')
|
||||
}
|
||||
assert.throws(() => env.factory()(env.art), /control failure/)
|
||||
assert.equal(env.controls.size, 0)
|
||||
assert.equal(env.subscriptions.get('destroy').size, 0)
|
||||
assert.equal(env.subscriptions.get('document-pip').size, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: throwing window cleanup still restores DOM and releases remaining resources', async () => {
|
||||
const env = setup()
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
const win = env.windows[0]
|
||||
const remove = win.removeEventListener
|
||||
win.removeEventListener = (...args) => {
|
||||
remove(...args)
|
||||
if (args[0] === 'resize')
|
||||
throw new Error('listener release failed')
|
||||
}
|
||||
await result.close()
|
||||
assert.equal(result.isActive, false)
|
||||
assert.equal(win.closed, true)
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
assert.match(env.art.notice.show, /close failed/)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: a rejecting original parent leaves the player recoverable in its original document', async () => {
|
||||
const env = setup()
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
env.parent.insertBefore = () => {
|
||||
throw new Error('parent rejects restoration')
|
||||
}
|
||||
await result.close()
|
||||
assert.equal(env.player.parentNode, env.document.body)
|
||||
assert.equal(env.player.ownerDocument, env.document)
|
||||
assert.equal(env.windows[0].closed, true)
|
||||
assert.equal(result.isActive, false)
|
||||
assert.match(env.art.notice.show, /close failed/)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: destruction during adoption restores the returned node without reactivating', async () => {
|
||||
const env = setup({ requestWindow: (_, create) => {
|
||||
const win = create()
|
||||
const adopt = win.document.adoptNode
|
||||
win.document.adoptNode = (node) => {
|
||||
env.art.destroy()
|
||||
return adopt(node)
|
||||
}
|
||||
return Promise.resolve(win)
|
||||
} })
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
await env.flush()
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
assert.equal(env.player.ownerDocument, env.document)
|
||||
assert.equal(env.windows[0].closed, true)
|
||||
assert.equal(result.isActive, false)
|
||||
assert.equal(env.controls.size, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: destruction during style insertion releases nodes created by the interrupted mount', async () => {
|
||||
const env = setup({ requestWindow: (_, create) => {
|
||||
const win = create()
|
||||
const append = win.document.head.appendChild
|
||||
win.document.head.appendChild = function (node) {
|
||||
env.art.destroy()
|
||||
return append.call(this, node)
|
||||
}
|
||||
return Promise.resolve(win)
|
||||
} })
|
||||
const result = env.factory()(env.art)
|
||||
await result.open()
|
||||
await env.flush()
|
||||
assert.equal(result.isActive, false)
|
||||
assert.deepEqual(env.parent.children, [env.player, env.sibling])
|
||||
assert.equal(env.windows[0].document.head.children.length, 0)
|
||||
})
|
||||
|
||||
test('Document PiP candidate: a throwing public notice setter rejects open with the original error', async () => {
|
||||
const env = setup({ requestWindow: () => Promise.reject(new Error('window rejected')) })
|
||||
const noticeError = new Error('notice setter failed')
|
||||
Object.defineProperty(env.art.notice, 'show', { get() {
|
||||
return ''
|
||||
}, set() {
|
||||
throw noticeError
|
||||
} })
|
||||
const result = env.factory()(env.art)
|
||||
await assert.rejects(result.open(), error => error === noticeError)
|
||||
assert.equal(result.isActive, false)
|
||||
})
|
||||
+39
-2
@@ -1,9 +1,32 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import vm from 'node:vm'
|
||||
import { transform } from 'esbuild'
|
||||
import { build, transform } from 'esbuild'
|
||||
import less from 'less'
|
||||
import { verifyDpipContract } from '../../refactor/scripts/dpip-contract.mjs'
|
||||
import { readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { getEntryFile } from '../../scripts/projects.js'
|
||||
|
||||
export async function dpipCandidate() {
|
||||
if (process.env.ARTPLAYER_DPIP_BASELINE === '1')
|
||||
return (await dpipHistorical()).find(item => item.name === 'frozen-workspace')
|
||||
if (process.env.ARTPLAYER_DPIP_ARTIFACT)
|
||||
return { name: 'candidate-artifact', code: await fs.readFile(process.env.ARTPLAYER_DPIP_ARTIFACT, 'utf8') }
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||||
const output = await build({ entryPoints: [getEntryFile(path.join(root, 'packages/artplayer-plugin-document-pip'))], bundle: true, write: false, format: 'cjs', target: 'es2020', plugins: [{
|
||||
name: 'dpip-less-inline',
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.less$/ }, async ({ path: filename }) => {
|
||||
const source = await fs.readFile(filename.replace(/\?inline$/, ''), 'utf8')
|
||||
return { contents: (await less.render(source)).css, loader: 'text' }
|
||||
})
|
||||
},
|
||||
}] })
|
||||
return { name: 'candidate-source', code: output.outputFiles[0].text }
|
||||
}
|
||||
|
||||
export async function dpipHistorical() {
|
||||
const contract = await verifyDpipContract()
|
||||
@@ -116,6 +139,8 @@ export function dpipEnvironment(implementation, settings = {}) {
|
||||
const controls = new Map()
|
||||
const proxies = []
|
||||
const rebinds = []
|
||||
const timers = new Map()
|
||||
let timerId = 0
|
||||
const window = { ...eventTarget(), document }
|
||||
document.defaultView = window
|
||||
function createWindow() {
|
||||
@@ -186,7 +211,18 @@ export function dpipEnvironment(implementation, settings = {}) {
|
||||
art.emit('destroy')
|
||||
},
|
||||
}
|
||||
const context = vm.createContext({ window, document, console: { warn: (...args) => warnings.push(args) }, module: { exports: {} } })
|
||||
const context = vm.createContext({ window, document, console: { warn: (...args) => warnings.push(args) }, module: { exports: {} }, setTimeout(callback, ms) {
|
||||
const id = timerId++
|
||||
timers.set(id, callback)
|
||||
sleeps.push(ms)
|
||||
const delay = settings.sleep ? settings.sleep(ms) : Promise.resolve()
|
||||
void delay.then(() => {
|
||||
const run = timers.get(id)
|
||||
timers.delete(id)
|
||||
run?.()
|
||||
})
|
||||
return id
|
||||
}, clearTimeout(id) { timers.delete(id) } })
|
||||
context.exports = context.module.exports
|
||||
const evaluate = () => vm.runInContext(`(() => { ${implementation.code}\n })()`, context, { timeout: 5000 })
|
||||
evaluate()
|
||||
@@ -208,6 +244,7 @@ export function dpipEnvironment(implementation, settings = {}) {
|
||||
proxies,
|
||||
subscriptions,
|
||||
rebinds,
|
||||
timers,
|
||||
evaluate,
|
||||
createWindow,
|
||||
async flush() {
|
||||
|
||||
Reference in new issue
Block a user