3386 lines · 123.2 KB · TypeScript
RawDownload
import { it as effectIt } from "@effect/vitest";import type { DesktopPreviewRecordingFrame } from "@t3tools/contracts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Cause from "effect/Cause";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import type * as Scope from "effect/Scope";import { TestClock } from "effect/testing";import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as ElectronWindow from "../electron/ElectronWindow.ts";import * as BrowserSession from "./BrowserSession.ts";import * as PreviewManager from "./Manager.ts"; describe("fitPictureInPictureContentSize", () => {  it("preserves the PiP content area across aspect-ratio changes", () => {    expect(PreviewManager.fitPictureInPictureContentSize([480, 320], 16 / 9)).toEqual([523, 294]);    expect(PreviewManager.fitPictureInPictureContentSize([480, 320], 9 / 16)).toEqual([294, 523]);  });   it("does not collapse toward the minimum size when orientation changes repeatedly", () => {    const portrait = PreviewManager.fitPictureInPictureContentSize([523, 294], 9 / 16);    const landscape = PreviewManager.fitPictureInPictureContentSize(portrait, 16 / 9);     expect(portrait).toEqual([294, 523]);    expect(landscape).toEqual([523, 294]);  });}); describe("isPreviewRefreshShortcut", () => {  const input = (overrides: Partial<Electron.Input> = {}) =>    ({      type: "keyDown",      key: "r",      meta: true,      control: false,      shift: false,      alt: false,      ...overrides,    }) as Electron.Input;   it("recognizes the platform refresh chord without matching modified variants", () => {    expect(PreviewManager.isPreviewRefreshShortcut(input())).toBe(true);    expect(PreviewManager.isPreviewRefreshShortcut(input({ meta: false, control: true }))).toBe(      true,    );    expect(PreviewManager.isPreviewRefreshShortcut(input({ shift: true }))).toBe(false);    expect(PreviewManager.isPreviewRefreshShortcut(input({ type: "keyUp" }))).toBe(false);  });}); const {  browserWindowConstructor,  createFromPath,  fromId,  getFocusedWebContents,  mkdir,  showItemInFolder,  webviewSend,  writeFile,  writeImage,} = vi.hoisted(() => ({  browserWindowConstructor: vi.fn(),  createFromPath: vi.fn((): { readonly isEmpty: () => boolean } => ({ isEmpty: () => false })),  fromId: vi.fn((_id?: number) => null),  getFocusedWebContents: vi.fn(() => null),  mkdir: vi.fn((_path: string) => undefined),  showItemInFolder: vi.fn(),  webviewSend: vi.fn(),  writeFile: vi.fn((_path: string, _data: Uint8Array) => undefined),  writeImage: vi.fn(),})); vi.mock("electron", () => ({  BrowserWindow: browserWindowConstructor,  clipboard: {    writeImage,  },  nativeImage: {    createFromPath,  },  shell: {    showItemInFolder,  },  session: {    fromPartition: vi.fn(),  },  webContents: {    fromId,    getFocusedWebContents,  },})); const browserSessionLayer = Layer.succeed(  BrowserSession.BrowserSession,  BrowserSession.BrowserSession.of({    getPartition: () => Effect.succeed("persist:t3code-preview-test"),    isPartition: (partition) => partition.startsWith("persist:t3code-preview-"),    getSession: () => Effect.die("unexpected getSession"),    clearCookies: () => Effect.void,    clearCache: () => Effect.void,  }),); const environmentLayer = Layer.succeed(  DesktopEnvironment.DesktopEnvironment,  DesktopEnvironment.DesktopEnvironment.of({    browserArtifactsDir: "/tmp/t3/dev/browser-artifacts",    dirname: "/tmp/t3/desktop",    path: {      join: (...parts: ReadonlyArray<string>) => parts.join("/"),    },  } as DesktopEnvironment.DesktopEnvironment["Service"]),); const fileSystemLayer = FileSystem.layerNoop({  makeDirectory: (path) =>    Effect.sync(() => {      mkdir(path);    }),  writeFile: (path, data) =>    Effect.sync(() => {      writeFile(path, data);    }),}); const layer = PreviewManager.layer.pipe(  Layer.provideMerge(browserSessionLayer),  Layer.provideMerge(environmentLayer),  Layer.provideMerge(fileSystemLayer),  Layer.provideMerge(Path.layer),  Layer.provideMerge(Layer.succeed(HostProcessPlatform, "darwin")),);const encodePreviewManagerError = Schema.encodeSync(PreviewManager.PreviewManagerError); const withManager = <A>(  use: (    manager: PreviewManager.PreviewManager["Service"],  ) => Effect.Effect<A, PreviewManager.PreviewManagerError, Scope.Scope>,) =>  Effect.gen(function* () {    const manager = yield* PreviewManager.PreviewManager;    return yield* use(manager);  }).pipe(Effect.provide(layer), Effect.scoped); interface TestCapturedPreviewImage {  readonly toJPEG: () => Buffer;  readonly getSize: () => { readonly width: number; readonly height: number };} const makeTestPreviewWebContents = (  capturePage: () => Promise<TestCapturedPreviewImage>,  id = 42,) =>  ({    id,    isDestroyed: () => false,    getType: () => "webview",    getURL: () => "https://example.com",    getTitle: () => "Example",    isLoading: () => false,    getZoomFactor: () => 1,    setZoomFactor: vi.fn(),    setAudioMuted: vi.fn(),    isCurrentlyAudible: () => false,    on: vi.fn(),    off: vi.fn(),    ipc: { on: vi.fn(), off: vi.fn() },    send: webviewSend,    navigationHistory: { canGoBack: () => false, canGoForward: () => false },    setWindowOpenHandler: vi.fn(),    debugger: {      isAttached: () => false,      attach: vi.fn(),      sendCommand: vi.fn(async () => undefined),      on: vi.fn(),      off: vi.fn(),    },    capturePage,  }) as never; const TEST_FAVICON = "data:image/png;base64,cG5n"; const makeSourcePng = (width = 1, height = 1): Buffer => {  const buffer = Buffer.alloc(24);  Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(buffer);  buffer.writeUInt32BE(width, 16);  buffer.writeUInt32BE(height, 20);  return buffer;}; const makeFaviconWebContents = (options?: {  readonly fetch?: (url: string, init?: RequestInit) => Promise<Response>;  readonly id?: number;  readonly rasterize?: (code: string) => Promise<unknown>;  readonly url?: string;}) => {  const sourcePng = makeSourcePng();  const listeners = new Map<string, (...args: never[]) => void>();  let currentUrl = options?.url ?? "http://localhost:3200/";  let destroyed = false;  let loading = false;  const fetch = vi.fn(    options?.fetch ??      (async () =>        new Response(new Uint8Array(sourcePng), {          headers: { "content-type": "image/png" },        })),  );  const executeJavaScriptInIsolatedWorld = vi.fn(    async (_worldId: number, scripts: ReadonlyArray<{ readonly code: string }>) =>      options?.rasterize ? options.rasterize(scripts[0]?.code ?? "") : TEST_FAVICON,  );  const reload = vi.fn();  const loadURL = vi.fn(async (url: string) => {    currentUrl = url;  });  const off = vi.fn();  const debuggerOff = vi.fn();  const webContents = {    id: options?.id ?? 42,    isDestroyed: () => destroyed,    getType: () => "webview",    getURL: () => currentUrl,    getTitle: () => "Preview",    isLoading: () => loading,    isDevToolsOpened: () => false,    getZoomFactor: () => 1,    setZoomFactor: vi.fn(),    setAudioMuted: vi.fn(),    isCurrentlyAudible: () => false,    reload,    reloadIgnoringCache: vi.fn(),    loadURL,    on: vi.fn((event: string, listener: (...args: never[]) => void) => {      listeners.set(event, listener);    }),    off,    ipc: { on: vi.fn(), off: vi.fn() },    send: webviewSend,    session: { fetch },    navigationHistory: { canGoBack: () => false, canGoForward: () => false },    setWindowOpenHandler: vi.fn(),    executeJavaScriptInIsolatedWorld,    debugger: {      isAttached: () => false,      attach: vi.fn(),      sendCommand: vi.fn(async () => undefined),      on: vi.fn(),      off: debuggerOff,    },  };  return {    executeJavaScriptInIsolatedWorld,    fetch,    debuggerOff,    listeners,    loadURL,    off,    reload,    setDestroyed: (value: boolean) => {      destroyed = value;    },    setLoading: (value: boolean) => {      loading = value;    },    setUrl: (url: string) => {      currentUrl = url;    },    webContents: webContents as never,  };}; const settle = function* (until: () => boolean) {  for (let attempt = 0; attempt < 30 && !until(); attempt++) {    yield* Effect.promise(() => Promise.resolve());  }}; const makeTestPictureInPictureWindow = (loadURL: () => Promise<void> = async () => undefined) => {  const listeners = new Map<string, () => void>();  const send = vi.fn();  let destroyed = false;  const pictureInPictureWindow = {    isDestroyed: vi.fn(() => destroyed),    once: vi.fn((event: string, listener: () => void) => {      listeners.set(event, listener);    }),    setAlwaysOnTop: vi.fn(),    setVisibleOnAllWorkspaces: vi.fn(),    setAspectRatio: vi.fn(),    getContentSize: vi.fn(() => [480, 320]),    setContentSize: vi.fn(),    loadURL: vi.fn(loadURL),    showInactive: vi.fn(() => {      if (destroyed) throw new Error("Picture-in-picture window is closed.");    }),    close: vi.fn(() => {      if (destroyed) return;      destroyed = true;      listeners.get("closed")?.();    }),    webContents: {      send,    },  };  return { pictureInPictureWindow, send };}; describe("PreviewManager", () => {  beforeEach(() => {    browserWindowConstructor.mockReset();    fromId.mockClear();    getFocusedWebContents.mockReset();    getFocusedWebContents.mockReturnValue(null);    mkdir.mockClear();    writeFile.mockClear();    showItemInFolder.mockClear();    writeImage.mockClear();    createFromPath.mockClear();    webviewSend.mockClear();  });   effectIt.effect("reports an unregistered webview as temporarily unavailable", () =>    withManager((manager) =>      Effect.gen(function* () {        expect(yield* manager.automationStatus("tab_1")).toEqual({          available: false,          visible: true,          tabId: "tab_1",          url: null,          title: null,          loading: false,        });         yield* manager.createTab("tab_1");         expect(yield* manager.automationStatus("tab_1")).toEqual({          available: false,          visible: true,          tabId: "tab_1",          url: null,          title: null,          loading: false,        });        expect(fromId).not.toHaveBeenCalled();      }),    ),  );   effectIt.effect("rejects a destroyed webview during registration", () =>    withManager((manager) =>      Effect.gen(function* () {        const getType = vi.fn(() => "webview" as const);        fromId.mockReturnValue({          id: 42,          isDestroyed: () => true,          getType,        } as never);        yield* manager.createTab("tab_destroyed_registration");         const exit = yield* Effect.exit(manager.registerWebview("tab_destroyed_registration", 42));         expect(Exit.isFailure(exit)).toBe(true);        if (Exit.isFailure(exit)) {          expect(Option.getOrThrow(Cause.findErrorOption(exit.cause))).toMatchObject({            _tag: "PreviewWebContentsNotFoundError",            tabId: "tab_destroyed_registration",            webContentsId: 42,          });        }        expect(getType).not.toHaveBeenCalled();      }),    ),  );   effectIt.effect("isolates failed state listeners and continues delivery", () => {    const loggedErrors: Array<unknown> = [];    const logger = Logger.make(({ message }) => {      for (const value of Array.isArray(message) ? message : [message]) {        if (typeof value === "object" && value !== null && "cause" in value) {          loggedErrors.push(Cause.squash(value.cause as Cause.Cause<never>));        }      }    });    const deliveryError = new ElectronWindow.ElectronWindowOperationError({      operation: "send-window-message",      platform: "darwin",      windowId: 42,      channel: "preview:state-change",      cause: new Error("renderer unavailable"),    });    const delivered = vi.fn();     return withManager((manager) =>      Effect.gen(function* () {        yield* manager.subscribeStateChanges(() => Effect.die(deliveryError));        yield* manager.subscribeStateChanges((tabId, state) =>          Effect.sync(() => {            delivered(tabId, state);          }),        );         const state = yield* manager.createTab("tab_listener_failure");         expect(delivered).toHaveBeenCalledOnce();        expect(delivered).toHaveBeenCalledWith("tab_listener_failure", state);        expect(loggedErrors).toHaveLength(1);        expect(loggedErrors[0]).toBeInstanceOf(ElectronWindow.ElectronWindowOperationError);        expect(loggedErrors[0]).toMatchObject({          operation: "send-window-message",          windowId: 42,          channel: "preview:state-change",        });      }),    ).pipe(      Effect.provide(        Logger.layer([logger], {          mergeWithExisting: false,        }),      ),    );  });   effectIt.effect("does not swallow state listener interruption", () =>    withManager((manager) =>      Effect.gen(function* () {        const exit = yield* Effect.scoped(          Effect.gen(function* () {            yield* manager.subscribeStateChanges(() => Effect.interrupt);            return yield* Effect.exit(manager.createTab("tab_interrupted_listener"));          }),        );         expect(Exit.isFailure(exit)).toBe(true);        if (Exit.isFailure(exit)) {          expect(Cause.hasInterrupts(exit.cause)).toBe(true);        }      }),    ),  );   effectIt.effect("queues navigation until the webview registers", () =>    withManager((manager) =>      Effect.gen(function* () {        const loadURL = vi.fn(async () => undefined);        const listeners = new Map<string, (...args: never[]) => void>();        fromId.mockReturnValue({          id: 42,          isDestroyed: () => false,          getType: () => "webview",          getURL: () => "about:blank",          getTitle: () => "",          isLoading: () => false,          getZoomFactor: () => 1,          setZoomFactor: vi.fn(),          setAudioMuted: vi.fn(),          isCurrentlyAudible: () => false,          loadURL,          on: vi.fn((event: string, listener: (...args: never[]) => void) => {            listeners.set(event, listener);          }),          off: vi.fn(),          ipc: { on: vi.fn(), off: vi.fn() },          send: webviewSend,          navigationHistory: { canGoBack: () => false, canGoForward: () => false },          setWindowOpenHandler: vi.fn(),          debugger: {            isAttached: () => false,            attach: vi.fn(),            sendCommand: vi.fn(async () => undefined),            on: vi.fn(),            off: vi.fn(),          },        } as never);         yield* manager.navigate("tab_pending", "localhost:3200");         expect(yield* manager.automationStatus("tab_pending")).toEqual({          available: false,          visible: true,          tabId: "tab_pending",          url: "http://localhost:3200/",          title: "",          loading: true,        });         yield* manager.registerWebview("tab_pending", 42);        yield* Effect.yieldNow;         expect(loadURL).toHaveBeenCalledOnce();

Lines 1–500 of 3386.