| ... | @@ -0,0 +1,179 @@ |
| 1 | import { EnvironmentId, ProviderInstanceId, USAGE_CONTRACT_VERSION } from "@t3tools/contracts"; |
| 2 | import { mergeUsage } from "@t3tools/shared/usageMerge"; |
| 3 | import { act } from "react"; |
| 4 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 5 | import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; |
| 6 | |
| 7 | const state = vi.hoisted(() => ({ |
| 8 | presentations: new Map(), |
| 9 | refreshProviders: vi.fn(async () => undefined), |
| 10 | })); |
| 11 | vi.mock("@effect/atom-react", () => ({ useAtomValue: () => state.presentations })); |
| 12 | vi.mock("../../state/presentation", () => ({ |
| 13 | environmentPresentations: { presentationsAtom: null }, |
| 14 | })); |
| 15 | vi.mock("../../state/server", () => ({ serverEnvironment: { refreshProviders: null } })); |
| 16 | vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => state.refreshProviders })); |
| 17 | vi.mock("../../env", () => ({ isElectron: false })); |
| 18 | vi.mock("../../hooks/useSettings", () => ({ usePrimarySettings: () => "24h" })); |
| 19 | vi.mock("../../state/usage", () => ({ |
| 20 | useUsage: () => ({ |
| 21 | merged: mergeUsage([], USAGE_CONTRACT_VERSION), |
| 22 | environments: [ |
| 23 | { |
| 24 | environmentId: EnvironmentId.make("test"), |
| 25 | label: "Test", |
| 26 | isPending: false, |
| 27 | error: null, |
| 28 | summary: null, |
| 29 | }, |
| 30 | ], |
| 31 | selectedEnvironments: [ |
| 32 | { |
| 33 | environmentId: EnvironmentId.make("test"), |
| 34 | label: "Test", |
| 35 | isPending: false, |
| 36 | error: null, |
| 37 | summary: null, |
| 38 | }, |
| 39 | ], |
| 40 | isPending: false, |
| 41 | isPartial: false, |
| 42 | refresh: async () => undefined, |
| 43 | }), |
| 44 | })); |
| 45 | vi.mock("./usagePagePreferences", () => ({ |
| 46 | readUsagePagePreferences: () => ({ metric: "limits", windowDays: 30 }), |
| 47 | saveUsagePagePreferences: vi.fn(), |
| 48 | })); |
| 49 | vi.mock("../ui/button", () => ({ Button: "button" })); |
| 50 | vi.mock("../ui/scroll-area", () => ({ ScrollArea: "div" })); |
| 51 | vi.mock("../ui/select", () => ({ |
| 52 | Select: "select", |
| 53 | SelectItem: "option", |
| 54 | SelectPopup: "div", |
| 55 | SelectTrigger: "div", |
| 56 | SelectValue: "span", |
| 57 | })); |
| 58 | vi.mock("../ui/sidebar", () => ({ SidebarInset: "div" })); |
| 59 | vi.mock("../ui/toggle-group", () => ({ Toggle: "button", ToggleGroup: "div" })); |
| 60 | vi.mock("../ui/tooltip", () => ({ Tooltip: "div", TooltipPopup: "div", TooltipTrigger: "div" })); |
| 61 | vi.mock("../ui/popover", () => ({ Popover: "div", PopoverPopup: "div", PopoverTrigger: "div" })); |
| 62 | vi.mock("../ui/menu", () => ({ |
| 63 | Menu: "div", |
| 64 | MenuCheckboxItem: "div", |
| 65 | MenuItem: "div", |
| 66 | MenuPopup: "div", |
| 67 | MenuSeparator: "hr", |
| 68 | MenuTrigger: "div", |
| 69 | })); |
| 70 | vi.mock("../WorkspaceBreadcrumb", () => ({ |
| 71 | WorkspaceBreadcrumb: "div", |
| 72 | WorkspaceBreadcrumbItem: "div", |
| 73 | WorkspaceBreadcrumbSeparator: "span", |
| 74 | })); |
| 75 | vi.mock("../WorkspacePageContainer", () => ({ WorkspacePageContainer: "main" })); |
| 76 | vi.mock("../WorkspacePageHeader", () => ({ WorkspacePageHeader: "header" })); |
| 77 | vi.mock("./UsageProviderChart", () => ({ UsageProviderChart: "div" })); |
| 78 | vi.mock("./UsagePriceOverrides", () => ({ UsagePriceOverrides: () => null })); |
| 79 | vi.mock("../chat/ProviderInstanceIcon", () => ({ ProviderInstanceIcon: () => null })); |
| 80 | vi.mock("../settings/RedactedSensitiveText", () => ({ RedactedSensitiveText: "span" })); |
| 81 | vi.mock("../settings/providerDriverMeta", () => ({ getDriverOption: () => ({ label: "Codex" }) })); |
| 82 | |
| 83 | import { UsagePage } from "./UsagePage"; |
| 84 | |
| 85 | let renderer: ReactTestRenderer; |
| 86 | beforeEach(() => { |
| 87 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |
| 88 | vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-09-11T12:00:00Z")); |
| 89 | state.refreshProviders.mockClear(); |
| 90 | state.presentations = new Map([ |
| 91 | [ |
| 92 | EnvironmentId.make("test"), |
| 93 | { |
| 94 | entry: { target: { label: "Test" } }, |
| 95 | connection: { phase: "connected" }, |
| 96 | serverConfig: { |
| 97 | providers: [ |
| 98 | { |
| 99 | instanceId: ProviderInstanceId.make("codex"), |
| 100 | driver: "codex", |
| 101 | enabled: true, |
| 102 | installed: true, |
| 103 | version: null, |
| 104 | status: "ready", |
| 105 | auth: { status: "authenticated" }, |
| 106 | checkedAt: "2026-09-11T12:00:00Z", |
| 107 | models: [], |
| 108 | slashCommands: [], |
| 109 | skills: [], |
| 110 | usageLimits: { |
| 111 | checkedAt: "2026-09-11T12:00:00Z", |
| 112 | windows: [ |
| 113 | { |
| 114 | id: "five_hour", |
| 115 | kind: "session", |
| 116 | label: "Session", |
| 117 | usedPercent: 40, |
| 118 | windowDurationMins: 300, |
| 119 | resetsAt: "2026-09-11T14:00:00Z", |
| 120 | }, |
| 121 | ], |
| 122 | }, |
| 123 | }, |
| 124 | ], |
| 125 | }, |
| 126 | }, |
| 127 | ], |
| 128 | ]); |
| 129 | }); |
| 130 | afterEach(async () => { |
| 131 | await act(() => renderer?.unmount()); |
| 132 | vi.restoreAllMocks(); |
| 133 | vi.unstubAllGlobals(); |
| 134 | }); |
| 135 | |
| 136 | it.each([0, 1])( |
| 137 | "refreshes the visible limits countdown with refresh button %i without switching tabs, even when quota is unchanged", |
| 138 | async (buttonIndex) => { |
| 139 | await act(() => { |
| 140 | renderer = create(<UsagePage />); |
| 141 | }); |
| 142 | expect( |
| 143 | JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)), |
| 144 | ).toContain("in 2h 0m"); |
| 145 | vi.mocked(Date.now).mockReturnValue(Date.parse("2026-09-11T12:30:00Z")); |
| 146 | await act(async () => { |
| 147 | renderer.root |
| 148 | .findAllByProps({ "aria-label": "Refresh limits" }) |
| 149 | .filter((node) => node.type === "button") |
| 150 | .at(buttonIndex)! |
| 151 | .props.onClick(); |
| 152 | }); |
| 153 | expect(state.refreshProviders).toHaveBeenCalledWith({ environmentId: "test", input: {} }); |
| 154 | expect( |
| 155 | JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)), |
| 156 | ).toContain("in 1h 30m"); |
| 157 | expect( |
| 158 | JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)), |
| 159 | ).not.toContain("in 2h 0m"); |
| 160 | }, |
| 161 | ); |
| 162 | |
| 163 | it("uses the current time when returning to limits from tokens", async () => { |
| 164 | await act(() => { |
| 165 | renderer = create(<UsagePage />); |
| 166 | }); |
| 167 | const selectMetric = (metric: string) => { |
| 168 | renderer.root |
| 169 | .findAll((node) => node.type === "div" && node.props["aria-label"] === "Usage metric")[0]! |
| 170 | .props.onValueChange([metric]); |
| 171 | }; |
| 172 | await act(() => selectMetric("tokens")); |
| 173 | vi.mocked(Date.now).mockReturnValue(Date.parse("2026-09-11T13:00:00Z")); |
| 174 | await act(() => selectMetric("limits")); |
| 175 | expect( |
| 176 | JSON.stringify(renderer.toJSON(), (key, value) => (key === "props" ? undefined : value)), |
| 177 | ).toContain("in 1h 0m"); |
| 178 | expect(state.refreshProviders).not.toHaveBeenCalled(); |
| 179 | }); |