mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 09:36:16 -08:00
fix(iframe): [PKG-IFRAME-03] release failed setup and bind message peers
This commit is contained in:
1 parent
b8d03e7366
commit
b8cdafedd1
23 files changed
+7940
-32
No files matched your search
@@ -0,0 +1,190 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Real source compiled into an isolated window for ownership and protocol boundaries.
|
||||
import test from 'node:test'
|
||||
import { iframeCandidate, iframeEnvironment } from './helpers/iframe.js'
|
||||
|
||||
const implementation = await iframeCandidate()
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve))
|
||||
function setup() {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const frame = new env.Frame()
|
||||
const instance = new env.Factory({ iframe: frame, url: '/first' })
|
||||
return { ...env, frame, instance }
|
||||
}
|
||||
function observe(promise) {
|
||||
const state = { status: 'pending' }
|
||||
promise.then(value => Object.assign(state, { status: 'resolved', value }), error => Object.assign(state, { status: 'rejected', error: error.message }))
|
||||
return state
|
||||
}
|
||||
|
||||
test('Iframe source setter failure releases its bound listener and preserves the exact setup error', () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const frame = new env.Frame()
|
||||
const failure = new Error('src assignment rejected')
|
||||
Object.defineProperty(frame, 'src', {
|
||||
get() { return '/' },
|
||||
set() {
|
||||
throw failure
|
||||
},
|
||||
})
|
||||
assert.throws(() => new env.Factory({ iframe: frame, url: '/' }), error => error === failure)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
|
||||
test('Iframe partial listener acquisition failure releases ownership and keeps the first error', () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const add = env.box.addEventListener
|
||||
const failure = new Error('registration rejected after acquiring')
|
||||
env.box.addEventListener = (...args) => {
|
||||
add(...args)
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => new env.Factory({ iframe: new env.Frame(), url: '/' }), error => error === failure)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
assert.equal(env.order.includes('src'), false)
|
||||
})
|
||||
|
||||
test('Iframe setup keeps the source error when releasing a partially constructed instance also throws', () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const frame = new env.Frame()
|
||||
const failure = new Error('original source error')
|
||||
Object.defineProperty(frame, 'src', {
|
||||
get() { return '/' },
|
||||
set() {
|
||||
throw failure
|
||||
},
|
||||
})
|
||||
const remove = env.box.removeEventListener
|
||||
let removals = 0
|
||||
env.box.removeEventListener = (...args) => {
|
||||
removals++
|
||||
remove(...args)
|
||||
throw new Error('secondary release error')
|
||||
}
|
||||
assert.throws(() => new env.Factory({ iframe: frame, url: '/' }), error => error === failure)
|
||||
assert.equal(removals, 1)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
})
|
||||
|
||||
test('Iframe destroy cancels requests even if listener release throws and does not repeat release', async () => {
|
||||
const env = setup()
|
||||
const state = observe(env.instance.postMessage({ type: 'waiting' }))
|
||||
const failure = new Error('listener release failure')
|
||||
const remove = env.box.removeEventListener
|
||||
let removals = 0
|
||||
env.box.removeEventListener = (...args) => {
|
||||
removals++
|
||||
remove(...args)
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => env.instance.destroy(), error => error === failure)
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
assert.doesNotThrow(() => env.instance.destroy())
|
||||
assert.equal(removals, 1)
|
||||
})
|
||||
|
||||
for (const injected of [false, true]) {
|
||||
test(`Iframe failed construction cancels a reentrant ${injected ? 'sent' : 'waiting'} request`, async () => {
|
||||
const env = iframeEnvironment(implementation)
|
||||
const frame = new env.Frame()
|
||||
const failure = new Error('source setter failed after reentry')
|
||||
const receive = env.Factory.prototype.onMessage
|
||||
let state
|
||||
let partial
|
||||
env.Factory.prototype.onMessage = function (event) {
|
||||
partial = this
|
||||
receive.call(this, event)
|
||||
state = observe(this.postMessage({ type: 'query' }))
|
||||
}
|
||||
Object.defineProperty(frame, 'src', {
|
||||
get() { return '/' },
|
||||
set() {
|
||||
env.dispatch({ type: injected ? 'inject' : 'custom' }, frame.contentWindow)
|
||||
throw failure
|
||||
},
|
||||
})
|
||||
assert.throws(() => new env.Factory({ iframe: frame, url: '/' }), error => error === failure)
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
|
||||
assert.equal(partial.destroyed, true)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(Object.keys(partial.promises).length, 0)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('Iframe parent rejects foreign handshakes, responses and callbacks but accepts its selected cross-origin peer', async () => {
|
||||
const env = setup()
|
||||
const seen = []
|
||||
env.instance.message(packet => seen.push(packet.type))
|
||||
env.dispatch({ type: 'inject' }, {})
|
||||
assert.equal(env.instance.injected, false)
|
||||
assert.deepEqual(seen, [])
|
||||
env.dispatch({ type: 'inject' }, env.frame.contentWindow, 'https://redirected.example')
|
||||
const state = observe(env.instance.postMessage({ type: 'query' }))
|
||||
env.dispatch({ type: 'custom-result', data: 'forged', id: 1234 }, {})
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'pending' })
|
||||
assert.deepEqual(seen, ['inject'])
|
||||
env.dispatch({ type: 'custom-result', data: 'real', id: 1234 }, env.frame.contentWindow, 'null')
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'resolved', value: 'real' })
|
||||
assert.deepEqual(seen, ['inject', 'custom-result'])
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe child rejects a foreign executable command and accepts only its actual parent peer', async () => {
|
||||
const env = iframeEnvironment(implementation, true)
|
||||
env.box.executions = 0
|
||||
const data = { type: 'commit', data: 'window.executions++; return 7', id: 17 }
|
||||
await env.Factory.onMessage({ data, source: {}, origin: 'https://sibling.example' })
|
||||
assert.equal(env.box.executions, 0)
|
||||
assert.equal(env.sent.length, 0)
|
||||
await env.Factory.onMessage({ data, source: env.box.parent, origin: 'https://parent.example' })
|
||||
assert.equal(env.box.executions, 1)
|
||||
assert.equal(env.sent[0].packet.data, 7)
|
||||
})
|
||||
|
||||
for (const data of [null, undefined, false, 3, 'message', {}, { type: null }, { type: 3 }]) {
|
||||
test(`Iframe receivers ignore malformed payload ${JSON.stringify(data)} without callbacks or child rejection`, async () => {
|
||||
const env = setup()
|
||||
let calls = 0
|
||||
env.instance.message(() => calls++)
|
||||
assert.doesNotThrow(() => env.dispatch(data, env.frame.contentWindow))
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(env.instance.injected, false)
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
await assert.doesNotReject(child.Factory.onMessage({ data, source: child.box.parent }))
|
||||
assert.equal(child.sent.length, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
test('Iframe public receivers keep source-less and null-source direct invocation as a local API', async () => {
|
||||
const env = setup()
|
||||
env.instance.onMessage({ data: { type: 'inject' } })
|
||||
const promise = env.instance.postMessage({ type: 'query' })
|
||||
env.instance.onMessage({ data: { type: 'custom', data: 9, id: 1234 }, source: null })
|
||||
assert.equal(await promise, 9)
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
await child.Factory.onMessage({ data: { type: 'commit', data: 'return 8', id: 9 }, source: null })
|
||||
assert.equal(child.sent[0].packet.data, 8)
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe trusted protocol retains generic message types and child commit error serialization', async () => {
|
||||
const env = setup()
|
||||
const seen = []
|
||||
env.instance.message(packet => seen.push({ ...packet }))
|
||||
env.dispatch({ type: '', data: 'custom empty type' }, env.frame.contentWindow)
|
||||
assert.deepEqual(seen, [{ type: '', data: 'custom empty type' }])
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
await assert.rejects(child.Factory.onMessage({ data: { type: 'commit', data: 3, id: 7 }, source: child.box.parent }), error => error.name === 'TypeError')
|
||||
assert.equal(child.sent[0].packet.type, 'error')
|
||||
assert.equal(child.sent[0].packet.id, 7)
|
||||
env.instance.destroy()
|
||||
})
|
||||
Reference in new issue
Block a user