Ctrl K
ComunidadGuías…

lib/billing-usage-history.test.ts

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

Archivo original ↗Preguntar a KiBot
// @vitest-environment jsdom
import { act, createElement, type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import {
  focusManager,
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { UsageHistoryResponse } from "@/lib/billing-types";

const mocks = vi.hoisted(() => ({ fetchHistory: vi.fn() }));
vi.mock("@/lib/billing-api", () => ({
  fetchUsageHistoryApi: mocks.fetchHistory,
}));
vi.mock("next-intl", () => ({
  useLocale: () => "en",
  useTranslations: () => (key: string) => key,
}));
vi.mock("@/lib/use-billing-query", () => ({
  useBillingSummary: () => ({
    includedUsd: 15,
    usageError: false,
    usage: {
      managedAi: true,
      periodStart: "2026-09-10",
      nextResetAt: "2026-10-10",
    },
  }),
  formatBudgetPercent: () => "1%",
}));
vi.mock("@/components/icon", () => ({ AppIcon: () => null }));
vi.mock("@/components/empty-state", () => ({
  EmptyState: ({ description }: { description: string }) =>
    createElement("p", null, description),
}));
vi.mock("@/components/billing/usage-budget", () => ({
  UsageLoadError: ({ onRetry }: { onRetry: () => void }) =>
    createElement(
      "div",
      { role: "alert" },
      "usageLoadFailed",
      createElement("button", { onClick: onRetry }, "retryUsage"),
    ),
}));
// 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 Container = ({ children }: { children?: ReactNode }) => children;
  const Button = ({
    children,
    onClick,
    disabled,
    "aria-label": label,
  }: {
    children?: ReactNode;
    onClick?: () => void;
    disabled?: boolean;
    "aria-label"?: string;
  }) =>
    createElement(
      "button",
      { onClick, disabled, "aria-label": label },
      children,
    );
  return {
    Button,
    Collapsible: Container,
    CollapsibleContent: Container,
    CollapsibleTrigger: Button,
    Select: Container,
    SelectContent: Container,
    SelectItem: Container,
    SelectTrigger: Button,
    SelectValue: () => null,
    cn: (...values: Array<string | boolean | null | undefined>) =>
      values.filter(Boolean).join(" "),
  };
})());
});
vi.mock("@/components/ui/button", async (importOriginal) => {
  const stubs = await uiStubs();
  return { ...(await importOriginal<Record<string, unknown>>()), Button: stubs.Button };
});
vi.mock("@/components/ui/collapsible", async (importOriginal) => {
  const stubs = await uiStubs();
  return { ...(await importOriginal<Record<string, unknown>>()), Collapsible: stubs.Collapsible, CollapsibleContent: stubs.CollapsibleContent, CollapsibleTrigger: stubs.CollapsibleTrigger };
});
vi.mock("@/components/ui/select", async (importOriginal) => {
  const stubs = await uiStubs();
  return { ...(await importOriginal<Record<string, unknown>>()), Select: stubs.Select, SelectContent: stubs.SelectContent, SelectItem: stubs.SelectItem, SelectTrigger: stubs.SelectTrigger, SelectValue: stubs.SelectValue };
});

const { UsageHistorySection } = await import(
  "@/components/billing/usage-history-section"
);
const history: UsageHistoryResponse = {
  total: 26,
  entries: [
    {
      runId: "run-1",
      segmentId: "numo",
      feature: "numo_chat",
      at: "2026-10-02T12:00:00Z",
      projectName: "Loaded project",
      usd: 0.15,
    },
  ],
};
let root: Root;
let container: HTMLDivElement;
let client: QueryClient;

beforeEach(() => {
  vi.useFakeTimers();
  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
  focusManager.setFocused(true);
  mocks.fetchHistory.mockReset();
  client = new QueryClient({
    defaultOptions: { queries: { retry: false, gcTime: Infinity } },
  });
  container = document.createElement("div");
  document.body.append(container);
  root = createRoot(container);
});
afterEach(async () => {
  await act(() => root.unmount());
  client.clear();
  container.remove();
  focusManager.setFocused(undefined);
  vi.useRealTimers();
  vi.unstubAllGlobals();
});
async function renderHistory() {
  await act(() =>
    root.render(
      createElement(
        QueryClientProvider,
        { client },
        createElement(UsageHistorySection),
      ),
    ),
  );
  await act(() => vi.advanceTimersByTimeAsync(1));
}
async function failNextPoll() {
  mocks.fetchHistory.mockRejectedValueOnce(
    new Error("Temporary network failure"),
  );
  await act(() => vi.advanceTimersByTimeAsync(60_000));
  expect(mocks.fetchHistory).toHaveBeenCalledTimes(2);
}

describe("usage history refresh failures", () => {
  it("preserves loaded rows and pagination when the 60-second poll fails", async () => {
    mocks.fetchHistory.mockResolvedValueOnce(history);
    await renderHistory();
    expect(container.textContent).toContain("Loaded project");
    await failNextPoll();
    expect(container.textContent).toContain("Loaded project");
    expect(
      container.querySelector('[aria-label="historyNext"]'),
    ).not.toBeNull();
    expect(container.querySelector('[role="status"]')?.textContent).toBe(
      "usageRefreshFailed",
    );
    expect(container.querySelector('[role="alert"]')).toBeNull();
  });

  it("retains a successfully loaded empty history after a failed poll", async () => {
    mocks.fetchHistory.mockResolvedValueOnce({ total: 0, entries: [] });
    await renderHistory();
    await failNextPoll();
    expect(container.textContent).toContain("historyEmpty");
    expect(container.querySelector('[role="status"]')?.textContent).toBe(
      "usageRefreshFailed",
    );
    expect(container.querySelector('[role="alert"]')).toBeNull();
  });

  it("shows the initial-load error and can retry when no cached history exists", async () => {
    mocks.fetchHistory.mockRejectedValueOnce(
      new Error("Initial network failure"),
    );
    await renderHistory();
    expect(container.querySelector('[role="alert"]')?.textContent).toContain(
      "usageLoadFailed",
    );
    expect(container.querySelector('[role="status"]')).toBeNull();
    mocks.fetchHistory.mockResolvedValueOnce(history);
    await act(async () => {
      container
        .querySelector<HTMLButtonElement>('[role="alert"] button')!
        .click();
      await vi.advanceTimersByTimeAsync(1);
    });
    expect(container.textContent).toContain("Loaded project");
    expect(container.querySelector('[role="alert"]')).toBeNull();
  });
});