109 lines
3.6 KiB
TypeScript
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;
|
|
});
|
|
});
|