import { EnvironmentId, ThreadId } from "@t3tools/contracts";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const { events, frameSubscription, onFrame, registrySet, save, startScreencast, stopScreencast, surfaceState,} = vi.hoisted(() => { const events: string[] = []; type Frame = { readonly tabId: string; readonly data: string; readonly width: number; readonly height: number; readonly receivedAt: string; }; const frameSubscription: { listener: ((frame: Frame) => void) | null } = { listener: null, }; const surfaceState = { byTabId: {} as Record<string, unknown>, }; return { events, frameSubscription, onFrame: vi.fn((listener: (frame: Frame) => void) => { frameSubscription.listener = listener; return () => { if (frameSubscription.listener === listener) frameSubscription.listener = null; }; }), registrySet: vi.fn((_atom: unknown, value: { readonly tabIds: ReadonlySet<string> }) => { events.push( value.tabIds.size === 0 ? "clear" : `publish:${Array.from(value.tabIds).join(",")}`, ); }), save: vi.fn(async (tabId: string) => ({ id: "recording-test", tabId, path: "/tmp/recording-test.webm", mimeType: "video/webm" as const, sizeBytes: 0, createdAt: "2026-06-26T00:00:00.000Z", })), startScreencast: vi.fn(async (tabId: string) => { events.push("start-screencast"); const surface = surfaceState.byTabId[tabId] as | { readonly content?: { readonly width: number; readonly height: number }; readonly rect?: { readonly width: number; readonly height: number }; } | undefined; const size = surface?.content ?? surface?.rect; frameSubscription.listener?.({ tabId, data: "initial-frame", width: size?.width ?? 1280, height: size?.height ?? 800, receivedAt: "2026-06-26T00:00:00.000Z", }); }), stopScreencast: vi.fn(async () => undefined), surfaceState, };}); vi.mock("~/components/preview/previewBridge", () => ({ previewBridge: { recording: { onFrame, save, startScreencast, stopScreencast }, },})); vi.mock("~/rpc/atomRegistry", () => ({ appAtomRegistry: { set: registrySet },})); vi.mock("./browserSurfaceStore", () => ({ useBrowserSurfaceStore: { getState: () => surfaceState, },})); import { BROWSER_RECORDING_FIRST_FRAME_SIZE_TIMEOUT_MS, BROWSER_RECORDING_STARTUP_SETTLE_TIMEOUT_MS, BrowserRecordingConflictError, findActiveBrowserRecordingRuntimeTabId, readActiveBrowserRecordingTabIds, readActiveBrowserRecordingTargets, startBrowserRecording, stopBrowserRecording,} from "./browserRecording";import { previewRuntimeTabId } from "./previewRuntimeTabId"; class FakeMediaRecorder { static isTypeSupported(): boolean { return true; } state: RecordingState = "inactive"; private readonly listeners = new Map<string, Set<EventListenerOrEventListenerObject>>(); addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { const listeners = this.listeners.get(type) ?? new Set(); listeners.add(listener); this.listeners.set(type, listeners); } start(): void { this.state = "recording"; } stop(): void { this.state = "inactive"; for (const listener of this.listeners.get("stop") ?? []) { if (typeof listener === "function") listener(new Event("stop")); else listener.handleEvent(new Event("stop")); } }} const emitRecordingFrame = () => { frameSubscription.listener?.({ tabId: "recording-tab", data: "startup-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:00.000Z", });}; describe("browser recording", () => { beforeEach(() => { events.length = 0; frameSubscription.listener = null; surfaceState.byTabId = { "recording-tab": { visible: true, rect: { x: 0, y: 0, width: 800, height: 600 }, content: { x: 0, y: 0, width: 800, height: 600, scale: 1, scrollLeft: 0, scrollTop: 0 }, }, }; vi.clearAllMocks(); vi.stubGlobal("window", globalThis); vi.stubGlobal("MediaRecorder", FakeMediaRecorder as unknown as typeof MediaRecorder); class ImmediateImage { private loadListener: EventListenerOrEventListenerObject | undefined; addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { if (type === "load") this.loadListener = listener; } set src(_value: string) { const event = new Event("load"); if (typeof this.loadListener === "function") this.loadListener(event); else this.loadListener?.handleEvent(event); } } vi.stubGlobal("Image", ImmediateImage as unknown as typeof Image); vi.stubGlobal("document", { createElement: () => ({ width: 0, height: 0, captureStream: () => ({}), getContext: () => ({ drawImage: vi.fn(), fillRect: vi.fn(), fillStyle: "" }), }), }); }); afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); }); it("starts recording for a visible tab", async () => { await startBrowserRecording("recording-tab"); expect(events).toEqual(["start-screencast", "publish:recording-tab"]); await stopBrowserRecording("recording-tab"); }); it("records a hidden tab without requiring it to become visible", async () => { surfaceState.byTabId = { "recording-tab": { visible: false, rect: { x: 0, y: 0, width: 800, height: 600 }, content: { x: 0, y: 0, width: 800, height: 600, scale: 1, scrollLeft: 0, scrollTop: 0 }, }, }; await startBrowserRecording("recording-tab"); expect(startScreencast).toHaveBeenCalledWith("recording-tab"); expect(events).toEqual(["start-screencast", "publish:recording-tab"]); await stopBrowserRecording("recording-tab"); }); it("fails startup instead of locking a fallback size when no frame arrives", async () => { vi.useFakeTimers(); startScreencast.mockImplementationOnce(async () => { events.push("start-screencast"); }); const startPromise = startBrowserRecording("recording-tab"); const rejection = expect(startPromise).rejects.toMatchObject({ operation: "wait-first-frame", tabId: "recording-tab", }); await Promise.resolve(); await vi.advanceTimersByTimeAsync(BROWSER_RECORDING_FIRST_FRAME_SIZE_TIMEOUT_MS); await rejection; expect(stopScreencast).toHaveBeenCalledWith("recording-tab"); expect(events.at(-1)).toBe("clear"); }); it("fixes hidden recording dimensions before MediaRecorder starts", async () => { const drawImage = vi.fn(); const fillRect = vi.fn(); let capturedStreamSize: { readonly width: number; readonly height: number } | undefined; const canvas = { width: 0, height: 0, captureStream: () => { capturedStreamSize = { width: canvas.width, height: canvas.height }; return {}; }, getContext: () => ({ drawImage, fillRect, fillStyle: "" }), }; vi.stubGlobal("document", { createElement: () => canvas, }); surfaceState.byTabId = {}; startScreencast.mockImplementationOnce(async (tabId: string) => { events.push("start-screencast"); frameSubscription.listener?.({ tabId, data: "captured-frame", width: 390, height: 844, receivedAt: "2026-06-26T00:00:00.000Z", }); }); await startBrowserRecording("recording-tab"); expect(canvas).toMatchObject({ width: 390, height: 844 }); expect(capturedStreamSize).toEqual({ width: 390, height: 844 }); expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 390, 844); frameSubscription.listener?.({ tabId: "recording-tab", data: "different-sized-frame", width: 1280, height: 720, receivedAt: "2026-06-26T00:00:01.000Z", }); expect(canvas).toMatchObject({ width: 390, height: 844 }); expect(fillRect).toHaveBeenLastCalledWith(0, 0, 390, 844); await stopBrowserRecording("recording-tab"); }); it("draws the newest decoded frames without starving behind decode latency", async () => { const drawImage = vi.fn(); class DeferredImage { static readonly instances: DeferredImage[] = []; private loadListener: EventListenerOrEventListenerObject | undefined; constructor() { DeferredImage.instances.push(this); } addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { if (type === "load") this.loadListener = listener; } set src(_value: string) {} finishLoading(): void { const event = new Event("load"); if (typeof this.loadListener === "function") this.loadListener(event); else this.loadListener?.handleEvent(event); } } vi.stubGlobal("Image", DeferredImage as unknown as typeof Image); vi.stubGlobal("document", { createElement: () => ({ width: 0, height: 0, captureStream: () => ({}), getContext: () => ({ drawImage, fillRect: vi.fn(), fillStyle: "" }), }), }); await startBrowserRecording("recording-tab"); frameSubscription.listener?.({ tabId: "recording-tab", data: "second-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:01.000Z", }); frameSubscription.listener?.({ tabId: "recording-tab", data: "third-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:02.000Z", }); DeferredImage.instances[1]?.finishLoading(); expect(drawImage).toHaveBeenCalledOnce(); DeferredImage.instances[2]?.finishLoading(); expect(drawImage).toHaveBeenCalledTimes(2); DeferredImage.instances[0]?.finishLoading(); expect(drawImage).toHaveBeenCalledTimes(2); await stopBrowserRecording("recording-tab"); }); it("records separate tabs concurrently", async () => { const firstThreadRef = { environmentId: EnvironmentId.make("environment-recording"), threadId: ThreadId.make("thread-recording-first"), }; const secondThreadRef = { environmentId: EnvironmentId.make("environment-recording"), threadId: ThreadId.make("thread-recording-second"), }; surfaceState.byTabId = { ...surfaceState.byTabId, "recording-tab-2": { visible: false, rect: { x: 0, y: 0, width: 390, height: 844 }, content: { x: 0, y: 0, width: 390, height: 844, scale: 1, scrollLeft: 0, scrollTop: 0 }, }, }; await Promise.all([ startBrowserRecording("recording-tab", firstThreadRef), startBrowserRecording("recording-tab-2", secondThreadRef), ]); expect(startScreencast).toHaveBeenCalledTimes(2); expect(onFrame).toHaveBeenCalledOnce(); expect(events).toContain("publish:recording-tab,recording-tab-2"); expect(readActiveBrowserRecordingTabIds()).toEqual( new Set(["recording-tab", "recording-tab-2"]), ); expect(readActiveBrowserRecordingTabIds(firstThreadRef)).toEqual(new Set(["recording-tab"])); expect(readActiveBrowserRecordingTabIds(secondThreadRef)).toEqual(new Set(["recording-tab-2"])); await stopBrowserRecording("recording-tab"); expect(readActiveBrowserRecordingTabIds()).toEqual(new Set(["recording-tab-2"])); await stopBrowserRecording("recording-tab-2"); expect(readActiveBrowserRecordingTabIds()).toEqual(new Set()); expect(save).toHaveBeenCalledTimes(2); }); it("keeps a recording reachable through its runtime id after a server epoch changes", async () => { const threadRef = { environmentId: EnvironmentId.make("environment-recording"), threadId: ThreadId.make("thread-recording-scoped"), }; const runtimeTabId = previewRuntimeTabId(threadRef, "epoch-a", "tab_1"); surfaceState.byTabId = { [runtimeTabId]: { visible: false, rect: { x: 0, y: 0, width: 1280, height: 800 }, content: { x: 0, y: 0, width: 1280, height: 800, scale: 1, scrollLeft: 0, scrollTop: 0, }, }, }; await startBrowserRecording(runtimeTabId, threadRef, "tab_1"); expect(startScreencast).toHaveBeenCalledWith(runtimeTabId); expect(readActiveBrowserRecordingTabIds(threadRef)).toEqual(new Set([runtimeTabId])); expect(readActiveBrowserRecordingTargets(threadRef)).toEqual([ { runtimeTabId, serverTabId: "tab_1" }, ]); expect(findActiveBrowserRecordingRuntimeTabId(threadRef, "tab_1")).toBe(runtimeTabId); const replacementRuntimeTabId = previewRuntimeTabId(threadRef, "epoch-b", "tab_1"); await expect( startBrowserRecording(replacementRuntimeTabId, threadRef, "tab_1"), ).rejects.toBeInstanceOf(BrowserRecordingConflictError); expect(startScreencast).toHaveBeenCalledTimes(1); await stopBrowserRecording(runtimeTabId); }); it("does not report success for a second start while the first is still starting", async () => { let finishStartingScreencast: (() => void) | undefined; startScreencast.mockImplementationOnce(async (tabId: string) => { events.push("start-screencast"); frameSubscription.listener?.({ tabId, data: "initial-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:00.000Z", }); await new Promise<void>((resolve) => { finishStartingScreencast = resolve; }); }); const firstStart = startBrowserRecording("recording-tab"); await vi.waitFor(() => expect(startScreencast).toHaveBeenCalledOnce()); await expect(startBrowserRecording("recording-tab")).rejects.toBeInstanceOf( BrowserRecordingConflictError, ); finishStartingScreencast?.(); await firstStart; await stopBrowserRecording("recording-tab"); }); it("does not report success for a start while the recording is stopping", async () => { let finishStoppingScreencast: (() => void) | undefined; stopScreencast.mockImplementationOnce(async () => { await new Promise<void>((resolve) => { finishStoppingScreencast = resolve; }); return undefined; }); await startBrowserRecording("recording-tab"); const stopPromise = stopBrowserRecording("recording-tab"); await vi.waitFor(() => expect(stopScreencast).toHaveBeenCalledOnce()); await expect(startBrowserRecording("recording-tab")).rejects.toBeInstanceOf( BrowserRecordingConflictError, ); finishStoppingScreencast?.(); await stopPromise; }); it("shares an in-progress stop with duplicate callers", async () => { let finishStoppingScreencast: (() => void) | undefined; stopScreencast.mockImplementationOnce(async () => { await new Promise<void>((resolve) => { finishStoppingScreencast = resolve; }); return undefined; }); await startBrowserRecording("recording-tab"); const firstStop = stopBrowserRecording("recording-tab"); await vi.waitFor(() => expect(stopScreencast).toHaveBeenCalledOnce()); const duplicateStop = stopBrowserRecording("recording-tab"); finishStoppingScreencast?.(); const [firstArtifact, duplicateArtifact] = await Promise.all([firstStop, duplicateStop]); expect(duplicateArtifact).toEqual(firstArtifact); expect(stopScreencast).toHaveBeenCalledOnce(); expect(save).toHaveBeenCalledOnce(); }); it("finishes startup before stopping so an active recording yields an artifact", async () => { let finishStartingScreencast: (() => void) | undefined; startScreencast.mockImplementationOnce(async () => { events.push("start-screencast"); await new Promise<void>((resolve) => { finishStartingScreencast = resolve; }); emitRecordingFrame(); }); const startPromise = startBrowserRecording("recording-tab"); await vi.waitFor(() => expect(startScreencast).toHaveBeenCalledOnce()); const stopPromise = stopBrowserRecording("recording-tab"); expect(stopScreencast).not.toHaveBeenCalled(); finishStartingScreencast?.(); await startPromise; await expect(stopPromise).resolves.toMatchObject({ tabId: "recording-tab" }); expect(stopScreencast).toHaveBeenCalledOnce(); expect(save).toHaveBeenCalledOnce();Lines 1–500 of 603. Load more
import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const { events, frameSubscription, onFrame, registrySet, save, startScreencast, stopScreencast, surfaceState, } = vi.hoisted(() => { const events: string[] = []; type Frame = { readonly tabId: string; readonly data: string; readonly width: number; readonly height: number; readonly receivedAt: string; }; const frameSubscription: { listener: ((frame: Frame) => void) | null } = { listener: null, }; const surfaceState = { byTabId: {} as Record<string, unknown>, }; return { events, frameSubscription, onFrame: vi.fn((listener: (frame: Frame) => void) => { frameSubscription.listener = listener; return () => { if (frameSubscription.listener === listener) frameSubscription.listener = null; }; }), registrySet: vi.fn((_atom: unknown, value: { readonly tabIds: ReadonlySet<string> }) => { events.push( value.tabIds.size === 0 ? "clear" : `publish:${Array.from(value.tabIds).join(",")}`, ); }), save: vi.fn(async (tabId: string) => ({ id: "recording-test", tabId, path: "/tmp/recording-test.webm", mimeType: "video/webm" as const, sizeBytes: 0, createdAt: "2026-06-26T00:00:00.000Z", })), startScreencast: vi.fn(async (tabId: string) => { events.push("start-screencast"); const surface = surfaceState.byTabId[tabId] as | { readonly content?: { readonly width: number; readonly height: number }; readonly rect?: { readonly width: number; readonly height: number }; } | undefined; const size = surface?.content ?? surface?.rect; frameSubscription.listener?.({ tabId, data: "initial-frame", width: size?.width ?? 1280, height: size?.height ?? 800, receivedAt: "2026-06-26T00:00:00.000Z", }); }), stopScreencast: vi.fn(async () => undefined), surfaceState, }; }); vi.mock("~/components/preview/previewBridge", () => ({ previewBridge: { recording: { onFrame, save, startScreencast, stopScreencast }, }, })); vi.mock("~/rpc/atomRegistry", () => ({ appAtomRegistry: { set: registrySet }, })); vi.mock("./browserSurfaceStore", () => ({ useBrowserSurfaceStore: { getState: () => surfaceState, }, })); import { BROWSER_RECORDING_FIRST_FRAME_SIZE_TIMEOUT_MS, BROWSER_RECORDING_STARTUP_SETTLE_TIMEOUT_MS, BrowserRecordingConflictError, findActiveBrowserRecordingRuntimeTabId, readActiveBrowserRecordingTabIds, readActiveBrowserRecordingTargets, startBrowserRecording, stopBrowserRecording, } from "./browserRecording"; import { previewRuntimeTabId } from "./previewRuntimeTabId"; class FakeMediaRecorder { static isTypeSupported(): boolean { return true; } state: RecordingState = "inactive"; private readonly listeners = new Map<string, Set<EventListenerOrEventListenerObject>>(); addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { const listeners = this.listeners.get(type) ?? new Set(); listeners.add(listener); this.listeners.set(type, listeners); } start(): void { this.state = "recording"; } stop(): void { this.state = "inactive"; for (const listener of this.listeners.get("stop") ?? []) { if (typeof listener === "function") listener(new Event("stop")); else listener.handleEvent(new Event("stop")); } } } const emitRecordingFrame = () => { frameSubscription.listener?.({ tabId: "recording-tab", data: "startup-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:00.000Z", }); }; describe("browser recording", () => { beforeEach(() => { events.length = 0; frameSubscription.listener = null; surfaceState.byTabId = { "recording-tab": { visible: true, rect: { x: 0, y: 0, width: 800, height: 600 }, content: { x: 0, y: 0, width: 800, height: 600, scale: 1, scrollLeft: 0, scrollTop: 0 }, }, }; vi.clearAllMocks(); vi.stubGlobal("window", globalThis); vi.stubGlobal("MediaRecorder", FakeMediaRecorder as unknown as typeof MediaRecorder); class ImmediateImage { private loadListener: EventListenerOrEventListenerObject | undefined; addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { if (type === "load") this.loadListener = listener; } set src(_value: string) { const event = new Event("load"); if (typeof this.loadListener === "function") this.loadListener(event); else this.loadListener?.handleEvent(event); } } vi.stubGlobal("Image", ImmediateImage as unknown as typeof Image); vi.stubGlobal("document", { createElement: () => ({ width: 0, height: 0, captureStream: () => ({}), getContext: () => ({ drawImage: vi.fn(), fillRect: vi.fn(), fillStyle: "" }), }), }); }); afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); }); it("starts recording for a visible tab", async () => { await startBrowserRecording("recording-tab"); expect(events).toEqual(["start-screencast", "publish:recording-tab"]); await stopBrowserRecording("recording-tab"); }); it("records a hidden tab without requiring it to become visible", async () => { surfaceState.byTabId = { "recording-tab": { visible: false, rect: { x: 0, y: 0, width: 800, height: 600 }, content: { x: 0, y: 0, width: 800, height: 600, scale: 1, scrollLeft: 0, scrollTop: 0 }, }, }; await startBrowserRecording("recording-tab"); expect(startScreencast).toHaveBeenCalledWith("recording-tab"); expect(events).toEqual(["start-screencast", "publish:recording-tab"]); await stopBrowserRecording("recording-tab"); }); it("fails startup instead of locking a fallback size when no frame arrives", async () => { vi.useFakeTimers(); startScreencast.mockImplementationOnce(async () => { events.push("start-screencast"); }); const startPromise = startBrowserRecording("recording-tab"); const rejection = expect(startPromise).rejects.toMatchObject({ operation: "wait-first-frame", tabId: "recording-tab", }); await Promise.resolve(); await vi.advanceTimersByTimeAsync(BROWSER_RECORDING_FIRST_FRAME_SIZE_TIMEOUT_MS); await rejection; expect(stopScreencast).toHaveBeenCalledWith("recording-tab"); expect(events.at(-1)).toBe("clear"); }); it("fixes hidden recording dimensions before MediaRecorder starts", async () => { const drawImage = vi.fn(); const fillRect = vi.fn(); let capturedStreamSize: { readonly width: number; readonly height: number } | undefined; const canvas = { width: 0, height: 0, captureStream: () => { capturedStreamSize = { width: canvas.width, height: canvas.height }; return {}; }, getContext: () => ({ drawImage, fillRect, fillStyle: "" }), }; vi.stubGlobal("document", { createElement: () => canvas, }); surfaceState.byTabId = {}; startScreencast.mockImplementationOnce(async (tabId: string) => { events.push("start-screencast"); frameSubscription.listener?.({ tabId, data: "captured-frame", width: 390, height: 844, receivedAt: "2026-06-26T00:00:00.000Z", }); }); await startBrowserRecording("recording-tab"); expect(canvas).toMatchObject({ width: 390, height: 844 }); expect(capturedStreamSize).toEqual({ width: 390, height: 844 }); expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 390, 844); frameSubscription.listener?.({ tabId: "recording-tab", data: "different-sized-frame", width: 1280, height: 720, receivedAt: "2026-06-26T00:00:01.000Z", }); expect(canvas).toMatchObject({ width: 390, height: 844 }); expect(fillRect).toHaveBeenLastCalledWith(0, 0, 390, 844); await stopBrowserRecording("recording-tab"); }); it("draws the newest decoded frames without starving behind decode latency", async () => { const drawImage = vi.fn(); class DeferredImage { static readonly instances: DeferredImage[] = []; private loadListener: EventListenerOrEventListenerObject | undefined; constructor() { DeferredImage.instances.push(this); } addEventListener(type: string, listener: EventListenerOrEventListenerObject): void { if (type === "load") this.loadListener = listener; } set src(_value: string) {} finishLoading(): void { const event = new Event("load"); if (typeof this.loadListener === "function") this.loadListener(event); else this.loadListener?.handleEvent(event); } } vi.stubGlobal("Image", DeferredImage as unknown as typeof Image); vi.stubGlobal("document", { createElement: () => ({ width: 0, height: 0, captureStream: () => ({}), getContext: () => ({ drawImage, fillRect: vi.fn(), fillStyle: "" }), }), }); await startBrowserRecording("recording-tab"); frameSubscription.listener?.({ tabId: "recording-tab", data: "second-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:01.000Z", }); frameSubscription.listener?.({ tabId: "recording-tab", data: "third-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:02.000Z", }); DeferredImage.instances[1]?.finishLoading(); expect(drawImage).toHaveBeenCalledOnce(); DeferredImage.instances[2]?.finishLoading(); expect(drawImage).toHaveBeenCalledTimes(2); DeferredImage.instances[0]?.finishLoading(); expect(drawImage).toHaveBeenCalledTimes(2); await stopBrowserRecording("recording-tab"); }); it("records separate tabs concurrently", async () => { const firstThreadRef = { environmentId: EnvironmentId.make("environment-recording"), threadId: ThreadId.make("thread-recording-first"), }; const secondThreadRef = { environmentId: EnvironmentId.make("environment-recording"), threadId: ThreadId.make("thread-recording-second"), }; surfaceState.byTabId = { ...surfaceState.byTabId, "recording-tab-2": { visible: false, rect: { x: 0, y: 0, width: 390, height: 844 }, content: { x: 0, y: 0, width: 390, height: 844, scale: 1, scrollLeft: 0, scrollTop: 0 }, }, }; await Promise.all([ startBrowserRecording("recording-tab", firstThreadRef), startBrowserRecording("recording-tab-2", secondThreadRef), ]); expect(startScreencast).toHaveBeenCalledTimes(2); expect(onFrame).toHaveBeenCalledOnce(); expect(events).toContain("publish:recording-tab,recording-tab-2"); expect(readActiveBrowserRecordingTabIds()).toEqual( new Set(["recording-tab", "recording-tab-2"]), ); expect(readActiveBrowserRecordingTabIds(firstThreadRef)).toEqual(new Set(["recording-tab"])); expect(readActiveBrowserRecordingTabIds(secondThreadRef)).toEqual(new Set(["recording-tab-2"])); await stopBrowserRecording("recording-tab"); expect(readActiveBrowserRecordingTabIds()).toEqual(new Set(["recording-tab-2"])); await stopBrowserRecording("recording-tab-2"); expect(readActiveBrowserRecordingTabIds()).toEqual(new Set()); expect(save).toHaveBeenCalledTimes(2); }); it("keeps a recording reachable through its runtime id after a server epoch changes", async () => { const threadRef = { environmentId: EnvironmentId.make("environment-recording"), threadId: ThreadId.make("thread-recording-scoped"), }; const runtimeTabId = previewRuntimeTabId(threadRef, "epoch-a", "tab_1"); surfaceState.byTabId = { [runtimeTabId]: { visible: false, rect: { x: 0, y: 0, width: 1280, height: 800 }, content: { x: 0, y: 0, width: 1280, height: 800, scale: 1, scrollLeft: 0, scrollTop: 0, }, }, }; await startBrowserRecording(runtimeTabId, threadRef, "tab_1"); expect(startScreencast).toHaveBeenCalledWith(runtimeTabId); expect(readActiveBrowserRecordingTabIds(threadRef)).toEqual(new Set([runtimeTabId])); expect(readActiveBrowserRecordingTargets(threadRef)).toEqual([ { runtimeTabId, serverTabId: "tab_1" }, ]); expect(findActiveBrowserRecordingRuntimeTabId(threadRef, "tab_1")).toBe(runtimeTabId); const replacementRuntimeTabId = previewRuntimeTabId(threadRef, "epoch-b", "tab_1"); await expect( startBrowserRecording(replacementRuntimeTabId, threadRef, "tab_1"), ).rejects.toBeInstanceOf(BrowserRecordingConflictError); expect(startScreencast).toHaveBeenCalledTimes(1); await stopBrowserRecording(runtimeTabId); }); it("does not report success for a second start while the first is still starting", async () => { let finishStartingScreencast: (() => void) | undefined; startScreencast.mockImplementationOnce(async (tabId: string) => { events.push("start-screencast"); frameSubscription.listener?.({ tabId, data: "initial-frame", width: 800, height: 600, receivedAt: "2026-06-26T00:00:00.000Z", }); await new Promise<void>((resolve) => { finishStartingScreencast = resolve; }); }); const firstStart = startBrowserRecording("recording-tab"); await vi.waitFor(() => expect(startScreencast).toHaveBeenCalledOnce()); await expect(startBrowserRecording("recording-tab")).rejects.toBeInstanceOf( BrowserRecordingConflictError, ); finishStartingScreencast?.(); await firstStart; await stopBrowserRecording("recording-tab"); }); it("does not report success for a start while the recording is stopping", async () => { let finishStoppingScreencast: (() => void) | undefined; stopScreencast.mockImplementationOnce(async () => { await new Promise<void>((resolve) => { finishStoppingScreencast = resolve; }); return undefined; }); await startBrowserRecording("recording-tab"); const stopPromise = stopBrowserRecording("recording-tab"); await vi.waitFor(() => expect(stopScreencast).toHaveBeenCalledOnce()); await expect(startBrowserRecording("recording-tab")).rejects.toBeInstanceOf( BrowserRecordingConflictError, ); finishStoppingScreencast?.(); await stopPromise; }); it("shares an in-progress stop with duplicate callers", async () => { let finishStoppingScreencast: (() => void) | undefined; stopScreencast.mockImplementationOnce(async () => { await new Promise<void>((resolve) => { finishStoppingScreencast = resolve; }); return undefined; }); await startBrowserRecording("recording-tab"); const firstStop = stopBrowserRecording("recording-tab"); await vi.waitFor(() => expect(stopScreencast).toHaveBeenCalledOnce()); const duplicateStop = stopBrowserRecording("recording-tab"); finishStoppingScreencast?.(); const [firstArtifact, duplicateArtifact] = await Promise.all([firstStop, duplicateStop]); expect(duplicateArtifact).toEqual(firstArtifact); expect(stopScreencast).toHaveBeenCalledOnce(); expect(save).toHaveBeenCalledOnce(); }); it("finishes startup before stopping so an active recording yields an artifact", async () => { let finishStartingScreencast: (() => void) | undefined; startScreencast.mockImplementationOnce(async () => { events.push("start-screencast"); await new Promise<void>((resolve) => { finishStartingScreencast = resolve; }); emitRecordingFrame(); }); const startPromise = startBrowserRecording("recording-tab"); await vi.waitFor(() => expect(startScreencast).toHaveBeenCalledOnce()); const stopPromise = stopBrowserRecording("recording-tab"); expect(stopScreencast).not.toHaveBeenCalled(); finishStartingScreencast?.(); await startPromise; await expect(stopPromise).resolves.toMatchObject({ tabId: "recording-tab" }); expect(stopScreencast).toHaveBeenCalledOnce(); expect(save).toHaveBeenCalledOnce();