1import { EnvironmentId, UsageDay, USAGE_CONTRACT_VERSION } from "@t3tools/contracts";
2import { mergeUsage } from "@t3tools/shared/usageMerge";
3import { act } from "react";
4import { createRoot, type Root } from "react-dom/client";
5import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
6
7const testState = vi.hoisted(() => ({
8 useUsage: vi.fn(),
9 navigate: vi.fn(),
10 canGoBack: true,
11}));
12
13vi.mock("../../env", () => ({ isElectron: false }));
14vi.mock("@tanstack/react-router", () => ({
15 useNavigate: () => testState.navigate,
16 useCanGoBack: () => testState.canGoBack,
17}));
18vi.mock("../../state/usage", () => ({ useUsage: testState.useUsage }));
19vi.mock("../ui/scroll-area", () => ({ ScrollArea: "div" }));
20vi.mock("../ui/select", () => ({
21 Select: "div",
22 SelectItem: "div",
23 SelectPopup: "div",
24 SelectTrigger: "div",
25 SelectValue: "div",
26}));
27vi.mock("../ui/sidebar", () => ({ SidebarInset: "div" }));
28vi.mock("../ui/toggle-group", () => ({ Toggle: "button", ToggleGroup: "div" }));
29vi.mock("../WorkspaceBreadcrumb", () => ({
30 WorkspaceBreadcrumb: "div",
31 WorkspaceBreadcrumbItem: "div",
32 WorkspaceBreadcrumbSeparator: "span",
33 WorkspaceBreadcrumbText: "span",
34}));
35vi.mock("../WorkspacePageContainer", () => ({ WorkspacePageContainer: "main" }));
36vi.mock("../WorkspacePageHeader", () => ({ WorkspacePageHeader: "header" }));
37vi.mock("./UsageProviderChart", () => ({ UsageProviderChart: "div" }));
38vi.mock("./UsagePriceOverrides", () => ({ UsagePriceOverrides: () => null }));
39vi.mock("./usageProviders", async (importOriginal) => {
40 const actual = await importOriginal<typeof import("./usageProviders")>();
41 return {
42 ...actual,
43 PROVIDER_ORDER: ["codex", "claude"],
44 PROVIDER_PRESENTATION: {
45 codex: { color: "white", label: "Codex", mark: "span" },
46 claude: { color: "orange", label: "Claude Code", mark: "span" },
47 },
48 };
49});
50
51import { UsagePage } from "./UsagePage";
52const environments = [
53 {
54 environmentId: EnvironmentId.make("test-environment"),
55 label: "Test environment",
56 isPending: false,
57 canReadDiagnostics: true,
58 error: null,
59 summary: {
60 contractVersion: USAGE_CONTRACT_VERSION,
61 readAt: "2026-08-11T12:37:00.000Z",
62 sinceDay: UsageDay.make("2026-08-10"),
63 untilDay: UsageDay.make("2026-08-11"),
64 timeZone: "UTC",
65 buckets: [],
66 sources: [],
67 pricing: { status: "fresh", source: "test", fetchedAt: null, knownModels: 1 },
68 scanDurationMs: 1,
69 },
70 },
71];
72
73beforeEach(() => {
74 testState.useUsage.mockReturnValue({
75 merged: mergeUsage([], USAGE_CONTRACT_VERSION),
76 environments,
77 selectedEnvironments: environments,
78 isPending: false,
79 shown: null,
80 isPartial: false,
81 refresh: vi.fn(),
82 });
83});
84
85describe("UsagePage Escape navigation", () => {
86 let renderer: Root;
87 let container: HTMLDivElement;
88 let back: ReturnType<typeof vi.spyOn>;
89
90 beforeEach(async () => {
91 back = vi.spyOn(window.history, "back").mockImplementation(() => {});
92 testState.navigate.mockClear();
93 testState.canGoBack = true;
94 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
95 container = document.createElement("div");
96 document.body.append(container);
97 renderer = createRoot(container);
98 await act(() => {
99 renderer.render(<UsagePage />);
100 });
101 });
102
103 afterEach(async () => {
104 await act(() => renderer.unmount());
105 container.remove();
106 back.mockRestore();
107 vi.unstubAllGlobals();
108 });
109
110 function escape(properties: { repeat?: boolean; isComposing?: boolean } = {}) {
111 return new KeyboardEvent("keydown", {
112 bubbles: true,
113 cancelable: true,
114 key: "Escape",
115 ...properties,
116 });
117 }
118
119 it("returns to the previous page on Escape", () => {
120 document.body.dispatchEvent(escape());
121 expect(back).toHaveBeenCalledOnce();
122 expect(testState.navigate).not.toHaveBeenCalled();
123 });
124
125 it("returns home when there is no previous app page", async () => {
126 testState.canGoBack = false;
127 await act(() => renderer.render(<UsagePage />));
128
129 document.body.dispatchEvent(escape());
130 expect(testState.navigate).toHaveBeenCalledWith({ to: "/" });
131 expect(back).not.toHaveBeenCalled();
132 });
133
134 it("closes the environment menu before Escape navigates back", async () => {
135 const trigger = container.querySelector<HTMLButtonElement>('[data-slot="menu-trigger"]')!;
136 await act(() => trigger.click());
137 expect(document.querySelector('[role="menu"]')).not.toBeNull();
138
139 await act(() => {
140 document.activeElement!.dispatchEvent(escape());
141 });
142 expect(trigger.getAttribute("aria-expanded")).toBe("false");
143 expect(back).not.toHaveBeenCalled();
144
145 document.body.dispatchEvent(escape());
146 expect(back).toHaveBeenCalledOnce();
147 });
148
149 it.each([{ repeat: true }, { isComposing: true }])("ignores Escape with %j", (properties) => {
150 document.body.dispatchEvent(escape(properties));
151 expect(back).not.toHaveBeenCalled();
152 expect(testState.navigate).not.toHaveBeenCalled();
153 });
154});
155
156// @vitest-environment jsdom