feat(dashboard): show exact token counts on hover in usage analytics cards and tables (#12553)

* feat(dashboard): show exact token counts on hover in usage analytics cards and tables

* fix(dashboard): lock tooltip position to prevent top-left slide animation

---------

Co-authored-by: ZaimMarzuki <ZaimMarzuki@users.noreply.github.com>
This commit is contained in:
ZaimMarzuki
2026-09-18 22:21:52 +07:00
committed by GitHub
parent 43758c7885
commit 060f70ed18
6 changed files with 488 additions and 59 deletions

View File

@@ -24,9 +24,10 @@ import { createPortal } from "react-dom";
interface TooltipProps {
children: ReactNode;
content?: string;
content?: ReactNode;
position?: "top" | "bottom" | "left" | "right";
className?: string;
tooltipClassName?: string;
delayMs?: number;
/**
* Issue #2352: Render the tooltip in a React portal so it escapes the
@@ -52,6 +53,7 @@ export default function Tooltip({
content,
position = "top",
className = "",
tooltipClassName = "",
delayMs = 200,
usePortal = true,
multiline = false,
@@ -60,7 +62,7 @@ export default function Tooltip({
const tooltipId = useId();
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const wrapperRef = useRef<HTMLSpanElement | null>(null);
const tooltipRef = useRef<HTMLSpanElement | null>(null);
const tooltipRef = useRef<HTMLDivElement | null>(null);
const show = useCallback(() => {
clearTimeout(timeoutRef.current);
@@ -90,39 +92,41 @@ export default function Tooltip({
if (!wrap || !tt) return;
const rect = wrap.getBoundingClientRect();
const tRect = tt.getBoundingClientRect();
const scrollY = window.scrollY;
const scrollX = window.scrollX;
let top = 0;
let left = 0;
switch (position) {
case "bottom":
top = rect.bottom + scrollY + 8;
left = rect.left + scrollX + rect.width / 2 - tRect.width / 2;
top = rect.bottom + 8;
left = rect.left + rect.width / 2 - tRect.width / 2;
break;
case "left":
top = rect.top + scrollY + rect.height / 2 - tRect.height / 2;
left = rect.left + scrollX - tRect.width - 8;
top = rect.top + rect.height / 2 - tRect.height / 2;
left = rect.left - tRect.width - 8;
break;
case "right":
top = rect.top + scrollY + rect.height / 2 - tRect.height / 2;
left = rect.right + scrollX + 8;
top = rect.top + rect.height / 2 - tRect.height / 2;
left = rect.right + 8;
break;
case "top":
default:
top = rect.top + scrollY - tRect.height - 8;
left = rect.left + scrollX + rect.width / 2 - tRect.width / 2;
top = rect.top - tRect.height - 8;
left = rect.left + rect.width / 2 - tRect.width / 2;
break;
}
// Clamp horizontally inside the viewport so a trigger near the right
// edge does not produce a tooltip that bleeds off the screen.
const margin = 8;
const maxLeft = window.innerWidth + scrollX - tRect.width - margin;
const minLeft = scrollX + margin;
const maxLeft = window.innerWidth - tRect.width - margin;
const minLeft = margin;
if (left > maxLeft) left = maxLeft;
if (left < minLeft) left = minLeft;
// Lock position immediately without any slide/flying animation from (0,0).
tt.style.transition = "none";
tt.style.top = `${top}px`;
tt.style.left = `${left}px`;
tt.style.visibility = "visible";
tt.style.opacity = "1";
}, [visible, usePortal, position, content]);
const positionClasses = {
@@ -149,14 +153,13 @@ export default function Tooltip({
);
const widthClass = multiline ? "max-w-xs whitespace-normal break-words" : "whitespace-nowrap";
const baseTooltipClass =
"z-50 px-2.5 py-1.5 text-xs font-medium text-white bg-gray-900/95 rounded-md shadow-lg pointer-events-none animate-in fade-in duration-150 motion-reduce:transition-none motion-reduce:animate-none border border-white/10";
const baseTooltipClass = `z-50 px-3 py-2 text-xs font-medium text-white bg-[#10141e]/95 rounded-lg shadow-xl pointer-events-none transition-opacity duration-150 motion-reduce:transition-none border border-white/10 backdrop-blur-sm ${tooltipClassName}`;
const portalEnabled = usePortal && typeof window !== "undefined";
const tooltipEl =
visible && content ? (
<span
<div
ref={tooltipRef}
id={tooltipId}
role="tooltip"
@@ -165,13 +168,15 @@ export default function Tooltip({
? `fixed ${baseTooltipClass} ${widthClass}`
: `absolute ${baseTooltipClass} ${widthClass} ${positionClasses[position] || positionClasses.top}`
}
// For portal-rendered tooltips, mount off-screen + hidden so the
// layout effect can measure dimensions before the user sees a flash.
// The useLayoutEffect above promotes visibility once coords are set.
style={portalEnabled ? { top: -9999, left: -9999, visibility: "hidden" } : undefined}
// For portal-rendered tooltips, mount hidden with opacity 0 at origin so
// layout effect sets the exact viewport coordinates before revealing it.
// This prevents the tooltip from flying or sliding in from top-left.
style={
portalEnabled ? { top: -9999, left: -9999, visibility: "hidden", opacity: 0 } : undefined
}
>
{content}
</span>
</div>
) : null;
return (

View File

@@ -275,24 +275,32 @@ export default function UsageAnalytics() {
icon="generating_tokens"
label={t("totalTokens")}
value={fmt(s.totalTokens)}
tooltip={fmtFull(s.totalTokens)}
subValue={`${fmtFull(s.totalRequests)} ${t("chartRequests")}`}
/>
<StatCard
icon="input"
label={t("inputTokens")}
value={fmt(s.promptTokens)}
tooltip={fmtFull(s.promptTokens)}
color="text-primary"
/>
<StatCard
icon="output"
label={t("outputTokens")}
value={fmt(s.completionTokens)}
tooltip={fmtFull(s.completionTokens)}
color="text-emerald-500"
/>
<StatCard
icon="payments"
label={t("estCost")}
value={fmtCost(s.totalCost)}
tooltip={
s.totalCost !== undefined && s.totalCost !== null
? `$${Number(s.totalCost).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}`
: undefined
}
color="text-amber-500"
/>
</div>
@@ -321,12 +329,16 @@ export default function UsageAnalytics() {
icon: "speed",
label: t("perfAvgTokens"),
value: fmt(avgTokensPerReq),
tooltip: `tokens : ${fmtFull(avgTokensPerReq)} tokens`,
color: "text-cyan-500",
},
{
icon: "request_quote",
label: t("perfCostReq"),
value: fmtCost(costPerReq),
tooltip: costPerReq
? `cost : $${Number(costPerReq).toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 6 })}`
: undefined,
color: "text-orange-500",
},
{
@@ -339,6 +351,7 @@ export default function UsageAnalytics() {
icon: "bolt",
label: t("perfFastReq"),
value: fmt(s.fastRequests || 0),
tooltip: `requests : ${fmtFull(s.fastRequests || 0)} requests`,
color: "text-sky-500",
},
],

View File

@@ -128,13 +128,22 @@ export function ModelTable({ byModel, summary }: ModelTableProps) {
<td className="px-4 py-2.5 text-right font-mono text-text-muted">
{fmtFull(m.requests)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-primary">
<td
className="px-4 py-2.5 text-right font-mono text-primary"
title={fmtFull(m.promptTokens)}
>
{fmt(m.promptTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-emerald-500">
<td
className="px-4 py-2.5 text-right font-mono text-emerald-500"
title={fmtFull(m.completionTokens)}
>
{fmt(m.completionTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono font-semibold">
<td
className="px-4 py-2.5 text-right font-mono font-semibold"
title={fmtFull(m.totalTokens)}
>
{fmt(m.totalTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-amber-500">

View File

@@ -92,7 +92,10 @@ export default function RequestCountTable({
<td className="px-4 py-2.5 text-right font-mono font-semibold">
{fmtFull(row.requests)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-text-muted">
<td
className="px-4 py-2.5 text-right font-mono text-text-muted"
title={fmtFull(row.totalTokens)}
>
{fmt(row.totalTokens)}
</td>
</tr>

View File

@@ -3,7 +3,7 @@
import { useState, useMemo, useCallback, useRef, useEffect } from "react";
import { useLocale, useTranslations } from "next-intl";
import Card from "../Card";
import { getModelColor } from "@/shared/constants/colors";
import Tooltip from "../Tooltip";
import { PROVIDER_COLORS } from "./chartColors";
import {
fmtCompact as fmt,
@@ -49,23 +49,63 @@ export function StatCard({
label,
value,
subValue,
tooltip,
color = "text-text-main",
}: {
icon: any;
label: any;
value: any;
subValue?: any;
tooltip?: string;
color?: string;
}) {
const isCost = String(label).toLowerCase().includes("cost");
const tooltipText = tooltip
? String(tooltip).includes(":")
? tooltip
: isCost
? `cost : ${String(tooltip).startsWith("$") ? tooltip : `$${tooltip}`}`
: `tokens : ${tooltip} tokens`
: null;
const valueElement = (
<span
className={`text-2xl font-bold ${color} truncate cursor-default`}
data-tooltip={tooltip}
title={tooltip ? undefined : String(value)}
>
{value}
</span>
);
return (
<Card className="px-4 py-3 flex flex-col gap-1 min-w-0">
<div className="flex items-center gap-1.5 text-text-muted text-[11px] uppercase font-semibold tracking-wide min-w-0">
<span className="material-symbols-outlined text-[14px] shrink-0">{icon}</span>
<span className="truncate">{label}</span>
</div>
<span className={`text-2xl font-bold ${color} truncate`} title={String(value)}>
{value}
</span>
{tooltipText ? (
<Tooltip
content={
<div className="flex flex-col gap-0.5 text-left py-0.5 min-w-[140px]">
<div className="font-semibold text-white/95 text-xs">{label}</div>
<div
className={`font-mono text-xs ${
color && color !== "text-text-main" ? color : "text-violet-400"
} tracking-wide`}
>
{tooltipText}
</div>
</div>
}
className="w-fit max-w-full"
delayMs={150}
>
{valueElement}
</Tooltip>
) : (
valueElement
)}
{subValue && <span className="text-xs text-text-muted truncate">{subValue}</span>}
</Card>
);
@@ -75,7 +115,7 @@ export function StatCard({
export type CompactStatSection = {
title: string;
items: Array<{ icon: string; label: string; value: any; color?: string }>;
items: Array<{ icon: string; label: string; value: any; tooltip?: string; color?: string }>;
/** On mobile use 1 column instead of 2 — useful when values can be long (model names, etc.) */
wideValues?: boolean;
};
@@ -99,28 +139,56 @@ export function CompactStatGrid({ sections }: { sections: CompactStatSection[] }
: "grid grid-cols-2 md:grid-cols-4 gap-x-5 gap-y-2"
}
>
{section.items.map((stat, i) => (
<div key={i} className="flex items-center justify-between gap-2 min-w-0 py-0.5">
<div
className={`flex items-center gap-1.5 ${section.wideValues ? "shrink-0" : "min-w-0"}`}
>
<span className="material-symbols-outlined text-[14px] text-text-muted shrink-0">
{stat.icon}
</span>
<span
className={`text-[11px] uppercase font-semibold tracking-wide text-text-muted ${section.wideValues ? "whitespace-nowrap" : "truncate"}`}
>
{stat.label}
</span>
</div>
{section.items.map((stat, i) => {
const statValueEl = (
<span
className={`text-sm font-bold text-right ${section.wideValues ? "truncate min-w-0" : "shrink-0"} ${stat.color || "text-text-main"}`}
title={String(stat.value)}
className={`text-sm font-bold text-right cursor-default ${section.wideValues ? "truncate min-w-0" : "shrink-0"} ${stat.color || "text-text-main"}`}
data-tooltip={stat.tooltip}
title={stat.tooltip ? undefined : String(stat.value)}
>
{stat.value}
</span>
</div>
))}
);
return (
<div key={i} className="flex items-center justify-between gap-2 min-w-0 py-0.5">
<div
className={`flex items-center gap-1.5 ${section.wideValues ? "shrink-0" : "min-w-0"}`}
>
<span className="material-symbols-outlined text-[14px] text-text-muted shrink-0">
{stat.icon}
</span>
<span
className={`text-[11px] uppercase font-semibold tracking-wide text-text-muted ${section.wideValues ? "whitespace-nowrap" : "truncate"}`}
>
{stat.label}
</span>
</div>
{stat.tooltip ? (
<Tooltip
content={
<div className="flex flex-col gap-0.5 text-left py-0.5 min-w-[120px]">
<div className="font-semibold text-white/95 text-xs">{stat.label}</div>
<div
className={`font-mono text-xs ${stat.color || "text-violet-400"} tracking-wide`}
>
{String(stat.tooltip).includes(":")
? stat.tooltip
: `${stat.label.toLowerCase().includes("cost") ? "cost" : "tokens"} : ${stat.tooltip}`}
</div>
</div>
}
className={section.wideValues ? "truncate min-w-0" : "shrink-0"}
delayMs={150}
>
{statValueEl}
</Tooltip>
) : (
statValueEl
)}
</div>
);
})}
</div>
</div>
))}
@@ -421,13 +489,22 @@ export function ApiKeyTable({ byApiKey }) {
<td className="px-4 py-2.5 text-right font-mono text-text-muted">
{fmtFull(row.requests)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-primary">
<td
className="px-4 py-2.5 text-right font-mono text-primary"
title={fmtFull(row.promptTokens)}
>
{fmt(row.promptTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-emerald-500">
<td
className="px-4 py-2.5 text-right font-mono text-emerald-500"
title={fmtFull(row.completionTokens)}
>
{fmt(row.completionTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono font-semibold">
<td
className="px-4 py-2.5 text-right font-mono font-semibold"
title={fmtFull(row.totalTokens)}
>
{fmt(row.totalTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-amber-500">
@@ -499,9 +576,26 @@ export function MostActiveDay7d({ activityMap }) {
<span className="text-xl font-bold capitalize" style={{ lineHeight: 1.2 }}>
{data.weekday}
</span>
<span className="text-xs mt-1" style={{ color: "var(--color-text-muted)" }}>
{t("datedTokenCount", { date: data.label, tokens: fmt(data.tokens) })}
</span>
<Tooltip
content={
<div className="flex flex-col gap-0.5 text-left py-0.5 min-w-[140px]">
<div className="font-semibold text-white/95 text-xs">{data.weekday}</div>
<div className="font-mono text-xs text-violet-400 tracking-wide">
tokens : {fmtFull(data.tokens)} tokens
</div>
</div>
}
className="w-fit max-w-full"
delayMs={150}
>
<span
className="text-xs mt-1 cursor-default"
style={{ color: "var(--color-text-muted)" }}
data-tooltip={fmtFull(data.tokens)}
>
{t("datedTokenCount", { date: data.label, tokens: fmt(data.tokens) })}
</span>
</Tooltip>
</>
) : (
<span className="text-xs" style={{ color: "var(--color-text-muted)" }}>
@@ -562,7 +656,7 @@ export function WeeklySquares7d({ activityMap }) {
{t("chartWeekly")}
</h3>
<div style={{ display: "flex", alignItems: "flex-end", gap: 6, justifyContent: "center" }}>
{days.map((d, i) => (
{days.map((d) => (
<div
key={d.key}
style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 4 }}
@@ -861,13 +955,22 @@ export function ProviderTable({ byProvider }) {
<td className="px-4 py-2.5 text-right font-mono text-text-muted">
{fmtFull(p.requests)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-primary">
<td
className="px-4 py-2.5 text-right font-mono text-primary"
title={fmtFull(p.promptTokens)}
>
{fmt(p.promptTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-emerald-500">
<td
className="px-4 py-2.5 text-right font-mono text-emerald-500"
title={fmtFull(p.completionTokens)}
>
{fmt(p.completionTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono font-semibold">
<td
className="px-4 py-2.5 text-right font-mono font-semibold"
title={fmtFull(p.totalTokens)}
>
{fmt(p.totalTokens)}
</td>
<td className="px-4 py-2.5 text-right font-mono text-amber-500">

View File

@@ -0,0 +1,296 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
StatCard,
CompactStatGrid,
ApiKeyTable,
ProviderTable,
MostActiveDay7d,
} from "@/shared/components/analytics/charts";
import { ModelTable } from "@/shared/components/analytics/ModelTable";
import RequestCountTable from "@/shared/components/analytics/RequestCountTable";
import { fmtFull } from "@/shared/utils/formatting";
vi.mock("next-intl", () => ({
useLocale: () => "en",
useTranslations: () => {
const t = (key: string, values?: Record<string, any>) => {
if (values) return `${key}:${JSON.stringify(values)}`;
return key;
};
t.has = () => false;
return t;
},
}));
describe("Analytics Token Hover Tooltips", () => {
let container: HTMLElement;
beforeEach(() => {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
document.body.innerHTML = "";
});
it("StatCard renders rich custom tooltip when provided, else falls back to value title", async () => {
vi.useFakeTimers();
const root = createRoot(container);
await act(async () => {
root.render(
<div>
<StatCard
icon="generating_tokens"
label="Total Tokens"
value="1.5B"
tooltip={fmtFull(1532481200)}
/>
<StatCard icon="payments" label="Est. Cost" value="$1180.27" />
</div>
);
});
const values = container.querySelectorAll(".text-2xl.font-bold");
expect(values).toHaveLength(2);
expect(values[0]?.getAttribute("data-tooltip")).toBe(fmtFull(1532481200));
expect(values[0]?.textContent).toBe("1.5B");
expect(values[1]?.getAttribute("title")).toBe("$1180.27");
expect(values[1]?.textContent).toBe("$1180.27");
// Trigger hover on the first StatCard value
const trigger = container.querySelector(".w-fit.max-w-full");
expect(trigger).toBeDefined();
await act(async () => {
trigger?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await vi.advanceTimersByTimeAsync(250);
});
// Check portal tooltip content in document.body
const tooltipPortal = document.body.querySelector('[role="tooltip"]');
expect(tooltipPortal).not.toBeNull();
expect(tooltipPortal?.textContent).toContain("Total Tokens");
expect(tooltipPortal?.textContent).toContain(`tokens : ${fmtFull(1532481200)} tokens`);
vi.useRealTimers();
});
it("CompactStatGrid renders rich custom tooltip when provided", async () => {
vi.useFakeTimers();
const root = createRoot(container);
await act(async () => {
root.render(
<CompactStatGrid
sections={[
{
title: "Performance",
items: [
{
icon: "speed",
label: "Avg Tokens/Req",
value: "131.5K",
tooltip: `tokens : ${fmtFull(131482)} tokens`,
},
{
icon: "bolt",
label: "Fast Requests",
value: "0",
},
],
},
]}
/>
);
});
const statElements = container.querySelectorAll(".text-sm.font-bold.text-right");
expect(statElements).toHaveLength(2);
expect(statElements[0]?.getAttribute("data-tooltip")).toBe(
`tokens : ${fmtFull(131482)} tokens`
);
expect(statElements[1]?.getAttribute("title")).toBe("0");
// Trigger hover on the first stat item
const trigger = statElements[0]?.closest(".relative.inline-flex");
expect(trigger).toBeDefined();
await act(async () => {
trigger?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await vi.advanceTimersByTimeAsync(250);
});
const tooltipPortal = document.body.querySelector('[role="tooltip"]');
expect(tooltipPortal).not.toBeNull();
expect(tooltipPortal?.textContent).toContain("Avg Tokens/Req");
expect(tooltipPortal?.textContent).toContain(`tokens : ${fmtFull(131482)} tokens`);
vi.useRealTimers();
});
it("ApiKeyTable renders full token counts as title hover tooltips", async () => {
const root = createRoot(container);
const mockData = [
{
apiKeyId: "key-12345678",
apiKeyName: "Default Key",
requests: 10,
promptTokens: 1532000000,
completionTokens: 6900000,
totalTokens: 1538900000,
cost: 12.34,
},
];
await act(async () => {
root.render(<ApiKeyTable byApiKey={mockData} />);
});
const rows = container.querySelectorAll("tbody tr");
expect(rows).toHaveLength(1);
const cells = rows[0]?.querySelectorAll("td");
// promptTokens
expect(cells?.[2]?.getAttribute("title")).toBe(fmtFull(1532000000));
expect(cells?.[2]?.textContent?.trim()).toBe("1.5B");
// completionTokens
expect(cells?.[3]?.getAttribute("title")).toBe(fmtFull(6900000));
expect(cells?.[3]?.textContent?.trim()).toBe("6.9M");
// totalTokens
expect(cells?.[4]?.getAttribute("title")).toBe(fmtFull(1538900000));
expect(cells?.[4]?.textContent?.trim()).toBe("1.5B");
});
it("ProviderTable renders full token counts as title hover tooltips", async () => {
const root = createRoot(container);
const mockData = [
{
provider: "anthropic",
requests: 15,
promptTokens: 2000000,
completionTokens: 500000,
totalTokens: 2500000,
cost: 5.5,
},
];
await act(async () => {
root.render(<ProviderTable byProvider={mockData} />);
});
const rows = container.querySelectorAll("tbody tr");
expect(rows).toHaveLength(1);
const cells = rows[0]?.querySelectorAll("td");
// promptTokens
expect(cells?.[2]?.getAttribute("title")).toBe(fmtFull(2000000));
expect(cells?.[2]?.textContent?.trim()).toBe("2.0M");
// completionTokens
expect(cells?.[3]?.getAttribute("title")).toBe(fmtFull(500000));
expect(cells?.[3]?.textContent?.trim()).toBe("500.0K");
// totalTokens
expect(cells?.[4]?.getAttribute("title")).toBe(fmtFull(2500000));
expect(cells?.[4]?.textContent?.trim()).toBe("2.5M");
});
it("ModelTable renders full token counts as title hover tooltips", async () => {
const root = createRoot(container);
const mockData = [
{
model: "claude-3-7-sonnet",
requests: 20,
promptTokens: 10000000,
completionTokens: 2000000,
totalTokens: 12000000,
cost: 10.0,
},
];
await act(async () => {
root.render(<ModelTable byModel={mockData} summary={{ totalTokens: 12000000 }} />);
});
const rows = container.querySelectorAll("tbody tr");
expect(rows).toHaveLength(1);
const cells = rows[0]?.querySelectorAll("td");
// promptTokens
expect(cells?.[2]?.getAttribute("title")).toBe(fmtFull(10000000));
expect(cells?.[2]?.textContent?.trim()).toBe("10.0M");
// completionTokens
expect(cells?.[3]?.getAttribute("title")).toBe(fmtFull(2000000));
expect(cells?.[3]?.textContent?.trim()).toBe("2.0M");
// totalTokens
expect(cells?.[4]?.getAttribute("title")).toBe(fmtFull(12000000));
expect(cells?.[4]?.textContent?.trim()).toBe("12.0M");
});
it("RequestCountTable renders full totalTokens count as title hover tooltip", async () => {
const root = createRoot(container);
const mockData = [
{
date: "2026-09-03",
provider: "openai",
requests: 5,
promptTokens: 1000,
completionTokens: 500,
totalTokens: 1500000,
},
];
await act(async () => {
root.render(
<RequestCountTable
rows={mockData}
sortBy="date"
sortOrder="desc"
onToggleSort={() => {}}
dateLabel="Date"
providerLabel="Provider"
requestsLabel="Requests"
totalLabel="Total"
/>
);
});
const rows = container.querySelectorAll("tbody tr");
expect(rows).toHaveLength(1);
const cells = rows[0]?.querySelectorAll("td");
// totalTokens cell (index 3)
expect(cells?.[3]?.getAttribute("title")).toBe(fmtFull(1500000));
expect(cells?.[3]?.textContent?.trim()).toBe("1.5M");
});
it("MostActiveDay7d renders rich custom tooltip on hover", async () => {
vi.useFakeTimers();
const root = createRoot(container);
// Use today's date formatted as YYYY-MM-DD
const today = new Date();
const todayKey = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
const mockActivityMap: Record<string, number> = {
[todayKey]: 309080259,
};
await act(async () => {
root.render(<MostActiveDay7d activityMap={mockActivityMap} />);
});
const trigger = container.querySelector(".relative.inline-flex.w-fit");
expect(trigger).toBeDefined();
await act(async () => {
trigger?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await vi.advanceTimersByTimeAsync(250);
});
const tooltipPortal = document.body.querySelector('[role="tooltip"]');
expect(tooltipPortal).not.toBeNull();
expect(tooltipPortal?.textContent).toContain(`tokens : ${fmtFull(309080259)} tokens`);
vi.useRealTimers();
});
});