| ... | @@ -5,6 +5,8 @@ import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; | ... | @@ -5,6 +5,8 @@ import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 5 | | 5 | |
| 6 | const testState = vi.hoisted(() => ({ | 6 | const testState = vi.hoisted(() => ({ |
| 7 | useUsage: vi.fn(), | 7 | useUsage: vi.fn(), |
| | 8 | metric: "cost" as "cost" | "tokens", |
| | 9 | breakdown: "time" as "model" | "time", |
| 8 | })); | 10 | })); |
| 9 | | 11 | |
| 10 | vi.mock("react", async (importOriginal) => { | 12 | vi.mock("react", async (importOriginal) => { |
| ... | @@ -24,9 +26,11 @@ vi.mock("react", async (importOriginal) => { | ... | @@ -24,9 +26,11 @@ vi.mock("react", async (importOriginal) => { |
| 24 | untilTime: "2026-08-11T12:37:00.000Z", | 26 | untilTime: "2026-08-11T12:37:00.000Z", |
| 25 | }, | 27 | }, |
| 26 | } | 28 | } |
| 27 | : initial === "model" | 29 | : initial === "cost" |
| 28 | ? "time" | 30 | ? testState.metric |
| 29 | : initial, | 31 | : initial === "model" |
| | 32 | ? testState.breakdown |
| | 33 | : initial, |
| 30 | vi.fn(), | 34 | vi.fn(), |
| 31 | ]), | 35 | ]), |
| 32 | }; | 36 | }; |
| ... | @@ -72,10 +76,40 @@ const providerTotals = (codex: number, claude: number) => | ... | @@ -72,10 +76,40 @@ const providerTotals = (codex: number, claude: number) => |
| 72 | ["claude", { costUsd: claude, totalTokens: claude * 1_000 }], | 76 | ["claude", { costUsd: claude, totalTokens: claude * 1_000 }], |
| 73 | ] as const); | 77 | ] as const); |
| 74 | | 78 | |
| | 79 | const modelTotals = Object.freeze([ |
| | 80 | { |
| | 81 | model: "expensive-model", |
| | 82 | provider: "claude" as const, |
| | 83 | costUsd: 10, |
| | 84 | totalTokens: 100, |
| | 85 | records: 1, |
| | 86 | costShare: 10 / 16, |
| | 87 | }, |
| | 88 | { |
| | 89 | model: "token-heavy-model", |
| | 90 | provider: "codex" as const, |
| | 91 | costUsd: 5, |
| | 92 | totalTokens: 1_000, |
| | 93 | records: 1, |
| | 94 | costShare: 5 / 16, |
| | 95 | }, |
| | 96 | { |
| | 97 | model: "token-heavy-cheaper-model", |
| | 98 | provider: "codex" as const, |
| | 99 | costUsd: 1, |
| | 100 | totalTokens: 1_000, |
| | 101 | records: 1, |
| | 102 | costShare: 1 / 16, |
| | 103 | }, |
| | 104 | ]); |
| | 105 | |
| 75 | beforeEach(() => { | 106 | beforeEach(() => { |
| | 107 | testState.metric = "cost"; |
| | 108 | testState.breakdown = "time"; |
| 76 | testState.useUsage.mockReturnValue({ | 109 | testState.useUsage.mockReturnValue({ |
| 77 | merged: { | 110 | merged: { |
| 78 | ...mergeUsage([], USAGE_CONTRACT_VERSION), | 111 | ...mergeUsage([], USAGE_CONTRACT_VERSION), |
| | 112 | models: modelTotals, |
| 79 | hourly: [ | 113 | hourly: [ |
| 80 | { | 114 | { |
| 81 | day: "2026-08-10", | 115 | day: "2026-08-10", |
| ... | @@ -110,4 +144,39 @@ describe("UsagePage hourly breakdown", () => { | ... | @@ -110,4 +144,39 @@ describe("UsagePage hourly breakdown", () => { |
| 110 | expect(body).toContain("$13.00"); | 144 | expect(body).toContain("$13.00"); |
| 111 | expect(body.indexOf("$11.00")).toBeLessThan(body.indexOf("$13.00")); | 145 | expect(body.indexOf("$11.00")).toBeLessThan(body.indexOf("$13.00")); |
| 112 | }); | 146 | }); |
| | 147 | |
| | 148 | it("keeps chronological ordering when the token metric is selected", () => { |
| | 149 | testState.metric = "tokens"; |
| | 150 | |
| | 151 | const markup = renderToStaticMarkup(<UsagePage />); |
| | 152 | const body = markup.match(/<tbody>(.*?)<\/tbody>/)?.[1] ?? ""; |
| | 153 | |
| | 154 | expect(body).toMatch(/\$11\.00.*\$13\.00/); |
| | 155 | }); |
| | 156 | }); |
| | 157 | |
| | 158 | describe("UsagePage model breakdown", () => { |
| | 159 | it("sorts models by cost when the cost metric is selected", () => { |
| | 160 | testState.breakdown = "model"; |
| | 161 | |
| | 162 | const markup = renderToStaticMarkup(<UsagePage />); |
| | 163 | const body = markup.match(/<tbody>(.*?)<\/tbody>/)?.[1] ?? ""; |
| | 164 | |
| | 165 | expect(body).toMatch(/expensive-model.*token-heavy-model.*token-heavy-cheaper-model/); |
| | 166 | }); |
| | 167 | |
| | 168 | it("sorts models by token usage when the token metric is selected", () => { |
| | 169 | testState.metric = "tokens"; |
| | 170 | testState.breakdown = "model"; |
| | 171 | |
| | 172 | const markup = renderToStaticMarkup(<UsagePage />); |
| | 173 | const body = markup.match(/<tbody>(.*?)<\/tbody>/)?.[1] ?? ""; |
| | 174 | |
| | 175 | expect(body).toMatch(/token-heavy-model.*token-heavy-cheaper-model.*expensive-model/); |
| | 176 | expect(modelTotals.map((model) => model.model)).toEqual([ |
| | 177 | "expensive-model", |
| | 178 | "token-heavy-model", |
| | 179 | "token-heavy-cheaper-model", |
| | 180 | ]); |
| | 181 | }); |
| 113 | }); | 182 | }); |