test(01-02): add RED handshake + port tests
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>
This commit is contained in:
89
tests/offscreen/port.test.ts
Normal file
89
tests/offscreen/port.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
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: (fn: () => void) => void };
|
||||
disconnect: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
|
||||
interface ChromeStub {
|
||||
runtime: {
|
||||
id: string;
|
||||
sendMessage: ReturnType<typeof vi.fn>;
|
||||
onMessage: { addListener: ReturnType<typeof vi.fn> };
|
||||
connect: () => PortStub;
|
||||
};
|
||||
}
|
||||
|
||||
interface GlobalWithChrome {
|
||||
chrome?: ChromeStub;
|
||||
MediaRecorder?: { isTypeSupported: (mime: string) => boolean };
|
||||
}
|
||||
|
||||
describe('port reconnect', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
(globalThis as unknown as GlobalWithChrome).MediaRecorder = {
|
||||
isTypeSupported: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
});
|
||||
|
||||
it('connects on module load', async () => {
|
||||
let connectCount = 0;
|
||||
const disconnectListeners: Array<() => void> = [];
|
||||
const stub: ChromeStub = {
|
||||
runtime: {
|
||||
id: 'ext-id-test',
|
||||
sendMessage: vi.fn(),
|
||||
onMessage: { addListener: vi.fn() },
|
||||
connect: () => {
|
||||
connectCount++;
|
||||
return {
|
||||
name: 'video-keepalive',
|
||||
postMessage: vi.fn(),
|
||||
onMessage: { addListener: vi.fn() },
|
||||
onDisconnect: {
|
||||
addListener: (fn: () => void) => disconnectListeners.push(fn),
|
||||
},
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
(globalThis as unknown as GlobalWithChrome).chrome = stub;
|
||||
await import('../../src/offscreen/recorder');
|
||||
expect(connectCount).toBe(1);
|
||||
});
|
||||
|
||||
it('reconnects when port disconnects', async () => {
|
||||
let connectCount = 0;
|
||||
const disconnectListeners: Array<() => void> = [];
|
||||
const stub: ChromeStub = {
|
||||
runtime: {
|
||||
id: 'ext-id-test',
|
||||
sendMessage: vi.fn(),
|
||||
onMessage: { addListener: vi.fn() },
|
||||
connect: () => {
|
||||
connectCount++;
|
||||
return {
|
||||
name: 'video-keepalive',
|
||||
postMessage: vi.fn(),
|
||||
onMessage: { addListener: vi.fn() },
|
||||
onDisconnect: {
|
||||
addListener: (fn: () => void) => disconnectListeners.push(fn),
|
||||
},
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
(globalThis as unknown as GlobalWithChrome).chrome = stub;
|
||||
await import('../../src/offscreen/recorder');
|
||||
expect(connectCount).toBe(1);
|
||||
// Fire the disconnect — module should reconnect
|
||||
disconnectListeners.forEach((fn) => fn());
|
||||
expect(connectCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user