mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 17:46:17 -08:00
fix(iframe): [PKG-IFRAME-03] own pending requests and cancellation
This commit is contained in:
1 parent
b3f22652c2
commit
b8d03e7366
21 files changed
+3826
-107
No files matched your search
+48
-12
@@ -1,13 +1,20 @@
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { iframeHistorical } from '../helpers/iframe.js'
|
||||
import { iframeCandidate, iframeHistorical } from '../helpers/iframe.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementations = (await iframeHistorical()).filter(item => item.global !== 'ArtplayerHelperIframe')
|
||||
const candidate = { ...await iframeCandidate(), lifecycle: true }
|
||||
const implementations = process.env.ARTPLAYER_IFRAME_LIFECYCLE_ONLY === '1'
|
||||
? [candidate]
|
||||
: [...(await iframeHistorical()).filter(item => item.global !== 'ArtplayerHelperIframe'), candidate]
|
||||
|
||||
for (const implementation of implementations) {
|
||||
for (const relation of ['same-origin', 'cross-origin']) {
|
||||
for (const scenario of ['round-trip', 'same-tick', 'destroy-pending', 'foreign-message', 'navigation', 'clone-failure']) {
|
||||
test(`Iframe ${implementation.name}/${relation}: ${scenario} historical behavior`, async ({ page }, testInfo) => {
|
||||
const scenarios = implementation.lifecycle
|
||||
? ['round-trip', 'same-tick', 'destroy-pending', 'destroy-wait', 'clone-failure']
|
||||
: ['round-trip', 'same-tick', 'destroy-pending', 'foreign-message', 'navigation', 'clone-failure']
|
||||
for (const scenario of scenarios) {
|
||||
test(`Iframe ${implementation.name}/${relation}: ${scenario} ${implementation.lifecycle ? 'lifecycle acceptance' : 'historical behavior'}`, async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=published')
|
||||
const parentOrigin = new URL(page.url()).origin
|
||||
const alternate = parentOrigin.replace('127.0.0.1', 'localhost')
|
||||
@@ -33,7 +40,7 @@ for (const implementation of implementations) {
|
||||
</script>` }))
|
||||
await page.route('**/iframe-baseline-foreign', route => route.fulfill({ contentType: 'text/html', body: '<!doctype html><script>parent.postMessage({type:"inject",data:"foreign"},"*")</script>' }))
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.IframeFactory = module.exports.default || module.exports; })();` })
|
||||
const manualInjection = scenario === 'foreign-message' || scenario === 'clone-failure'
|
||||
const manualInjection = scenario === 'foreign-message' || scenario === 'clone-failure' || scenario === 'destroy-wait'
|
||||
const url = `${childOrigin}/iframe-baseline-child?auto=${manualInjection ? 0 : 1}&epoch=1`
|
||||
await page.evaluate((url) => {
|
||||
window.packets = []
|
||||
@@ -90,9 +97,11 @@ for (const implementation of implementations) {
|
||||
}
|
||||
finally { Date.now = now }
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.packets.filter(packet => packet.type === 'response' && packet.id === 1234).length)).toBe(2)
|
||||
await expect.poll(() => page.evaluate(() => window.packets.filter(packet => packet.type === 'response').length)).toBe(2)
|
||||
result = await page.evaluate(() => ({ states: window.states, pending: Object.keys(window.tool.promises).length }))
|
||||
expect(result).toEqual({ states: { first: { status: 'pending' }, second: { status: 'resolved', value: 1 } }, pending: 0 })
|
||||
expect(result).toEqual(implementation.lifecycle
|
||||
? { states: { first: { status: 'resolved', value: 1 }, second: { status: 'resolved', value: 2 } }, pending: 0 }
|
||||
: { states: { first: { status: 'pending' }, second: { status: 'resolved', value: 1 } }, pending: 0 })
|
||||
}
|
||||
else if (scenario === 'destroy-pending') {
|
||||
await page.evaluate(() => window.track('held', window.tool.postMessage({ type: 'hold' })))
|
||||
@@ -102,7 +111,22 @@ for (const implementation of implementations) {
|
||||
await child.evaluate(() => window.Tool.postMessage({ type: 'response', data: 'late', id: window.held.id }))
|
||||
await expect.poll(() => page.evaluate(() => window.packets.some(packet => packet.type === 'response' && packet.data === 'late'))).toBe(true)
|
||||
result = await page.evaluate(() => ({ destroyed: window.tool.destroyed, state: window.states.held, pending: Object.keys(window.tool.promises).length }))
|
||||
expect(result).toEqual({ destroyed: true, state: { status: 'pending' }, pending: 1 })
|
||||
expect(result).toEqual(implementation.lifecycle
|
||||
? { destroyed: true, state: { status: 'rejected', error: 'The instance has been destroyed' }, pending: 0 }
|
||||
: { destroyed: true, state: { status: 'pending' }, pending: 1 })
|
||||
}
|
||||
else if (scenario === 'destroy-wait') {
|
||||
result = await page.evaluate(async () => {
|
||||
window.track('waiting', window.tool.postMessage({ type: 'hold' }))
|
||||
window.tool.destroy()
|
||||
await Promise.resolve()
|
||||
return { state: window.states.waiting, pending: Object.keys(window.tool.promises).length }
|
||||
})
|
||||
expect(result).toEqual({ state: { status: 'rejected', error: 'The instance has been destroyed' }, pending: 0 })
|
||||
const child = page.frames().find(frame => frame.url() === url)
|
||||
await child.evaluate(() => window.Tool.inject())
|
||||
await expect.poll(() => page.evaluate(() => window.packets.some(packet => packet.type === 'inject'))).toBe(true)
|
||||
expect(await page.evaluate(() => window.tool.injected)).toBe(false)
|
||||
}
|
||||
else if (scenario === 'foreign-message') {
|
||||
expect(await page.evaluate(() => window.tool.injected)).toBe(false)
|
||||
@@ -143,9 +167,12 @@ for (const implementation of implementations) {
|
||||
})
|
||||
const child = page.frames().find(frame => frame.url() === url)
|
||||
await child.evaluate(() => window.Tool.inject())
|
||||
await expect.poll(() => page.evaluate(() => window.cloneErrors.length)).toBe(1)
|
||||
if (implementation.lifecycle)
|
||||
await expect.poll(() => page.evaluate(() => window.states.deferredClone.status)).toBe('rejected')
|
||||
else
|
||||
await expect.poll(() => page.evaluate(() => window.cloneErrors.length)).toBe(1)
|
||||
result = await page.evaluate(async () => {
|
||||
const deferredId = Object.keys(window.tool.promises)[0]
|
||||
const deferredId = Object.keys(window.tool.promises)[0] || Date.now()
|
||||
const now = Date.now
|
||||
Date.now = () => Number(deferredId) + 1
|
||||
let failure
|
||||
@@ -156,7 +183,16 @@ for (const implementation of implementations) {
|
||||
finally { Date.now = now }
|
||||
return { failure, deferred: window.states.deferredClone, pending: Object.keys(window.tool.promises).length, uncaught: window.cloneErrors }
|
||||
})
|
||||
expect(result).toEqual({ failure: 'DataCloneError', deferred: { status: 'pending' }, pending: 2, uncaught: ['DataCloneError'] })
|
||||
if (implementation.lifecycle) {
|
||||
expect(result.failure).toBe('DataCloneError')
|
||||
expect(result.deferred.status).toBe('rejected')
|
||||
expect(result.deferred.error).toMatch(/clon/i)
|
||||
expect(result.pending).toBe(0)
|
||||
expect(result.uncaught).toEqual([])
|
||||
}
|
||||
else {
|
||||
expect(result).toEqual({ failure: 'DataCloneError', deferred: { status: 'pending' }, pending: 2, uncaught: ['DataCloneError'] })
|
||||
}
|
||||
}
|
||||
else {
|
||||
const nextUrl = `${childOrigin}/iframe-baseline-child?auto=0&epoch=2`
|
||||
@@ -177,7 +213,7 @@ for (const implementation of implementations) {
|
||||
}
|
||||
const packets = await page.evaluate(() => window.packets)
|
||||
expect(packets.filter(packet => packet.fromChild).every(packet => packet.origin === childOrigin)).toBe(true)
|
||||
await testInfo.attach('iframe-behavior', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), relation, scenario, parentOrigin, childOrigin, foreignOrigin, result, packets, scope: 'Actual browser iframe WindowProxy/source/origin and postMessage. Local route fulfills immutable library bytes. Date.now is controlled for collision and distinct clone-failure IDs. Only the deliberate child error/DataCloneError is intercepted; known historical defects are asserted, not fixed or waived.' }) })
|
||||
await testInfo.attach('iframe-behavior', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), relation, scenario, parentOrigin, childOrigin, foreignOrigin, result, packets, lifecycleAcceptance: Boolean(implementation.lifecycle), scope: 'Actual browser iframe WindowProxy/source/origin and postMessage. Local route fulfills immutable library bytes. Date.now is controlled for collision and distinct clone-failure IDs. Only the deliberate child error/DataCloneError is intercepted. Historical defects are asserted for historical rows; lifecycle acceptance requires cleanup and absence of uncaught clone errors. Navigation and trust are not candidate acceptance yet.' }) })
|
||||
}
|
||||
finally {
|
||||
await page.evaluate(() => {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { verifyIframeContract } from '../../refactor/scripts/iframe-contract.mjs'
|
||||
import { readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from './load.js'
|
||||
|
||||
export async function iframeCandidate() {
|
||||
if (process.env.ARTPLAYER_IFRAME_BASELINE === '1')
|
||||
return (await iframeHistorical()).find(item => item.name === 'workspace.js')
|
||||
return { name: process.env.ARTPLAYER_IFRAME_ARTIFACT ? 'candidate-artifact' : 'candidate-source', global: 'ArtplayerToolIframe', namespace: false, code: process.env.ARTPLAYER_IFRAME_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_IFRAME_ARTIFACT, 'utf8') : await compilePackage('artplayer-tool-iframe', 'umd') }
|
||||
}
|
||||
|
||||
export async function iframeHistorical() {
|
||||
const { baseline, archive, sources } = await verifyIframeContract()
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Candidate assertions also run unchanged against the frozen workspace.
|
||||
import test from 'node:test'
|
||||
import { iframeCandidate, iframeEnvironment } from './helpers/iframe.js'
|
||||
|
||||
const implementation = await iframeCandidate()
|
||||
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(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 }
|
||||
}
|
||||
function tick(env) {
|
||||
for (const [id, timer] of [...env.timers]) {
|
||||
assert.equal(timer.delay, 200)
|
||||
env.timers.delete(id)
|
||||
timer.callback()
|
||||
}
|
||||
}
|
||||
|
||||
test('Iframe candidate preserves instance shape, descriptors, binding and listener-before-src order', () => {
|
||||
const env = setup(false)
|
||||
const instance = env.instance
|
||||
assert.deepEqual(Object.getOwnPropertyNames(env.Factory.prototype), ['constructor', 'onMessage', 'postMessage', 'commit', 'message', 'destroy'])
|
||||
assert.deepEqual(Object.keys(instance), ['url', '$iframe', 'promises', 'injected', 'destroyed', 'messageCallback', 'onMessage'])
|
||||
for (const key of Object.keys(instance)) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(instance, key)
|
||||
assert(descriptor.writable && descriptor.enumerable && descriptor.configurable)
|
||||
}
|
||||
assert.deepEqual(env.order, ['listen:message', 'src'])
|
||||
assert.equal(instance.messageCallback(), null)
|
||||
const receiver = instance.onMessage
|
||||
receiver({ data: { type: 'inject' } })
|
||||
assert.equal(instance.injected, true)
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves numeric envelopes, resove spelling and generic response callback receiver', async () => {
|
||||
const env = setup()
|
||||
const registry = env.instance.promises
|
||||
const seen = []
|
||||
env.instance.message(function (packet) {
|
||||
seen.push({ receiver: this === env.instance, ...packet })
|
||||
})
|
||||
const pending = env.instance.postMessage({ type: 'custom-query', data: 3, id: 900 })
|
||||
assert.deepEqual(Object.keys(registry[1234]), ['resove', 'reject'])
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(env.sent)), [{ packet: { type: 'custom-query', data: 3, id: 1234 }, origin: '*' }])
|
||||
env.dispatch({ type: 'custom-result', data: 4, id: 1234 })
|
||||
assert.equal(await pending, 4)
|
||||
assert.equal(registry, env.instance.promises)
|
||||
assert.deepEqual(Object.keys(registry), [])
|
||||
assert.deepEqual(seen, [{ receiver: true, type: 'custom-result', data: 4 }])
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate matches simultaneous requests independently and out of order', async () => {
|
||||
const env = setup()
|
||||
const first = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const second = observe(env.instance.postMessage({ type: 'two' }))
|
||||
assert.deepEqual(env.sent.map(item => item.packet.id), [1234, 1235])
|
||||
env.dispatch({ type: 'response', data: 2, id: 1235 })
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
await flush()
|
||||
assert.deepEqual([first, second], [{ status: 'resolved', value: 1 }, { status: 'resolved', value: 2 }])
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate never reuses a settled ID when the clock moves backwards', async () => {
|
||||
const env = setup()
|
||||
const first = env.instance.postMessage({ type: 'one' })
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
assert.equal(await first, 1)
|
||||
env.now(1000)
|
||||
const second = observe(env.instance.postMessage({ type: 'two' }))
|
||||
assert.equal(env.sent.at(-1).packet.id, 1235)
|
||||
env.dispatch({ type: 'response', data: 'duplicate', id: 1234 })
|
||||
await flush()
|
||||
assert.equal(second.status, 'pending')
|
||||
env.dispatch({ type: 'response', data: 2, id: 1235 })
|
||||
await flush()
|
||||
assert.deepEqual(second, { status: 'resolved', value: 2 })
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate separates two instances sharing the same iframe window', async () => {
|
||||
const env = setup()
|
||||
const second = new env.Factory({ iframe: env.instance.$iframe, url: '/first' })
|
||||
env.dispatch({ type: 'inject' })
|
||||
const one = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const two = observe(second.postMessage({ type: 'two' }))
|
||||
assert.deepEqual(env.sent.map(item => item.packet.id), [1234, 1235])
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
env.dispatch({ type: 'response', data: 2, id: 1235 })
|
||||
await flush()
|
||||
assert.deepEqual([one, two], [{ status: 'resolved', value: 1 }, { status: 'resolved', value: 2 }])
|
||||
env.instance.destroy()
|
||||
second.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate destroys sent requests immediately and ignores saved receivers', async () => {
|
||||
const env = setup()
|
||||
const state = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const receiver = env.instance.onMessage
|
||||
let calls = 0
|
||||
env.instance.message(() => calls++)
|
||||
env.instance.destroy()
|
||||
receiver({ data: { type: 'response', data: 'late', id: 1234 } })
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(env.handlers.get('message').size, 0)
|
||||
assert.equal(env.instance.destroy(), undefined)
|
||||
})
|
||||
|
||||
test('Iframe candidate destroys waiting requests immediately and cancels every polling timer', async () => {
|
||||
const env = setup(false)
|
||||
const states = Array.from({ length: 3 }, () => observe(env.instance.postMessage({ type: 'wait' })))
|
||||
const late = [...env.timers.values()].map(timer => timer.callback)
|
||||
env.instance.destroy()
|
||||
await flush()
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert(states.every(state => state.status === 'rejected' && state.error === 'The instance has been destroyed'))
|
||||
late.forEach(callback => callback())
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves 200ms injection waiting and allocates unique IDs after handshake', async () => {
|
||||
const env = setup(false)
|
||||
const one = observe(env.instance.postMessage({ type: 'one' }))
|
||||
const two = observe(env.instance.postMessage({ type: 'two' }))
|
||||
tick(env)
|
||||
assert.equal(env.sent.length, 0)
|
||||
assert.equal(env.timers.size, 2)
|
||||
env.dispatch({ type: 'inject' })
|
||||
assert.equal(env.sent.length, 0)
|
||||
tick(env)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.deepEqual(env.sent.map(item => item.packet.id), [1234, 1235])
|
||||
env.dispatch({ type: 'response', data: 1, id: 1234 })
|
||||
env.dispatch({ type: 'error', data: 'remote failure', id: 1235 })
|
||||
await flush()
|
||||
assert.deepEqual([one, two], [{ status: 'resolved', value: 1 }, { status: 'rejected', error: 'remote failure' }])
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
for (const deferred of [false, true]) {
|
||||
test(`Iframe candidate clears ${deferred ? 'deferred' : 'immediate'} send failure and preserves the exact rejection`, async () => {
|
||||
const env = setup(!deferred)
|
||||
const failure = new Error('clone failure')
|
||||
env.instance.$iframe.contentWindow.postMessage = () => {
|
||||
throw failure
|
||||
}
|
||||
const promise = env.instance.postMessage({ type: 'query' })
|
||||
const rejected = assert.rejects(promise, error => error === failure)
|
||||
if (deferred) {
|
||||
env.dispatch({ type: 'inject' })
|
||||
tick(env)
|
||||
}
|
||||
await rejected
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
assert.equal(env.timers.size, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
test('Iframe candidate rejects after destroy without allocating a request or timer', async () => {
|
||||
const env = setup(false)
|
||||
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)
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
})
|
||||
|
||||
test('Iframe candidate permits public resove/reject callbacks and releases their owned requests', async () => {
|
||||
const env = setup()
|
||||
const first = env.instance.postMessage({ type: 'one' })
|
||||
env.instance.promises[1234].resove(5)
|
||||
assert.equal(await first, 5)
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
const second = env.instance.postMessage({ type: 'two' })
|
||||
const rejected = assert.rejects(second, /external rejection/)
|
||||
env.instance.promises[env.sent.at(-1).packet.id].reject(new Error('external rejection'))
|
||||
await rejected
|
||||
assert.equal(Object.keys(env.instance.promises).length, 0)
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate cancels owned promises even when public records were removed by a consumer', async () => {
|
||||
const env = setup()
|
||||
const state = observe(env.instance.postMessage({ type: 'one' }))
|
||||
delete env.instance.promises[1234]
|
||||
env.instance.destroy()
|
||||
await flush()
|
||||
assert.deepEqual(state, { status: 'rejected', error: 'The instance has been destroyed' })
|
||||
})
|
||||
|
||||
test('Iframe candidate ignores inherited request keys without changing generic message callbacks', () => {
|
||||
const env = setup()
|
||||
const seen = []
|
||||
env.instance.message(packet => seen.push(packet.type))
|
||||
assert.doesNotThrow(() => env.dispatch({ type: 'custom', id: 'toString' }))
|
||||
assert.deepEqual(seen, ['custom'])
|
||||
env.instance.promises[7] = undefined
|
||||
assert.doesNotThrow(() => env.dispatch({ type: 'empty', id: 7 }))
|
||||
assert.deepEqual(seen, ['custom', 'empty'])
|
||||
env.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves commit serialization, synchronous response and async resolver behavior', async () => {
|
||||
const parent = setup()
|
||||
const promise = parent.instance.commit(() => {
|
||||
return 7
|
||||
})
|
||||
const packet = parent.sent[0].packet
|
||||
assert.equal(packet.data.trim(), 'return 7')
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
const handling = child.Factory.onMessage({ data: packet })
|
||||
assert.equal(child.sent[0].packet.data, 7)
|
||||
await handling
|
||||
parent.dispatch(child.sent[0].packet)
|
||||
assert.equal(await promise, 7)
|
||||
const async = child.Factory.onMessage({ data: { type: 'commit', data: 'resolve(8)', id: 18 } })
|
||||
assert.equal(child.sent.length, 1)
|
||||
await async
|
||||
assert.equal(child.sent[1].packet.data, 8)
|
||||
parent.instance.destroy()
|
||||
})
|
||||
|
||||
test('Iframe candidate preserves child errors, injection repetition and argument validation', async () => {
|
||||
const parent = setup()
|
||||
assert.throws(() => new parent.Factory({ iframe: {}, url: '/' }), /needs to be a HTMLIFrameElement/)
|
||||
assert.throws(() => parent.instance.commit(null), /needs to be a function/)
|
||||
assert.throws(() => parent.instance.message(null), /needs to be a function/)
|
||||
assert.throws(() => parent.Factory.inject(), /can only be used in iframe/)
|
||||
const child = iframeEnvironment(implementation, true)
|
||||
child.Factory.inject()
|
||||
child.Factory.inject()
|
||||
assert.equal(child.handlers.get('message').size, 1)
|
||||
assert.deepEqual(child.sent.map(item => item.packet.id), [0, 0])
|
||||
await assert.rejects(child.Factory.onMessage({ data: { type: 'commit', data: 'throw new Error("child failure")', id: 19 } }), /child failure/)
|
||||
assert.equal(child.sent.at(-1).packet.type, 'error')
|
||||
assert.equal(child.sent.at(-1).packet.data, 'child failure')
|
||||
parent.instance.destroy()
|
||||
})
|
||||
Reference in new issue
Block a user