mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:17:46 -08:00
345 lines
13 KiB
JavaScript
345 lines
13 KiB
JavaScript
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)
|
|
})
|