706 lines · 25.3 KB · TypeScript
RawDownload
import {  DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER,  EnvironmentId,  ThreadId,} from "@t3tools/contracts";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const {  clientSettings,  events,  getDisplayMedia,  registrySet,  requestDisplayMediaCapture,  save,  startScreencast,  stopScreencast,} = vi.hoisted(() => {  const events: string[] = [];  return {    clientSettings: { browserRecordingFrameRate: 30 as 30 | 60 },    events,    getDisplayMedia: vi.fn(),    requestDisplayMediaCapture: vi.fn((_tabId: string) => undefined),    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");    }),    stopScreencast: vi.fn(async () => undefined),  };}); vi.mock("~/components/preview/previewBridge", () => ({  previewBridge: {    recording: {      onFrame: vi.fn(),      save,      startScreencast: async (tabId: string) => {        await startScreencast(tabId);        requestDisplayMediaCapture(tabId);      },      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,  BrowserRecordingStartCancelledError,  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);    getDisplayMedia.mockResolvedValue({      getTracks: () => [{ stop: vi.fn() }],    });    requestDisplayMediaCapture.mockImplementation((tabId: string) => {      const trigger = Reflect.get(globalThis, DESKTOP_PREVIEW_RECORDING_CAPTURE_TRIGGER);      if (typeof trigger !== "function" || trigger(tabId) !== true) {        throw new Error(`No pending display-media capture for ${tabId}.`);      }    });    vi.stubGlobal("navigator", { mediaDevices: { getDisplayMedia } });    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("routes gesture-free starts through the desktop capture trigger", async () => {    await startBrowserRecording("automation-recording-tab");     expect(requestDisplayMediaCapture).toHaveBeenCalledWith("automation-recording-tab");    expect(getDisplayMedia).toHaveBeenCalledOnce();    await stopBrowserRecording("automation-recording-tab");  });   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);    });    getDisplayMedia.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 armed by the main process", async () => {    const stopTrack = vi.fn();    const stream = { getTracks: () => [{ stop: stopTrack }] } as unknown as MediaStream;    getDisplayMedia.mockResolvedValueOnce(stream);     await startBrowserRecording("recording-tab");     expect(getDisplayMedia).toHaveBeenCalledWith({      audio: false,      video: { frameRate: { max: 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(getDisplayMedia).toHaveBeenCalledWith({      audio: false,      video: { frameRate: { max: 60 } },    });    await stopBrowserRecording("recording-tab");  });   it("stops the native stream when MediaRecorder cleanup fails", async () => {    const stopTrack = vi.fn();    getDisplayMedia.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 () => {    getDisplayMedia.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();    getDisplayMedia.mockImplementationOnce(      () =>        new Promise<MediaStream>((resolve) => {          finishCapture = resolve;        }),    );     const startPromise = startBrowserRecording("recording-tab");    await vi.waitFor(() => expect(getDisplayMedia).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("serializes display media grants for concurrent recording starts", async () => {    let finishFirstCapture!: (stream: MediaStream) => void;    const stream = { getTracks: () => [{ stop: vi.fn() }] } as unknown as MediaStream;    getDisplayMedia      .mockImplementationOnce(        () =>          new Promise<MediaStream>((resolve) => {            finishFirstCapture = resolve;          }),      )      .mockResolvedValueOnce(stream);     const firstStart = startBrowserRecording("recording-tab");    await vi.waitFor(() => expect(getDisplayMedia).toHaveBeenCalledOnce());    const secondStart = startBrowserRecording("recording-tab-2");    await vi.waitFor(() => expect(readActiveBrowserRecordingTabIds().size).toBe(2));     expect(startScreencast).toHaveBeenCalledTimes(1);    finishFirstCapture(stream);    await Promise.all([firstStart, secondStart]);     expect(startScreencast.mock.calls).toEqual([["recording-tab"], ["recording-tab-2"]]);    expect(getDisplayMedia).toHaveBeenCalledTimes(2);    await Promise.all([      stopBrowserRecording("recording-tab"),      stopBrowserRecording("recording-tab-2"),    ]);  });   it("cancels a queued recording when stopped before its media grant", async () => {    let finishFirstCapture!: (stream: MediaStream) => void;    const stream = { getTracks: () => [{ stop: vi.fn() }] } as unknown as MediaStream;    getDisplayMedia.mockImplementationOnce(      () =>        new Promise<MediaStream>((resolve) => {          finishFirstCapture = resolve;        }),    );     const firstStart = startBrowserRecording("recording-tab");    await vi.waitFor(() => expect(getDisplayMedia).toHaveBeenCalledOnce());    const secondStart = startBrowserRecording("recording-tab-2");    await vi.waitFor(() => expect(readActiveBrowserRecordingTabIds().size).toBe(2));     const secondStop = stopBrowserRecording("recording-tab-2");    await expect(secondStart).rejects.toBeInstanceOf(BrowserRecordingStartCancelledError);    await expect(secondStop).resolves.toBeNull();    expect(startScreencast).toHaveBeenCalledTimes(1);     finishFirstCapture(stream);    await firstStart;    await stopBrowserRecording("recording-tab");    expect(getDisplayMedia).toHaveBeenCalledOnce();  });   it("latches a stop that arrives before the start becomes queued", async () => {    let releaseDelayedPaint!: (timestamp: number) => void;    let frameId = 0;    vi.stubGlobal(      "requestAnimationFrame",      vi.fn((callback: FrameRequestCallback) => {        frameId += 1;        if (frameId === 1) releaseDelayedPaint = callback;        else callback(frameId);        return frameId;      }),    );    let finishBlockingCapture!: (stream: MediaStream) => void;    const stream = { getTracks: () => [{ stop: vi.fn() }] } as unknown as MediaStream;    getDisplayMedia.mockImplementationOnce(      () =>        new Promise<MediaStream>((resolve) => {          finishBlockingCapture = resolve;        }),    );     const delayedStart = startBrowserRecording("delayed-tab");    await vi.waitFor(() =>      expect(readActiveBrowserRecordingTabIds().has("delayed-tab")).toBe(true),    );    const blockingStart = startBrowserRecording("blocking-tab");    await vi.waitFor(() => expect(getDisplayMedia).toHaveBeenCalledOnce());     const delayedStop = stopBrowserRecording("delayed-tab");    releaseDelayedPaint(1);     await expect(delayedStart).rejects.toBeInstanceOf(BrowserRecordingStartCancelledError);    await expect(delayedStop).resolves.toBeNull();    expect(startScreencast).toHaveBeenCalledOnce();     finishBlockingCapture(stream);    await blockingStart;    await stopBrowserRecording("blocking-tab");  });   it("finishes an uncontended pre-grant start before stopping", async () => {    const animationFrames: FrameRequestCallback[] = [];    vi.stubGlobal(      "requestAnimationFrame",      vi.fn((callback: FrameRequestCallback) => {        animationFrames.push(callback);        return animationFrames.length;      }),    );     const startPromise = startBrowserRecording("recording-tab");    await vi.waitFor(() =>      expect(readActiveBrowserRecordingTabIds().has("recording-tab")).toBe(true),    );    const stopPromise = stopBrowserRecording("recording-tab");    expect(startScreencast).not.toHaveBeenCalled();     animationFrames.shift()?.(1);    animationFrames.shift()?.(2);    await startPromise;    await expect(stopPromise).resolves.toMatchObject({ tabId: "recording-tab" });  });   it("keeps a recording reachable through its runtime id after a server epoch changes", async () => {    const threadRef = {

Lines 1–500 of 706.