






















@@ -1,9 +1,18 @@
11/* @vitest-environment jsdom */
2233import { render } from "lit";
4-import { describe, expect, it } from "vitest";
5-import { renderSessionsCard, renderUsageInsights } from "./usage-render-overview.ts";
6-import type { UsageAggregates, UsageSessionEntry, UsageTotals } from "./usageTypes.ts";
4+import { afterEach, describe, expect, it, vi } from "vitest";
5+import {
6+renderDailyChartCompact,
7+renderSessionsCard,
8+renderUsageInsights,
9+} from "./usage-render-overview.ts";
10+import type {
11+CostDailyEntry,
12+UsageAggregates,
13+UsageSessionEntry,
14+UsageTotals,
15+} from "./usageTypes.ts";
716817const totals: UsageTotals = {
918input: 100,
@@ -40,6 +49,89 @@ const aggregates = {
4049daily: [],
4150} as unknown as UsageAggregates;
425152+function rect(left: number, top: number, width: number, height: number): DOMRect {
53+return {
54+x: left,
55+y: top,
56+ left,
57+ top,
58+ width,
59+ height,
60+right: left + width,
61+bottom: top + height,
62+toJSON: () => ({}),
63+} as DOMRect;
64+}
65+66+function setViewport(width: number, height: number) {
67+Object.defineProperty(window, "innerWidth", { configurable: true, value: width });
68+Object.defineProperty(window, "innerHeight", { configurable: true, value: height });
69+}
70+71+function mockTooltipRect(width: number, height: number) {
72+vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
73+function (this: HTMLElement) {
74+if (this.classList.contains("daily-bar-tooltip--floating")) {
75+return rect(0, 0, width, height);
76+}
77+return rect(0, 0, 0, 0);
78+},
79+);
80+}
81+82+function mockElementRect(
83+element: HTMLElement,
84+left: number,
85+top: number,
86+width: number,
87+height: number,
88+) {
89+Object.defineProperty(element, "getBoundingClientRect", {
90+configurable: true,
91+value: () => rect(left, top, width, height),
92+});
93+}
94+95+function dailyEntry(date: string, totalTokens: number, totalCost = 0): CostDailyEntry {
96+return {
97+ ...totals,
98+ date,
99+input: totalTokens,
100+output: 0,
101+cacheRead: 0,
102+cacheWrite: 0,
103+ totalTokens,
104+ totalCost,
105+};
106+}
107+108+function renderDailyChart(
109+daily: CostDailyEntry[],
110+onSelectDay = vi.fn<(day: string, shiftKey: boolean) => void>(),
111+) {
112+const container = document.createElement("div");
113+document.body.append(container);
114+render(
115+renderDailyChartCompact(daily, [], "tokens", "total", () => {}, onSelectDay),
116+container,
117+);
118+return {
119+ container,
120+ onSelectDay,
121+bars: Array.from(container.querySelectorAll<HTMLElement>(".daily-bar-wrapper")),
122+};
123+}
124+125+function getFloatingTooltip(): HTMLElement | null {
126+return document.body.querySelector(".daily-bar-tooltip--floating");
127+}
128+129+afterEach(() => {
130+document.body.replaceChildren();
131+window.dispatchEvent(new Event("scroll"));
132+vi.restoreAllMocks();
133+});
134+43135function directText(element: Element | null | undefined): string | undefined {
44136return Array.from(element?.childNodes ?? [])
45137.filter((node) => node.nodeType === Node.TEXT_NODE)
@@ -92,6 +184,101 @@ describe("renderUsageInsights", () => {
92184});
93185});
94186187+describe("renderDailyChartCompact", () => {
188+it("shows one floating tooltip for tall and short daily bars and hides it on mouse leave", () => {
189+setViewport(800, 600);
190+mockTooltipRect(180, 64);
191+const { bars } = renderDailyChart([
192+dailyEntry("2026-05-01", 1_200_000, 3.5),
193+dailyEntry("2026-05-02", 4, 0.01),
194+]);
195+196+mockElementRect(bars[0], 100, 100, 24, 200);
197+bars[0].dispatchEvent(new MouseEvent("mouseenter"));
198+199+let tooltip = getFloatingTooltip();
200+expect(tooltip).not.toBeNull();
201+expect(tooltip?.textContent).toContain("1.2M tokens");
202+expect(tooltip?.style.top).toBe("28px");
203+expect(document.body.querySelectorAll(".daily-bar-tooltip--floating")).toHaveLength(1);
204+205+bars[0].dispatchEvent(new MouseEvent("mouseleave"));
206+expect(getFloatingTooltip()).toBeNull();
207+208+mockElementRect(bars[1], 200, 320, 24, 6);
209+bars[1].dispatchEvent(new MouseEvent("mouseenter"));
210+211+tooltip = getFloatingTooltip();
212+expect(tooltip).not.toBeNull();
213+expect(tooltip?.textContent).toContain("4 tokens");
214+bars[1].dispatchEvent(new MouseEvent("mouseleave"));
215+});
216+217+it("flips below when the bar is near the top and clamps inside a narrow viewport", () => {
218+setViewport(120, 140);
219+mockTooltipRect(100, 40);
220+const { bars } = renderDailyChart([dailyEntry("2026-05-03", 10_000, 1)]);
221+222+mockElementRect(bars[0], 110, 12, 20, 20);
223+bars[0].dispatchEvent(new MouseEvent("mouseenter"));
224+225+const tooltip = getFloatingTooltip();
226+expect(tooltip?.dataset.placement).toBe("below");
227+expect(tooltip?.style.top).toBe("40px");
228+expect(tooltip?.style.left).toBe("12px");
229+bars[0].dispatchEvent(new MouseEvent("mouseleave"));
230+});
231+232+it("clears the floating tooltip when the chart DOM is removed", async () => {
233+setViewport(800, 600);
234+mockTooltipRect(160, 56);
235+const { bars, container } = renderDailyChart([dailyEntry("2026-05-04", 500, 0.2)]);
236+mockElementRect(bars[0], 300, 220, 24, 80);
237+238+bars[0].dispatchEvent(new MouseEvent("mouseenter"));
239+expect(getFloatingTooltip()).not.toBeNull();
240+241+container.remove();
242+await Promise.resolve();
243+expect(getFloatingTooltip()).toBeNull();
244+});
245+246+it("shows on keyboard focus, hides on blur, and keeps day selection operable", () => {
247+setViewport(800, 600);
248+mockTooltipRect(160, 56);
249+const { bars, onSelectDay } = renderDailyChart([dailyEntry("2026-05-04", 500, 0.2)]);
250+mockElementRect(bars[0], 300, 220, 24, 80);
251+252+bars[0].dispatchEvent(new Event("focus"));
253+expect(getFloatingTooltip()?.textContent).toContain("500 tokens");
254+255+bars[0].dispatchEvent(new Event("blur"));
256+expect(getFloatingTooltip()).toBeNull();
257+258+bars[0].dispatchEvent(new MouseEvent("click", { bubbles: true, shiftKey: true }));
259+expect(onSelectDay).toHaveBeenCalledWith("2026-05-04", true);
260+261+bars[0].dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
262+expect(onSelectDay).toHaveBeenCalledWith("2026-05-04", false);
263+264+const space = new KeyboardEvent("keydown", {
265+bubbles: true,
266+cancelable: true,
267+key: " ",
268+shiftKey: true,
269+});
270+bars[0].dispatchEvent(space);
271+expect(space.defaultPrevented).toBe(true);
272+expect(onSelectDay).toHaveBeenCalledWith("2026-05-04", true);
273+274+bars[0].dispatchEvent(new MouseEvent("mouseenter"));
275+bars[0].dispatchEvent(new Event("pointerdown", { bubbles: true }));
276+bars[0].dispatchEvent(new Event("focus"));
277+bars[0].dispatchEvent(new MouseEvent("mouseleave"));
278+expect(getFloatingTooltip()).toBeNull();
279+});
280+});
281+95282describe("renderSessionsCard", () => {
96283const noop = () => {};
97284此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。