Files
ArtPlayer/test/iframe.test.js
T

163 lines
7.4 KiB
JavaScript

import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Repository Node runner.
import test from 'node:test'
import { iframeEnvironment, iframeHistorical } from './helpers/iframe.js'
const implementations = (await iframeHistorical()).filter(item => item.global !== 'ArtplayerHelperIframe')
const flush = () => new Promise(resolve => setImmediate(resolve))
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
}
function setup(implementation, injected = true) {
const env = iframeEnvironment(implementation)
const instance = new env.Factory({ iframe: new env.Frame(), url: '/first' })
if (injected)
env.dispatch({ type: 'inject' })
return { ...env, instance }
}
for (const implementation of implementations) {
test(`Iframe ${implementation.name}: same-millisecond requests overwrite a pending callback`, async () => {
const env = setup(implementation)
const first = observe(env.instance.postMessage({ type: 'one', data: 1 }))
const second = observe(env.instance.postMessage({ type: 'two', data: 2 }))
assert.deepEqual(env.sent.map(item => item.packet.id), [1234, 1234])
assert.equal(Object.keys(env.instance.promises).length, 1)
env.dispatch({ type: 'response', data: 'first response', id: 1234 })
await flush()
assert.deepEqual(first, { status: 'pending' })
assert.deepEqual(second, { status: 'resolved', value: 'first response' })
env.instance.destroy()
})
test(`Iframe ${implementation.name}: destroy retains already-sent pending requests`, async () => {
const env = setup(implementation)
const state = observe(env.instance.postMessage({ type: 'one', data: 1 }))
env.instance.destroy()
await flush()
assert.equal(state.status, 'pending')
assert.deepEqual(Object.keys(env.instance.promises), ['1234'])
assert.equal(env.handlers.get('message').size, 0)
})
test(`Iframe ${implementation.name}: destroy leaves injection polling until the old timer runs`, async () => {
const env = setup(implementation, false)
const state = observe(env.instance.postMessage({ type: 'one', data: 1 }))
assert.equal(env.timers.size, 1)
env.instance.destroy()
await flush()
assert.equal(state.status, 'pending')
const [id, timer] = [...env.timers][0]
assert.equal(timer.delay, 200)
env.timers.delete(id)
timer.callback()
await flush()
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
assert.equal(env.timers.size, 0)
assert.equal(env.sent.length, 0)
})
test(`Iframe ${implementation.name}: requests started after destroy reject without a timer`, async () => {
const env = setup(implementation)
env.instance.destroy()
await assert.rejects(env.instance.postMessage({ type: 'late' }), /The instance has been destroyed/)
assert.equal(env.timers.size, 0)
assert.equal(env.sent.length, 0)
})
test(`Iframe ${implementation.name}: distinct IDs settle out of order and errors retain their message`, async () => {
const env = setup(implementation)
const first = observe(env.instance.postMessage({ type: 'one' }))
env.now(1235)
const second = observe(env.instance.postMessage({ type: 'two' }))
env.dispatch({ type: 'error', data: 'remote failure', id: 1235 })
env.dispatch({ type: 'response', data: 7, id: 1234 })
await flush()
assert.deepEqual(first, { status: 'resolved', value: 7 })
assert.deepEqual(second, { status: 'rejected', error: 'remote failure' })
assert.equal(Object.keys(env.instance.promises).length, 0)
env.instance.destroy()
})
test(`Iframe ${implementation.name}: custom non-error response types remain part of the generic protocol`, async () => {
const env = setup(implementation)
const result = env.instance.postMessage({ type: 'custom-query' })
env.dispatch({ type: 'custom-result', data: { ok: true }, id: 1234 })
assert.deepEqual(await result, { ok: true })
env.instance.destroy()
})
test(`Iframe ${implementation.name}: malformed message data throws from the public receiver`, () => {
const env = setup(implementation)
assert.throws(() => env.dispatch(null), error => error.name === 'TypeError')
env.instance.destroy()
})
test(`Iframe ${implementation.name}: immediate send failure rejects but leaves its public record`, async () => {
const env = setup(implementation)
env.instance.$iframe.contentWindow.postMessage = () => {
throw new Error('clone failure')
}
await assert.rejects(env.instance.postMessage({ type: 'one' }), /clone failure/)
assert.deepEqual(Object.keys(env.instance.promises), ['1234'])
env.instance.destroy()
})
test(`Iframe ${implementation.name}: send failure after polling escapes while its promise stays pending`, async () => {
const env = setup(implementation, false)
const state = observe(env.instance.postMessage({ type: 'one' }))
env.dispatch({ type: 'inject' })
env.instance.$iframe.contentWindow.postMessage = () => {
throw new Error('late clone failure')
}
const [id, timer] = [...env.timers][0]
env.timers.delete(id)
assert.throws(() => timer.callback(), /late clone failure/)
await flush()
assert.equal(state.status, 'pending')
assert.deepEqual(Object.keys(env.instance.promises), ['1234'])
env.instance.destroy()
})
test(`Iframe ${implementation.name}: saved message receiver can still settle and notify after destroy`, async () => {
const env = setup(implementation)
const state = observe(env.instance.postMessage({ type: 'one' }))
const callback = env.instance.onMessage
let calls = 0
env.instance.message(() => calls++)
env.instance.destroy()
callback({ data: { type: 'response', data: 'late', id: 1234 } })
await flush()
assert.deepEqual(state, { status: 'resolved', value: 'late' })
assert.equal(calls, 1)
})
test(`Iframe ${implementation.name}: navigation retains the previous injected state`, () => {
const env = setup(implementation)
env.instance.$iframe.src = '/second'
observe(env.instance.postMessage({ type: 'before-second-inject' }))
assert.equal(env.instance.injected, true)
assert.equal(env.sent.length, 1)
assert.equal(env.timers.size, 0)
assert.equal(env.instance.url, '/first')
env.instance.destroy()
})
test(`Iframe ${implementation.name}: repeated child inject repeats handshake but deduplicates its DOM listener`, () => {
const env = iframeEnvironment(implementation, true)
env.Factory.inject()
env.Factory.inject()
assert.deepEqual(env.sent.map(item => item.packet.type), ['inject', 'inject'])
assert.equal(env.handlers.get('message').size, 1)
})
test(`Iframe ${implementation.name}: child errors send a packet then reject and later commands still run`, async () => {
const env = iframeEnvironment(implementation, true)
await assert.rejects(env.Factory.onMessage({ data: { type: 'commit', data: 'throw new Error("child failure")', id: 1 } }), /child failure/)
await env.Factory.onMessage({ data: { type: 'commit', data: 'return 9', id: 2 } })
assert.deepEqual(JSON.parse(JSON.stringify(env.sent)), [{ packet: { type: 'error', data: 'child failure', id: 1 }, origin: '*' }, { packet: { type: 'response', data: 9, id: 2 }, origin: '*' }])
})
}