571 lines · 20.7 KB · TypeScript
RawDownload
import { EnvironmentId, ThreadId } from "@t3tools/contracts";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const { clientSettings, events, getUserMedia, registrySet, save, startScreencast, stopScreencast } =  vi.hoisted(() => {    const events: string[] = [];    return {      clientSettings: { browserRecordingFrameRate: 30 as 30 | 60 },      events,      getUserMedia: vi.fn(),      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");        return { sourceId: `source:${tabId}`, width: 1000, height: 620 };      }),      stopScreencast: vi.fn(async () => undefined),    };  }); vi.mock("~/components/preview/previewBridge", () => ({  previewBridge: {    recording: { onFrame: vi.fn(), save, startScreencast, stopScreencast },  },})); vi.mock("~/rpc/atomRegistry", () => ({  appAtomRegistry: { set: registrySet },})); vi.mock("~/hooks/useSettings", () => ({  ensureClientSettingsHydrated: vi.fn(async () => undefined),  getClientSettings: () => clientSettings,})); import {  BROWSER_RECORDING_PAINT_SETTLE_TIMEOUT_MS,  BROWSER_RECORDING_STARTUP_SETTLE_TIMEOUT_MS,  BrowserRecordingCaptureTimeoutError,  BrowserRecordingConflictError,  BrowserRecordingFormatUnavailableError,  findActiveBrowserRecordingRuntimeTabId,  readActiveBrowserRecordingTabIds,  readActiveBrowserRecordingTargets,  startBrowserRecording,  stopBrowserRecording,} from "./browserRecording";import { useBrowserSurfaceStore } from "./browserSurfaceStore";import { previewRuntimeTabId } from "./previewRuntimeTabId"; class FakeMediaRecorder {  static readonly instances: FakeMediaRecorder[] = [];  static supportedTypes = new Set(["video/webm;codecs=vp9"]);  static outputMimeType: string | undefined;  static stopError: unknown;  static isTypeSupported(type: string): boolean {    return this.supportedTypes.has(type);  }   state: RecordingState = "inactive";  readonly mimeType: string;  readonly stream: MediaStream;  readonly options: MediaRecorderOptions | undefined;  private readonly listeners = new Map<string, Set<EventListenerOrEventListenerObject>>();   constructor(stream: MediaStream, options?: MediaRecorderOptions) {    this.stream = stream;    this.options = options;    this.mimeType =      FakeMediaRecorder.outputMimeType ?? options?.mimeType ?? "video/browser-default";    FakeMediaRecorder.instances.push(this);  }   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 {    if (FakeMediaRecorder.stopError !== undefined) throw FakeMediaRecorder.stopError;    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"));    }  }} describe("browser recording", () => {  let animationFrameCount = 0;   beforeEach(() => {    events.length = 0;    vi.clearAllMocks();    FakeMediaRecorder.instances.length = 0;    FakeMediaRecorder.supportedTypes = new Set(["video/webm;codecs=vp9"]);    FakeMediaRecorder.outputMimeType = undefined;    FakeMediaRecorder.stopError = undefined;    clientSettings.browserRecordingFrameRate = 30;    animationFrameCount = 0;    vi.stubGlobal("window", globalThis);    vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {      animationFrameCount += 1;      callback(animationFrameCount);      return animationFrameCount;    });    vi.stubGlobal("cancelAnimationFrame", vi.fn());    vi.stubGlobal("MediaRecorder", FakeMediaRecorder as unknown as typeof MediaRecorder);    getUserMedia.mockResolvedValue({      getTracks: () => [{ stop: vi.fn() }],    });    vi.stubGlobal("navigator", { mediaDevices: { getUserMedia } });    useBrowserSurfaceStore.setState({ activityByTabId: {}, byTabId: {} });  });   afterEach(() => {    vi.useRealTimers();    vi.unstubAllGlobals();  });   it("starts recording for a visible tab", async () => {    await startBrowserRecording("recording-tab");    const startupEvents = [...events];     await stopBrowserRecording("recording-tab");    expect(startupEvents).toEqual(["publish:recording-tab", "start-screencast"]);  });   it("paints and holds a hidden browser surface for the recording lifetime", async () => {    startScreencast.mockImplementationOnce(async (tabId: string) => {      expect(animationFrameCount).toBe(2);      expect(useBrowserSurfaceStore.getState().activityByTabId[tabId]).toBe(1);      return { sourceId: `source:${tabId}`, width: 1000, height: 620 };    });    getUserMedia.mockImplementationOnce(async () => {      expect(animationFrameCount).toBe(2);      expect(useBrowserSurfaceStore.getState().activityByTabId["background-tab"]).toBe(1);      return { getTracks: () => [{ stop: vi.fn() }] };    });     await startBrowserRecording("background-tab");    expect(useBrowserSurfaceStore.getState().activityByTabId["background-tab"]).toBe(1);     await stopBrowserRecording("background-tab");    expect(useBrowserSurfaceStore.getState().activityByTabId["background-tab"]).toBeUndefined();  });   it("bounds compositor warmup when animation frames are paused", async () => {    vi.useFakeTimers();    const cancelAnimationFrame = vi.fn();    vi.stubGlobal(      "requestAnimationFrame",      vi.fn(() => 42),    );    vi.stubGlobal("cancelAnimationFrame", cancelAnimationFrame);     const startPromise = startBrowserRecording("hidden-window-tab");    await vi.advanceTimersByTimeAsync(BROWSER_RECORDING_PAINT_SETTLE_TIMEOUT_MS);     await startPromise;    expect(cancelAnimationFrame).toHaveBeenCalledWith(42);    await stopBrowserRecording("hidden-window-tab");  });   it("records the native tab stream at the preview's current dimensions", async () => {    const stopTrack = vi.fn();    const stream = { getTracks: () => [{ stop: stopTrack }] } as unknown as MediaStream;    getUserMedia.mockResolvedValueOnce(stream);     await startBrowserRecording("recording-tab");     expect(getUserMedia).toHaveBeenCalledWith({      audio: false,      video: {        mandatory: {          chromeMediaSource: "tab",          chromeMediaSourceId: "source:recording-tab",          minWidth: 1000,          maxWidth: 1000,          minHeight: 620,          maxHeight: 620,          maxFrameRate: 30,        },      },    });    expect(FakeMediaRecorder.instances[0]?.stream).toBe(stream);     await stopBrowserRecording("recording-tab");    expect(stopTrack).toHaveBeenCalledOnce();  });   it("uses the configured recording frame rate", async () => {    clientSettings.browserRecordingFrameRate = 60;     await startBrowserRecording("recording-tab");     expect(getUserMedia).toHaveBeenCalledWith({      audio: false,      video: {        mandatory: expect.objectContaining({ maxFrameRate: 60 }),      },    });    await stopBrowserRecording("recording-tab");  });   it("stops the native stream when MediaRecorder cleanup fails", async () => {    const stopTrack = vi.fn();    getUserMedia.mockResolvedValueOnce({      getTracks: () => [{ stop: stopTrack }],    });     await startBrowserRecording("recording-tab");    FakeMediaRecorder.stopError = new Error("stop failed");     await expect(stopBrowserRecording("recording-tab")).rejects.toMatchObject({      operation: "cleanup",      tabId: "recording-tab",    });    expect(stopTrack).toHaveBeenCalledOnce();  });   it("uses the best supported encoder and saves the recorder's actual format", async () => {    FakeMediaRecorder.supportedTypes = new Set([      "video/mp4;codecs=avc1.42e01e",      "video/webm;codecs=vp9",      "video/webm;codecs=av1",    ]);    FakeMediaRecorder.outputMimeType = "video/webm;codecs=av01";     await startBrowserRecording("recording-tab");    await stopBrowserRecording("recording-tab");     expect(FakeMediaRecorder.instances[0]?.options).toEqual({      mimeType: "video/webm;codecs=av1",    });    expect(save).toHaveBeenCalledWith(      "recording-tab",      "video/webm;codecs=av01",      expect.any(Uint8Array),    );  });   it("lets the browser select the format when no preferred encoding is supported", async () => {    FakeMediaRecorder.supportedTypes = new Set();    FakeMediaRecorder.outputMimeType = "video/platform-default";     await startBrowserRecording("recording-tab");    await stopBrowserRecording("recording-tab");     expect(FakeMediaRecorder.instances[0]?.options).toBeUndefined();    expect(save).toHaveBeenCalledWith(      "recording-tab",      "video/platform-default",      expect.any(Uint8Array),    );  });   it("reports when MediaRecorder provides no output format", async () => {    FakeMediaRecorder.supportedTypes = new Set();    FakeMediaRecorder.outputMimeType = "";     await startBrowserRecording("recording-tab");     await expect(stopBrowserRecording("recording-tab")).rejects.toBeInstanceOf(      BrowserRecordingFormatUnavailableError,    );    expect(save).not.toHaveBeenCalled();    expect(readActiveBrowserRecordingTabIds()).toEqual(new Set());  });   it("releases the native capture lease when stream acquisition fails", async () => {    getUserMedia.mockRejectedValueOnce(new Error("capture failed"));     await expect(startBrowserRecording("recording-tab")).rejects.toMatchObject({      operation: "capture-media-stream",      tabId: "recording-tab",    });     expect(stopScreencast).toHaveBeenCalledWith("recording-tab");    expect(events.at(-1)).toBe("clear");  });   it("times out stalled stream acquisition and stops a late stream", async () => {    vi.useFakeTimers();    let finishCapture!: (stream: MediaStream) => void;    const stopTrack = vi.fn();    getUserMedia.mockImplementationOnce(      () =>        new Promise<MediaStream>((resolve) => {          finishCapture = resolve;        }),    );     const startPromise = startBrowserRecording("recording-tab");    await vi.waitFor(() => expect(getUserMedia).toHaveBeenCalledOnce());    const rejection = expect(startPromise).rejects.toMatchObject({      _tag: "BrowserRecordingCaptureTimeoutError",      tabId: "recording-tab",      timeoutMs: BROWSER_RECORDING_STARTUP_SETTLE_TIMEOUT_MS,    });    await vi.advanceTimersByTimeAsync(BROWSER_RECORDING_STARTUP_SETTLE_TIMEOUT_MS);     await rejection;    await expect(startPromise).rejects.toBeInstanceOf(BrowserRecordingCaptureTimeoutError);    expect(stopScreencast).toHaveBeenCalledWith("recording-tab");    expect(readActiveBrowserRecordingTabIds()).toEqual(new Set());    expect(useBrowserSurfaceStore.getState().activityByTabId["recording-tab"]).toBeUndefined();     finishCapture({ getTracks: () => [{ stop: stopTrack }] } as unknown as MediaStream);    await vi.advanceTimersByTimeAsync(0);    expect(stopTrack).toHaveBeenCalledOnce();  });   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"),    };    await Promise.all([      startBrowserRecording("recording-tab", firstThreadRef),      startBrowserRecording("recording-tab-2", secondThreadRef),    ]);     expect(startScreencast).toHaveBeenCalledTimes(2);    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");    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 () => {      events.push("start-screencast");      await new Promise<void>((resolve) => {        finishStartingScreencast = resolve;      });      return { sourceId: "source:recording-tab", width: 1000, height: 620 };    });     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;      });      return { sourceId: "source:recording-tab", width: 1000, height: 620 };    });     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();    expect(events.at(-1)).toBe("clear");  });   it("does not release the recording slot until a cancelled start settles", async () => {    let finishStartingScreencast: (() => void) | undefined;    startScreencast.mockImplementationOnce(async () => {      events.push("start-screencast");      await new Promise<void>((resolve) => {        finishStartingScreencast = resolve;      });      return { sourceId: "source:recording-tab", width: 1000, height: 620 };    });     const firstStart = startBrowserRecording("recording-tab");    await vi.waitFor(() => expect(startScreencast).toHaveBeenCalledOnce());     const stopPromise = stopBrowserRecording("recording-tab");    const restartAfterStop = stopPromise.then(() => startBrowserRecording("recording-tab"));    await new Promise((resolve) => setTimeout(resolve, 0));    const startCallsBeforeFirstSettled = startScreencast.mock.calls.length;     finishStartingScreencast?.();    await firstStart;    await stopPromise;    await restartAfterStop;    await stopBrowserRecording("recording-tab");     expect(startCallsBeforeFirstSettled).toBe(1);  });   it("keeps the recording slot while a failed stop waits for startup", async () => {    let finishStartingScreencast: (() => void) | undefined;

Lines 1–500 of 571.