Merge pull request #21695 from BerriAI/litellm_ui_unit_tests

[Test] UI - Add vitest unit tests for Teams, Models, and Usage
This commit is contained in:
yuneng-jiang 2026-02-20 11:30:18 -08:00 committed by GitHub
commit d5fa49fbb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 743 additions and 1 deletions

View file

@ -0,0 +1,217 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, beforeEach, expect, it, vi } from "vitest";
import ModelRetrySettingsTab from "./ModelRetrySettingsTab";
// TabPanel requires a parent Tabs context in Tremor. We stub it to render children
// directly so the component can be tested in isolation.
vi.mock("@tremor/react", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tremor/react")>();
return {
...actual,
TabPanel: ({ children }: { children: React.ReactNode }) => React.createElement("div", null, children),
// Keep Select/SelectItem as the real implementation so scope-switching is testable
};
});
type GlobalRetryPolicy = { [key: string]: number };
type ModelGroupRetryPolicy = { [key: string]: { [key: string]: number } | undefined };
const DEFAULT_RETRY = 0;
const buildProps = (overrides: Record<string, unknown> = {}) => ({
selectedModelGroup: "global" as string | null,
setSelectedModelGroup: vi.fn(),
availableModelGroups: ["gpt-4", "claude-3-opus"],
globalRetryPolicy: null as GlobalRetryPolicy | null,
setGlobalRetryPolicy: vi.fn(),
defaultRetry: DEFAULT_RETRY,
modelGroupRetryPolicy: null as ModelGroupRetryPolicy | null,
setModelGroupRetryPolicy: vi.fn(),
handleSaveRetrySettings: vi.fn(),
...overrides,
});
describe("ModelRetrySettingsTab", () => {
it("should render the 'Global Retry Policy' heading when selectedModelGroup is 'global'", () => {
render(<ModelRetrySettingsTab {...buildProps()} />);
expect(screen.getByText("Global Retry Policy")).toBeInTheDocument();
});
it("should render a model-specific heading when a model group is selected", () => {
render(<ModelRetrySettingsTab {...buildProps({ selectedModelGroup: "gpt-4" })} />);
expect(screen.getByText("Retry Policy for gpt-4")).toBeInTheDocument();
});
it("should render a row for every error type in the retry policy map", () => {
render(<ModelRetrySettingsTab {...buildProps()} />);
expect(screen.getByText(/BadRequestError \(400\)/)).toBeInTheDocument();
expect(screen.getByText(/AuthenticationError/)).toBeInTheDocument();
expect(screen.getByText(/TimeoutError \(408\)/)).toBeInTheDocument();
expect(screen.getByText(/RateLimitError \(429\)/)).toBeInTheDocument();
expect(screen.getByText(/ContentPolicyViolationError \(400\)/)).toBeInTheDocument();
expect(screen.getByText(/InternalServerError \(500\)/)).toBeInTheDocument();
});
it("should use defaultRetry when globalRetryPolicy is null (global scope)", () => {
render(<ModelRetrySettingsTab {...buildProps({ defaultRetry: 3 })} />);
// All 6 spinbutton inputs should show the defaultRetry value
const inputs = screen.getAllByRole("spinbutton");
inputs.forEach((input) => {
expect(input).toHaveValue("3");
});
});
it("should show globalRetryPolicy values when they are set (global scope)", () => {
const globalRetryPolicy: GlobalRetryPolicy = {
RateLimitErrorRetries: 5,
};
render(<ModelRetrySettingsTab {...buildProps({ globalRetryPolicy, defaultRetry: 0 })} />);
// The RateLimitError row is the 4th entry in the map
const inputs = screen.getAllByRole("spinbutton");
const rateLimitInput = inputs[3]; // 0-indexed: Bad(0), Auth(1), Timeout(2), Rate(3)
expect(rateLimitInput).toHaveValue("5");
// Unset entries fall back to defaultRetry (0)
expect(inputs[0]).toHaveValue("0");
});
it("should fall back to globalRetryPolicy when no model-specific value is set (model scope)", () => {
const globalRetryPolicy: GlobalRetryPolicy = {
TimeoutErrorRetries: 7,
};
render(
<ModelRetrySettingsTab
{...buildProps({
selectedModelGroup: "gpt-4",
globalRetryPolicy,
modelGroupRetryPolicy: null,
defaultRetry: 1,
})}
/>,
);
// The TimeoutError row is 3rd (index 2)
const inputs = screen.getAllByRole("spinbutton");
expect(inputs[2]).toHaveValue("7");
// Rows without a global value fall back to defaultRetry
expect(inputs[0]).toHaveValue("1");
});
it("should prefer model-specific retry count over the global value (model scope)", () => {
const globalRetryPolicy: GlobalRetryPolicy = {
RateLimitErrorRetries: 3,
};
const modelGroupRetryPolicy: ModelGroupRetryPolicy = {
"gpt-4": { RateLimitErrorRetries: 9 },
};
render(
<ModelRetrySettingsTab
{...buildProps({
selectedModelGroup: "gpt-4",
globalRetryPolicy,
modelGroupRetryPolicy,
defaultRetry: 0,
})}
/>,
);
// The model-specific value (9) should win over global (3)
const inputs = screen.getAllByRole("spinbutton");
expect(inputs[3]).toHaveValue("9");
});
it("should show the global reference value text for each row in model-specific scope", () => {
const globalRetryPolicy: GlobalRetryPolicy = { BadRequestErrorRetries: 2 };
render(
<ModelRetrySettingsTab
{...buildProps({
selectedModelGroup: "gpt-4",
globalRetryPolicy,
defaultRetry: 0,
})}
/>,
);
// "(Global: X)" annotations are shown next to each row label in model scope
expect(screen.getByText("(Global: 2)")).toBeInTheDocument();
});
it("should not show global reference annotations in global scope", () => {
render(<ModelRetrySettingsTab {...buildProps({ selectedModelGroup: "global" })} />);
expect(screen.queryByText(/Global:/)).not.toBeInTheDocument();
});
it("should call handleSaveRetrySettings when the Save button is clicked", async () => {
const user = userEvent.setup();
const handleSaveRetrySettings = vi.fn();
render(<ModelRetrySettingsTab {...buildProps({ handleSaveRetrySettings })} />);
await user.click(screen.getByRole("button", { name: /save/i }));
expect(handleSaveRetrySettings).toHaveBeenCalledTimes(1);
});
it("should call setGlobalRetryPolicy with an updater function when an input changes (global scope)", async () => {
const user = userEvent.setup();
const setGlobalRetryPolicy = vi.fn();
render(
<ModelRetrySettingsTab
{...buildProps({
selectedModelGroup: "global",
globalRetryPolicy: { BadRequestErrorRetries: 0 },
setGlobalRetryPolicy,
defaultRetry: 0,
})}
/>,
);
const inputs = screen.getAllByRole("spinbutton");
await user.clear(inputs[0]);
await user.type(inputs[0], "4");
// setGlobalRetryPolicy is called with a function updater
expect(setGlobalRetryPolicy).toHaveBeenCalled();
const updater = setGlobalRetryPolicy.mock.calls.at(-1)![0];
expect(typeof updater).toBe("function");
// Calling the updater returns the merged policy
const result = updater({ BadRequestErrorRetries: 0 });
expect(result).toMatchObject({ BadRequestErrorRetries: 4 });
});
it("should call setModelGroupRetryPolicy with an updater function when an input changes (model scope)", async () => {
const user = userEvent.setup();
const setModelGroupRetryPolicy = vi.fn();
render(
<ModelRetrySettingsTab
{...buildProps({
selectedModelGroup: "gpt-4",
modelGroupRetryPolicy: { "gpt-4": { BadRequestErrorRetries: 0 } },
setModelGroupRetryPolicy,
defaultRetry: 0,
})}
/>,
);
const inputs = screen.getAllByRole("spinbutton");
await user.clear(inputs[0]);
await user.type(inputs[0], "2");
expect(setModelGroupRetryPolicy).toHaveBeenCalled();
const updater = setModelGroupRetryPolicy.mock.calls.at(-1)![0];
expect(typeof updater).toBe("function");
// Calling the updater returns the merged model-group policy
const result = updater({ "gpt-4": { BadRequestErrorRetries: 0 } });
expect(result["gpt-4"]).toMatchObject({ BadRequestErrorRetries: 2 });
});
});

View file

@ -0,0 +1,151 @@
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, expect, it, vi } from "vitest";
import { Organization } from "@/components/networking";
import TeamsFilters from "./TeamsFilters";
type FilterState = {
team_id: string;
team_alias: string;
organization_id: string;
sort_by: string;
sort_order: "asc" | "desc";
};
const emptyFilters: FilterState = {
team_alias: "",
team_id: "",
organization_id: "",
sort_by: "",
sort_order: "asc",
};
const mockOrganizations: Organization[] = [
{ organization_id: "org-1", organization_alias: "Acme Corp" } as Organization,
{ organization_id: "org-2", organization_alias: "Globex" } as Organization,
];
const renderFilters = (overrides: Partial<Parameters<typeof TeamsFilters>[0]> = {}) => {
const defaults = {
filters: emptyFilters,
organizations: mockOrganizations,
showFilters: false,
onToggleFilters: vi.fn(),
onChange: vi.fn(),
onReset: vi.fn(),
};
return render(<TeamsFilters {...defaults} {...overrides} />);
};
describe("TeamsFilters", () => {
it("should render the team name search input, Filters button, and Reset Filters button", () => {
renderFilters();
expect(screen.getByPlaceholderText("Search by Team Name...")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /^filters$/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /reset filters/i })).toBeInTheDocument();
});
it("should reflect the current team_alias filter value in the search input", () => {
renderFilters({ filters: { ...emptyFilters, team_alias: "Platform" } });
expect(screen.getByPlaceholderText("Search by Team Name...")).toHaveValue("Platform");
});
it("should call onChange with 'team_alias' key when the search input changes", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderFilters({ onChange });
await user.type(screen.getByPlaceholderText("Search by Team Name..."), "Dev");
expect(onChange).toHaveBeenCalledWith("team_alias", expect.stringContaining("D"));
});
it("should call onToggleFilters with the inverted boolean when the Filters button is clicked", async () => {
const user = userEvent.setup();
const onToggleFilters = vi.fn();
renderFilters({ showFilters: false, onToggleFilters });
await user.click(screen.getByRole("button", { name: /^filters$/i }));
expect(onToggleFilters).toHaveBeenCalledWith(true);
});
it("should call onToggleFilters(false) when filters are currently expanded", async () => {
const user = userEvent.setup();
const onToggleFilters = vi.fn();
renderFilters({ showFilters: true, onToggleFilters });
await user.click(screen.getByRole("button", { name: /^filters$/i }));
expect(onToggleFilters).toHaveBeenCalledWith(false);
});
it("should call onReset when the Reset Filters button is clicked", async () => {
const user = userEvent.setup();
const onReset = vi.fn();
renderFilters({ onReset });
await user.click(screen.getByRole("button", { name: /reset filters/i }));
expect(onReset).toHaveBeenCalledTimes(1);
});
it("should not show the Team ID input when showFilters is false", () => {
renderFilters({ showFilters: false });
expect(screen.queryByPlaceholderText("Enter Team ID")).not.toBeInTheDocument();
});
it("should show the Team ID input when showFilters is true", () => {
renderFilters({ showFilters: true });
expect(screen.getByPlaceholderText("Enter Team ID")).toBeInTheDocument();
});
it("should call onChange with 'team_id' key when the Team ID input changes", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderFilters({ showFilters: true, onChange });
await user.type(screen.getByPlaceholderText("Enter Team ID"), "abc");
expect(onChange).toHaveBeenCalledWith("team_id", expect.stringContaining("a"));
});
it("should reflect the current team_id filter value in the Team ID input", () => {
renderFilters({ showFilters: true, filters: { ...emptyFilters, team_id: "team-xyz" } });
expect(screen.getByPlaceholderText("Enter Team ID")).toHaveValue("team-xyz");
});
it("should show the active filter indicator on the Filters button when team_alias is set", () => {
renderFilters({ filters: { ...emptyFilters, team_alias: "Platform" } });
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument();
});
it("should show the active filter indicator on the Filters button when team_id is set", () => {
renderFilters({ filters: { ...emptyFilters, team_id: "team-123" } });
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument();
});
it("should show the active filter indicator on the Filters button when organization_id is set", () => {
renderFilters({ filters: { ...emptyFilters, organization_id: "org-1" } });
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument();
});
it("should not show the active filter indicator when all filters are empty", () => {
renderFilters({ filters: emptyFilters });
const filtersButton = screen.getByRole("button", { name: /^filters$/i });
expect(within(filtersButton).queryByTestId("active-filter-indicator")).not.toBeInTheDocument();
});
});

View file

@ -70,7 +70,7 @@ const TeamsFilters = ({
</svg>
Filters
{(filters.team_id || filters.team_alias || filters.organization_id) && (
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
<span data-testid="active-filter-indicator" className="w-2 h-2 rounded-full bg-blue-500"></span>
)}
</button>

View file

@ -0,0 +1,138 @@
import { act, render, screen } from "@testing-library/react";
import React from "react";
import { describe, expect, it, vi } from "vitest";
import { Team } from "@/components/key_team_helpers/key_list";
import ModelsCell from "./ModelsCell";
// The Icon component from @tremor/react does not forward onClick to the rendered element
// by default in the test environment, so we stub it with a clickable button so accordion
// interaction can be tested end-to-end.
vi.mock("@tremor/react", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tremor/react")>();
return {
...actual,
Icon: ({ onClick, "aria-label": ariaLabel }: { onClick?: () => void; "aria-label"?: string }) =>
React.createElement("button", { onClick, "aria-label": ariaLabel ?? "accordion-toggle", type: "button" }),
};
});
const makeTeam = (models: string[], overrides: Partial<Team> = {}): Team => ({
team_id: "team-1",
team_alias: "Engineering",
models,
max_budget: null,
budget_duration: null,
tpm_limit: null,
rpm_limit: null,
organization_id: "org-1",
created_at: "2024-01-01T00:00:00Z",
keys: [],
members_with_roles: [],
spend: 0,
...overrides,
});
// Wrap in a table so the <td> from TableCell renders without HTML warnings.
const renderModelsCell = (team: Team) =>
render(
<table>
<tbody>
<tr>
<ModelsCell team={team} />
</tr>
</tbody>
</table>,
);
describe("ModelsCell", () => {
it("should show 'All Proxy Models' badge when the models array is empty", () => {
renderModelsCell(makeTeam([]));
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
});
it("should show an 'All Proxy Models' badge when the model value is 'all-proxy-models'", () => {
renderModelsCell(makeTeam(["all-proxy-models"]));
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
});
it("should display individual model badges for up to 3 models without an accordion", () => {
renderModelsCell(makeTeam(["gpt-4", "gpt-3.5-turbo", "claude-3"]));
expect(screen.getByText("gpt-4")).toBeInTheDocument();
expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
expect(screen.getByText("claude-3")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /accordion/i })).not.toBeInTheDocument();
});
it("should truncate model names longer than 30 characters with an ellipsis", () => {
const longName = "a-very-long-model-name-exceeding-thirty-chars";
renderModelsCell(makeTeam([longName]));
const badge = screen.getByText((text) => text.endsWith("..."));
expect(badge).toBeInTheDocument();
expect(badge.textContent!.length).toBeLessThanOrEqual(33); // 30 chars + "..."
});
it("should show the first 3 models and a '+N more models' badge when there are more than 3 models", () => {
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"]));
expect(screen.getByText("m1")).toBeInTheDocument();
expect(screen.getByText("m2")).toBeInTheDocument();
expect(screen.getByText("m3")).toBeInTheDocument();
expect(screen.getByText("+2 more models")).toBeInTheDocument();
expect(screen.queryByText("m4")).not.toBeInTheDocument();
expect(screen.queryByText("m5")).not.toBeInTheDocument();
});
it("should use singular 'more model' when there is exactly 1 overflow model", () => {
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4"]));
expect(screen.getByText("+1 more model")).toBeInTheDocument();
});
it("should show the accordion toggle button when there are more than 3 models", () => {
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4"]));
expect(screen.getByRole("button", { name: /accordion/i })).toBeInTheDocument();
});
it("should expand to show all models when the accordion toggle is clicked", () => {
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"]));
act(() => {
screen.getByRole("button", { name: /accordion/i }).click();
});
expect(screen.getByText("m4")).toBeInTheDocument();
expect(screen.getByText("m5")).toBeInTheDocument();
expect(screen.queryByText("+2 more models")).not.toBeInTheDocument();
});
it("should collapse back to show the overflow badge after a second click on the toggle", () => {
renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"]));
const toggle = screen.getByRole("button", { name: /accordion/i });
act(() => {
toggle.click();
});
act(() => {
toggle.click();
});
expect(screen.queryByText("m4")).not.toBeInTheDocument();
expect(screen.getByText("+2 more models")).toBeInTheDocument();
});
it("should render 'all-proxy-models' entries in the overflow section as 'All Proxy Models' badges", () => {
renderModelsCell(makeTeam(["m1", "m2", "m3", "all-proxy-models"]));
act(() => {
screen.getByRole("button", { name: /accordion/i }).click();
});
// There should now be an "All Proxy Models" badge in the expanded section
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
});
});

View file

@ -0,0 +1,171 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, expect, it, vi } from "vitest";
import { Team } from "@/components/key_team_helpers/key_list";
import DeleteTeamModal from "./DeleteTeamModal";
const makeTeam = (overrides: Partial<Team> = {}): Team => ({
team_id: "team-1",
team_alias: "Engineering",
models: [],
max_budget: null,
budget_duration: null,
tpm_limit: null,
rpm_limit: null,
organization_id: "org-1",
created_at: "2024-01-01T00:00:00Z",
keys: [],
members_with_roles: [],
spend: 0,
...overrides,
});
const renderModal = (props: Partial<Parameters<typeof DeleteTeamModal>[0]> = {}) => {
const defaults = {
teams: [makeTeam()],
teamToDelete: "team-1",
onCancel: vi.fn(),
onConfirm: vi.fn(),
};
return render(<DeleteTeamModal {...defaults} {...props} />);
};
describe("DeleteTeamModal", () => {
it("should render the title, team name label, and confirmation input", () => {
renderModal();
expect(screen.getByText("Delete Team")).toBeInTheDocument();
expect(screen.getByText("Engineering")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Enter team name exactly")).toBeInTheDocument();
});
it("should render Cancel and Force Delete buttons", () => {
renderModal();
expect(screen.getByRole("button", { name: /^cancel$/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /force delete/i })).toBeInTheDocument();
});
it("should not show the warning banner when the team has no keys", () => {
renderModal({ teams: [makeTeam({ keys: [] })] });
expect(screen.queryByText(/Warning/i)).not.toBeInTheDocument();
});
it("should show a warning with singular 'key' when the team has exactly 1 key", () => {
const team = makeTeam({ keys: [{ token: "tok-1" } as any] });
renderModal({ teams: [team] });
expect(screen.getByText(/This team has 1 associated key\./)).toBeInTheDocument();
});
it("should show a warning with plural 'keys' when the team has multiple keys", () => {
const team = makeTeam({
keys: [{ token: "tok-1" } as any, { token: "tok-2" } as any, { token: "tok-3" } as any],
});
renderModal({ teams: [team] });
expect(screen.getByText(/This team has 3 associated keys\./)).toBeInTheDocument();
});
it("should note that associated keys will also be deleted in the warning", () => {
const team = makeTeam({ keys: [{ token: "tok-1" } as any] });
renderModal({ teams: [team] });
expect(screen.getByText(/Deleting the team will also delete all associated keys/)).toBeInTheDocument();
});
it("should disable Force Delete when the input is empty", () => {
renderModal();
expect(screen.getByRole("button", { name: /force delete/i })).toBeDisabled();
});
it("should keep Force Delete disabled when the input does not exactly match the team name", async () => {
const user = userEvent.setup();
renderModal();
await user.type(screen.getByPlaceholderText("Enter team name exactly"), "engineer");
expect(screen.getByRole("button", { name: /force delete/i })).toBeDisabled();
});
it("should enable Force Delete only after typing the exact team name (case-sensitive)", async () => {
const user = userEvent.setup();
renderModal();
const input = screen.getByPlaceholderText("Enter team name exactly");
await user.type(input, "Engineering");
expect(screen.getByRole("button", { name: /force delete/i })).toBeEnabled();
});
it("should call onConfirm when Force Delete is clicked with a valid input", async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
renderModal({ onConfirm });
await user.type(screen.getByPlaceholderText("Enter team name exactly"), "Engineering");
await user.click(screen.getByRole("button", { name: /force delete/i }));
expect(onConfirm).toHaveBeenCalledTimes(1);
});
it("should not call onConfirm when Force Delete is clicked with an invalid input", async () => {
const user = userEvent.setup();
const onConfirm = vi.fn();
renderModal({ onConfirm });
// Button is disabled so click has no effect
await user.click(screen.getByRole("button", { name: /force delete/i }));
expect(onConfirm).not.toHaveBeenCalled();
});
it("should call onCancel when the Cancel button is clicked", async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
renderModal({ onCancel });
await user.click(screen.getByRole("button", { name: /^cancel$/i }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
it("should call onCancel when the Close button is clicked", async () => {
const user = userEvent.setup();
const onCancel = vi.fn();
renderModal({ onCancel });
await user.click(screen.getByRole("button", { name: /^close$/i }));
expect(onCancel).toHaveBeenCalledTimes(1);
});
it("should reset the confirmation input when Cancel is clicked", async () => {
const user = userEvent.setup();
renderModal();
const input = screen.getByPlaceholderText("Enter team name exactly");
await user.type(input, "Engineering");
expect(input).toHaveValue("Engineering");
await user.click(screen.getByRole("button", { name: /^cancel$/i }));
expect(input).toHaveValue("");
});
it("should reset the confirmation input when the Close button is clicked", async () => {
const user = userEvent.setup();
renderModal();
const input = screen.getByPlaceholderText("Enter team name exactly");
await user.type(input, "Engineering");
await user.click(screen.getByRole("button", { name: /^close$/i }));
expect(input).toHaveValue("");
});
});

View file

@ -24,6 +24,7 @@ const DeleteTeamModal = ({ teams, teamToDelete, onCancel, onConfirm }: DeleteTea
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Delete Team</h3>
<button
aria-label="Close"
onClick={() => {
onCancel();
setDeleteConfirmInput("");

View file

@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { valueFormatter, valueFormatterSpend } from "./value_formatters";
describe("valueFormatter", () => {
it("should format numbers >= 1,000,000 as millions with 2 decimal places", () => {
expect(valueFormatter(1_000_000)).toBe("1.00M");
expect(valueFormatter(1_500_000)).toBe("1.50M");
expect(valueFormatter(2_750_000)).toBe("2.75M");
expect(valueFormatter(10_000_000)).toBe("10.00M");
});
it("should format numbers in the thousands range as 'k' suffix", () => {
expect(valueFormatter(1_000)).toBe("1k");
expect(valueFormatter(5_500)).toBe("5.5k");
expect(valueFormatter(999_999)).toBe("999.999k");
});
it("should return the plain string for numbers below 1,000", () => {
expect(valueFormatter(0)).toBe("0");
expect(valueFormatter(1)).toBe("1");
expect(valueFormatter(999)).toBe("999");
expect(valueFormatter(42)).toBe("42");
});
it("should treat exactly 1,000,000 as the millions boundary", () => {
expect(valueFormatter(1_000_000)).toBe("1.00M");
});
it("should treat exactly 1,000 as the thousands boundary", () => {
expect(valueFormatter(1_000)).toBe("1k");
});
});
describe("valueFormatterSpend", () => {
it("should return '$0' when the value is exactly zero", () => {
expect(valueFormatterSpend(0)).toBe("$0");
});
it("should format numbers >= 1,000,000 as dollar millions", () => {
expect(valueFormatterSpend(1_000_000)).toBe("$1M");
expect(valueFormatterSpend(2_500_000)).toBe("$2.5M");
expect(valueFormatterSpend(10_000_000)).toBe("$10M");
});
it("should format numbers >= 1,000 as dollar thousands", () => {
expect(valueFormatterSpend(1_000)).toBe("$1k");
expect(valueFormatterSpend(5_500)).toBe("$5.5k");
expect(valueFormatterSpend(999_999)).toBe("$999.999k");
});
it("should format numbers below 1,000 as plain dollar amounts", () => {
expect(valueFormatterSpend(1)).toBe("$1");
expect(valueFormatterSpend(99.99)).toBe("$99.99");
expect(valueFormatterSpend(999)).toBe("$999");
});
it("should treat exactly 1,000,000 as the millions boundary", () => {
expect(valueFormatterSpend(1_000_000)).toBe("$1M");
});
it("should treat exactly 1,000 as the thousands boundary", () => {
expect(valueFormatterSpend(1_000)).toBe("$1k");
});
});