2071 lines · 74.9 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(),    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 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("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(),          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();        expect(loadURL).toHaveBeenCalledWith("http://localhost:3200/");      }),    ),  );   effectIt.effect("mirrors Electron's effective zoom across registration and navigation", () =>    withManager((manager) =>      Effect.gen(function* () {        let effectiveZoom = 0.9;        let zoomReadable = true;        let url = "https://example.com";        const listeners = new Map<string, (...args: unknown[]) => void>();        const setZoomFactor = vi.fn();        fromId.mockReturnValue({          id: 42,          isDestroyed: () => false,          getType: () => "webview",          getURL: () => url,          getTitle: () => "Example",          isLoading: () => false,          getZoomFactor: () => {            if (!zoomReadable) throw new Error("zoom unavailable");            return effectiveZoom;          },          setZoomFactor,          on: vi.fn((event: string, listener: (...args: unknown[]) => 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);        const states: PreviewManager.PreviewTabState[] = [];         yield* manager.subscribeStateChanges((_tabId, state) =>          Effect.sync(() => {            states.push(state);          }),        );        yield* manager.createTab("tab_zoom");        yield* manager.registerWebview("tab_zoom", 42);         expect(states.at(-1)?.zoomFactor).toBe(0.9);        expect(setZoomFactor).not.toHaveBeenCalled();         effectiveZoom = 1.25;        listeners.get("did-navigate")?.();        yield* Effect.yieldNow;         expect(states.at(-1)?.zoomFactor).toBe(1.25);        expect(setZoomFactor).not.toHaveBeenCalled();         zoomReadable = false;        url = "https://example.com/after-zoom-read-failed";        listeners.get("did-navigate")?.();        yield* Effect.yieldNow;         expect(states.at(-1)?.navStatus).toEqual({          kind: "Success",          url,          title: "Example",        });        expect(states.at(-1)?.zoomFactor).toBe(1.25);         const replacementSetZoomFactor = vi.fn();        fromId.mockReturnValue({          id: 43,          isDestroyed: () => false,          getType: () => "webview",          getURL: () => url,          getTitle: () => "Example",          isLoading: () => false,          getZoomFactor: () => 1,          setZoomFactor: replacementSetZoomFactor,          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(),          },        } as never);         yield* manager.registerWebview("tab_zoom", 43);         expect(replacementSetZoomFactor).toHaveBeenCalledWith(1.25);        expect(states.at(-1)?.zoomFactor).toBe(1.25);      }),    ),  );   effectIt.effect("emulates prefers-color-scheme and re-applies it across webview swaps", () =>    withManager((manager) =>      Effect.gen(function* () {        const makeWebContents = (id: number) => {          const sendCommand = vi.fn(async () => undefined);          return {            sendCommand,            wc: {              id,              isDestroyed: () => false,              isDevToolsOpened: () => false,              getType: () => "webview",              getURL: () => "https://example.com",              getTitle: () => "Example",              isLoading: () => false,              getZoomFactor: () => 1,              setZoomFactor: vi.fn(),              on: vi.fn(),              off: vi.fn(),              ipc: { on: vi.fn(), off: vi.fn() },              send: webviewSend,              navigationHistory: { canGoBack: () => false, canGoForward: () => false },

Lines 1–500 of 2071.