294 lines · 9.5 KB · TypeScript
RawDownload
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const { events, onFrame, registrySet, save, startScreencast, stopScreencast, surfaceState } =  vi.hoisted(() => {    const events: string[] = [];    const surfaceState = {      byTabId: {} as Record<string, unknown>,    };    return {      events,      onFrame: vi.fn(() => vi.fn()),      registrySet: vi.fn((_atom: unknown, value: string | null) => {        events.push(value === null ? "clear" : `publish:${value}`);      }),      save: vi.fn(async () => ({        id: "recording-test",        tabId: "recording-tab",        path: "/tmp/recording-test.webm",        mimeType: "video/webm" as const,        sizeBytes: 0,        createdAt: "2026-06-26T00:00:00.000Z",      })),      startScreencast: vi.fn(async () => {        events.push("start-screencast");      }),      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_STARTUP_SETTLE_TIMEOUT_MS,  BrowserRecordingConflictError,  BrowserRecordingOperationError,  BrowserRecordingRequiresVisibleTabError,  startBrowserRecording,  stopBrowserRecording,} from "./browserRecording"; 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"));    }  }} describe("browser recording", () => {  beforeEach(() => {    events.length = 0;    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);    vi.stubGlobal("document", {      createElement: () => ({        width: 0,        height: 0,        captureStream: () => ({}),        getContext: () => ({ drawImage: vi.fn() }),      }),    });  });   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("rejects recording for a hidden tab before starting screencast", 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 expect(startBrowserRecording("recording-tab")).rejects.toBeInstanceOf(      BrowserRecordingRequiresVisibleTabError,    );     expect(startScreencast).not.toHaveBeenCalled();    expect(registrySet).not.toHaveBeenCalled();  });   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;      });    });     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("stops a screencast that finishes starting after cancellation", async () => {    let finishStartingScreencast: (() => void) | undefined;    startScreencast.mockImplementationOnce(async () => {      events.push("start-screencast");      await new Promise<void>((resolve) => {        finishStartingScreencast = resolve;      });    });     const startPromise = startBrowserRecording("recording-tab");    const rejectedStart = expect(startPromise).rejects.toBeInstanceOf(      BrowserRecordingOperationError,    );    await vi.waitFor(() => expect(startScreencast).toHaveBeenCalledOnce());     const stopPromise = stopBrowserRecording("recording-tab");    await vi.waitFor(() => expect(stopScreencast).toHaveBeenCalledOnce());    finishStartingScreencast?.();     await rejectedStart;    await stopPromise;    expect(stopScreencast).toHaveBeenCalledTimes(2);    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;      });    });     const firstStart = startBrowserRecording("recording-tab");    const rejectedFirstStart = expect(firstStart).rejects.toBeInstanceOf(      BrowserRecordingOperationError,    );    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 rejectedFirstStart;    await stopPromise;    await restartAfterStop;    await stopBrowserRecording("recording-tab");     expect(startCallsBeforeFirstSettled).toBe(1);  });   it("fails a stop that waits too long for startup without freeing the recording slot", async () => {    vi.useFakeTimers();    let finishStartingScreencast: (() => void) | undefined;    startScreencast.mockImplementationOnce(async () => {      events.push("start-screencast");      await new Promise<void>((resolve) => {        finishStartingScreencast = resolve;      });    });     const startPromise = startBrowserRecording("recording-tab");    const rejectedStart = expect(startPromise).rejects.toBeInstanceOf(      BrowserRecordingOperationError,    );    expect(startScreencast).toHaveBeenCalledOnce();     const stopPromise = stopBrowserRecording("recording-tab");    await Promise.resolve();    await Promise.resolve();    expect(stopScreencast).toHaveBeenCalledOnce();     const rejection = expect(stopPromise).rejects.toMatchObject({      operation: "wait-startup",      tabId: "recording-tab",    });    await vi.advanceTimersByTimeAsync(BROWSER_RECORDING_STARTUP_SETTLE_TIMEOUT_MS);     await rejection;    expect(save).not.toHaveBeenCalled();    await expect(startBrowserRecording("recording-tab")).rejects.toBeInstanceOf(      BrowserRecordingConflictError,    );     finishStartingScreencast?.();    await rejectedStart;    const cleanupResult = await stopBrowserRecording("recording-tab");    expect(cleanupResult).toBeNull();    expect(save).not.toHaveBeenCalled();    expect(events.at(-1)).toBe("clear");  });});