Files
openmaic/OpenMAIC/tests/media/asset-replacement-events.test.ts
2026-08-16 14:58:47 +08:00

109 lines
3.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
__resetAssetReplacementChannelForTesting,
bindAssetReplacementChannel,
notifyAssetReplaced,
observeAssetReplacements,
type AssetReplacementPool,
} from '@/lib/media/asset-replacement-events';
const pool: AssetReplacementPool = {
invalidate: vi.fn(async () => {}),
resolve: vi.fn(async () => 'blob:refreshed'),
release: vi.fn(async () => {}),
};
/** Lets the channel's asynchronous delivery settle. */
const settle = () => new Promise((resolve) => setTimeout(resolve, 10));
describe('asset replacement events', () => {
afterEach(() => {
__resetAssetReplacementChannelForTesting();
vi.clearAllMocks();
});
it('notifies observers in the replacing realm', async () => {
const observed: string[] = [];
const stop = observeAssetReplacements((ref) => {
observed.push(ref);
});
await notifyAssetReplaced('ast_local', pool);
expect(observed).toEqual(['ast_local']);
stop();
});
it('receives peer replacements in a realm that never sends one', async () => {
// A passive tab only renders: it binds the channel when it starts observing,
// never through the sender path.
const refreshed: string[] = [];
const stop = observeAssetReplacements((ref) => {
refreshed.push(ref);
});
bindAssetReplacementChannel(() => pool);
const peerChannel = new BroadcastChannel('maic-asset-replacements');
peerChannel.postMessage('ast_from_peer');
await settle();
expect(refreshed).toEqual(['ast_from_peer']);
peerChannel.close();
stop();
});
it('keeps a failing observer from failing the committed replacement', async () => {
// The durable write has already committed by the time observers run.
const stopFailing = observeAssetReplacements(() => {
throw new Error('stale dynamic import');
});
const seen: string[] = [];
const stopHealthy = observeAssetReplacements((ref) => {
seen.push(ref);
});
try {
await expect(notifyAssetReplaced('ast_committed', pool)).resolves.toBeUndefined();
expect(seen).toEqual(['ast_committed']);
} finally {
stopFailing();
stopHealthy();
}
});
it('propagates a same-id replacement to a mounted consumer in another realm', async () => {
// Tab B: a consumer holding a lease, listening on its own channel instance.
// The module-local observer set is shared in-process, so the peer realm is
// modelled by a second channel that mirrors what tab B's module would do.
const refreshedInPeerTab: string[] = [];
const peerChannel = new BroadcastChannel('maic-asset-replacements');
peerChannel.onmessage = (event: MessageEvent) => {
if (typeof event.data === 'string') refreshedInPeerTab.push(event.data);
};
// Tab A: its module bound the channel on load, then it replaces the bytes.
bindAssetReplacementChannel(() => pool);
await notifyAssetReplaced('ast_shared', pool);
await settle();
expect(refreshedInPeerTab).toEqual(['ast_shared']);
peerChannel.close();
});
it('still notifies local observers when the channel is unavailable', async () => {
const original = globalThis.BroadcastChannel;
// @ts-expect-error - modelling an environment without the API
delete globalThis.BroadcastChannel;
const observed: string[] = [];
const stop = observeAssetReplacements((ref) => {
observed.push(ref);
});
await expect(notifyAssetReplaced('ast_no_channel', pool)).resolves.toBeUndefined();
expect(observed).toEqual(['ast_no_channel']);
stop();
globalThis.BroadcastChannel = original;
});
});