Three RED tests pin Pattern 4 (handshake) and Pattern 5 / Pitfall 4
(port reconnect on disconnect) contracts:
handshake.test.ts:
- 'sends OFFSCREEN_READY after listener registration' — exactly one
OFFSCREEN_READY emitted at module load, AFTER onMessage.addListener
port.test.ts:
- 'connects on module load' — chrome.runtime.connect called once
- 'reconnects when port disconnects' — firing onDisconnect triggers
immediate re-connect (Pitfall 4 idle-timer reset)
chrome.runtime is stubbed locally (no vitest-chrome dependency added).
No 'as any' / no '@ts-ignore'; casts are 'as unknown as T'.
Plan 04 must wire OFFSCREEN_READY send + port.connect({ name:
'video-keepalive' }) + onDisconnect-driven reconnect at the import-side
effect layer of src/offscreen/recorder.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
1.9 KiB
TypeScript
68 lines
1.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
interface PortStub {
|
|
name: string;
|
|
postMessage: ReturnType<typeof vi.fn>;
|
|
onMessage: { addListener: ReturnType<typeof vi.fn> };
|
|
onDisconnect: { addListener: ReturnType<typeof vi.fn> };
|
|
disconnect: ReturnType<typeof vi.fn>;
|
|
}
|
|
|
|
interface ChromeStub {
|
|
runtime: {
|
|
id: string;
|
|
sendMessage: (m: unknown) => void;
|
|
onMessage: { addListener: ReturnType<typeof vi.fn> };
|
|
connect: () => PortStub;
|
|
};
|
|
}
|
|
|
|
interface GlobalWithChrome {
|
|
chrome?: ChromeStub;
|
|
MediaRecorder?: { isTypeSupported: (mime: string) => boolean };
|
|
}
|
|
|
|
function buildChromeStub(calls: unknown[]): ChromeStub {
|
|
return {
|
|
runtime: {
|
|
id: 'ext-id-test',
|
|
sendMessage: (m: unknown) => {
|
|
calls.push(m);
|
|
},
|
|
onMessage: { addListener: vi.fn() },
|
|
connect: () => ({
|
|
name: 'video-keepalive',
|
|
postMessage: vi.fn(),
|
|
onMessage: { addListener: vi.fn() },
|
|
onDisconnect: { addListener: vi.fn() },
|
|
disconnect: vi.fn(),
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('OFFSCREEN_READY handshake', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
(globalThis as unknown as GlobalWithChrome).MediaRecorder = {
|
|
isTypeSupported: vi.fn().mockReturnValue(true),
|
|
};
|
|
});
|
|
|
|
it('sends OFFSCREEN_READY after listener registration', async () => {
|
|
const calls: unknown[] = [];
|
|
const stub = buildChromeStub(calls);
|
|
(globalThis as unknown as GlobalWithChrome).chrome = stub;
|
|
await import('../../src/offscreen/recorder');
|
|
expect(stub.runtime.onMessage.addListener).toHaveBeenCalled();
|
|
expect(calls).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ type: 'OFFSCREEN_READY' })])
|
|
);
|
|
const readyCount = calls.filter(
|
|
(m): m is { type: string } =>
|
|
typeof m === 'object' && m !== null && (m as { type?: unknown }).type === 'OFFSCREEN_READY'
|
|
).length;
|
|
expect(readyCount).toBe(1);
|
|
});
|
|
});
|