Files
ArtPlayer/test/dpip-lifecycle.test.js
T

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)
})