Ctrl K
ComunidadGuías…

lib/account-desktop-section.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// @vitest-environment jsdom
// @vitest-environment-options {"url":"https://www.minddy.app/account/preferences"}

import { act, createElement } from "react";
import type * as React from "react";
import { createRoot, type Root } from "react-dom/client";
import { NextIntlClientProvider } from "next-intl";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { AccountDesktopSection } from "@/components/settings/account-desktop-section";
import type { DesktopBridge } from "@/lib/desktop/bridge";
import messages from "@/messages/en.json";

// The hint of each row measures itself with a ResizeObserver to decide
// whether it truncates (MIN-593). jsdom has none.
vi.stubGlobal(
  "ResizeObserver",
  class {
    observe() {}
    unobserve() {}
    disconnect() {}
  },
);

// Stubs for the UI primitives the component under test renders.
const uiStubs = vi.hoisted(() => {
  let value: Promise<Record<string, unknown>> | undefined;
  return () => (value ??= (async () => {
  const { createElement: element, Fragment } = await import("react");
  return {
    cn: (...values: unknown[]) => values.filter(Boolean).join(" "),
    Button: ({
      children,
      variant: _variant,
      size: _size,
      ...props
    }: React.ButtonHTMLAttributes<HTMLButtonElement> & {
      variant?: string;
      size?: string;
    }) => element("button", props, children),
    Switch: ({
      checked,
      onCheckedChange,
      ...props
    }: Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> & {
      checked: boolean;
      onCheckedChange: (checked: boolean) => void;
    }) =>
      element("button", {
        ...props,
        role: "switch",
        "aria-checked": checked,
        onClick: () => onCheckedChange(!checked),
      }),
    Popover: ({ children }: React.PropsWithChildren) => element(Fragment, null, children),
    PopoverTrigger: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) =>
      element("button", props, children),
    PopoverContent: ({ children }: React.PropsWithChildren) =>
      element("div", null, children),
    // The hint renders its overflow tooltip through `components/ui/tooltip`
    // (built on Radix). The test needs the tooltip JSX to pass children
    // through, nothing more.
    Tooltip: ({ children }: React.PropsWithChildren) => element(Fragment, null, children),
    TooltipTrigger: ({
      children,
      ...props
    }: React.HTMLAttributes<HTMLElement>) => element("span", props, children),
    TooltipContent: ({ children }: React.PropsWithChildren) =>
      element("div", null, children),
  };
})());
});
vi.mock("@/components/ui/button", async (importOriginal) => {
  const stubs = await uiStubs();
  return { ...(await importOriginal<Record<string, unknown>>()), Button: stubs.Button };
});
vi.mock("@/components/ui/switch", async (importOriginal) => {
  const stubs = await uiStubs();
  return { ...(await importOriginal<Record<string, unknown>>()), Switch: stubs.Switch };
});
vi.mock("@/components/ui/popover", async (importOriginal) => {
  const stubs = await uiStubs();
  return { ...(await importOriginal<Record<string, unknown>>()), Popover: stubs.Popover, PopoverTrigger: stubs.PopoverTrigger, PopoverContent: stubs.PopoverContent };
});
// The web download row links through the app router, which a unit render
// does not mount; a plain anchor keeps the destination observable.
vi.mock("@/components/app-link", async () => {
  const { createElement: element } = await import("react");
  return {
    default: ({ children, href, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) =>
      element("a", { ...props, href }, children),
  };
});
vi.mock("@/components/ui/tooltip", async (importOriginal) => {
  const stubs = await uiStubs();
  return { ...(await importOriginal<Record<string, unknown>>()), Tooltip: stubs.Tooltip, TooltipTrigger: stubs.TooltipTrigger, TooltipContent: stubs.TooltipContent };
});

type BridgeMocks = {
  bridge: DesktopBridge;
  openServerPicker: ReturnType<typeof vi.fn>;
  checkForUpdates: ReturnType<typeof vi.fn>;
};

function currentBridge(): BridgeMocks {
  const openServerPicker = vi.fn();
  const checkForUpdates = vi.fn().mockResolvedValue(undefined);
  const bridge = {
    version: "1.2.3",
    platform: "win32",
    openServerPicker,
    checkForUpdates,
  } as unknown as DesktopBridge;
  return {
    bridge,
    openServerPicker,
    checkForUpdates,
  };
}

function legacyBridge(): DesktopBridge {
  return {
    version: "1.0.0",
    platform: "linux",
  } as unknown as DesktopBridge;
}

describe("AccountDesktopSection", () => {
  let container: HTMLDivElement;
  let root: Root;

  beforeEach(() => {
    (window as typeof window & { IS_REACT_ACT_ENVIRONMENT: boolean })
      .IS_REACT_ACT_ENVIRONMENT = true;
    container = document.createElement("div");
    document.body.append(container);
    root = createRoot(container);
  });

  afterEach(() => {
    act(() => root.unmount());
    container.remove();
    delete window.minddy;
    vi.restoreAllMocks();
    delete (window as typeof window & { IS_REACT_ACT_ENVIRONMENT?: boolean })
      .IS_REACT_ACT_ENVIRONMENT;
  });

  async function renderSection() {
    await act(async () => {
      root.render(
        createElement(NextIntlClientProvider, {
          locale: "en",
          messages,
          children: createElement(AccountDesktopSection),
        }),
      );
    });
  }

  function button(label: string): HTMLButtonElement {
    const match = Array.from(container.querySelectorAll("button")).find(
      (candidate) => candidate.textContent === label,
    );
    if (!match) throw new Error(`Missing button: ${label}`);
    return match;
  }

  it("offers the download in a browser instead of the app controls", async () => {
    await renderSection();
    // One card in every context (MIN-737): on the web it carries the link
    // that used to sit in the profile, never controls the bridge would own.
    expect(container.textContent).toContain(messages.Account.desktopSectionTitle);
    expect(container.querySelector('a[href="/download"]')).not.toBeNull();
  });

  it("renders Cloud state and routes each control to its argument-free bridge action", async () => {
    const mocks = currentBridge();
    window.minddy = mocks.bridge;
    await renderSection();

    expect(container.textContent).toContain("https://www.minddy.app");
    expect(container.textContent).toContain("minddy 1.2.3");
    // The preview channel is retired (MIN-765): no switch leads to an origin
    // whose accounts differ from minddy Cloud.
    expect(container.querySelector('button[role="switch"]')).toBeNull();

    act(() => button("Change…").click());
    expect(mocks.openServerPicker).toHaveBeenCalledWith();
    expect(mocks.checkForUpdates).not.toHaveBeenCalled();

    await act(async () => button("Check for updates…").click());
    expect(mocks.checkForUpdates).toHaveBeenCalledWith();
    expect(container.textContent).not.toContain("Copy report…");
  });

  it("labels the connected server with its origin", async () => {
    window.minddy = currentBridge().bridge;
    await renderSection();

    expect(container.textContent).toContain("Connected server");
    expect(container.textContent).toContain(window.location.origin);
  });

  it("keeps the card compatible with a legacy shell", async () => {
    window.minddy = legacyBridge();
    await renderSection();

    expect(container.textContent).toContain("minddy 1.0.0");
    expect(container.textContent).not.toContain("Change…");
    expect(container.textContent).not.toContain("Check for updates…");
  });
});