litellm/ui/litellm-dashboard/tests/setupTests.ts
ryan-crabbe-berri 83ae623733
refactor(ui): codemod every toast call site onto lib/toast and delete the antd-era facades (#37253)
MessageManager and NotificationManager were thin facades over lib/toast since #37207. This
rewrites their ~750 call sites (226 files) to import { toast } from @/lib/toast directly:
success/info/warning/error keep their names, fromBackend becomes fromError, destroy/clear
become dismiss. The one config-object caller (CreateMCPServer's admin-review branch) becomes
an explicit toast.success(message, { description }). Behaviour is unchanged: no production
caller passed a duration, so every toast keeps the same kind, title and default duration.

Tests: the global vitest mock now targets @/lib/toast (toast.test.ts opts back out with
vi.unmock), so the per-file vi.mock boilerplate for the facades is deleted and assertions read
toast.success / toast.fromError. The two facade files, their test and their filename-case
suppressions are removed, along with the commented-out facade calls left in networking.tsx
2026-08-18 18:24:49 +00:00

260 lines
8.4 KiB
TypeScript

import "@testing-library/jest-dom";
import { cleanup } from "@testing-library/react";
import React from "react";
import { afterEach, vi } from "vitest";
const ensureTestLocalStorage = () => {
if (typeof window === "undefined" || typeof window.Storage === "undefined") {
return;
}
if (typeof window.localStorage?.getItem === "function" && typeof window.localStorage?.clear === "function") {
return;
}
const storageStores = new WeakMap<Storage, Map<string, string>>();
const storagePrototype = window.Storage.prototype;
const getStore = (storage: Storage) => {
let store = storageStores.get(storage);
if (store === undefined) {
store = new Map<string, string>();
storageStores.set(storage, store);
}
return store;
};
Object.defineProperties(storagePrototype, {
getItem: {
configurable: true,
writable: true,
value(this: Storage, key: string) {
const store = getStore(this);
const normalizedKey = String(key);
return store.has(normalizedKey) ? store.get(normalizedKey)! : null;
},
},
setItem: {
configurable: true,
writable: true,
value(this: Storage, key: string, value: string) {
const store = getStore(this);
store.set(String(key), String(value));
},
},
removeItem: {
configurable: true,
writable: true,
value(this: Storage, key: string) {
const store = getStore(this);
store.delete(String(key));
},
},
clear: {
configurable: true,
writable: true,
value(this: Storage) {
const store = getStore(this);
store.clear();
},
},
key: {
configurable: true,
writable: true,
value(this: Storage, index: number) {
const store = getStore(this);
return Array.from(store.keys())[index] ?? null;
},
},
});
const localStorage = Object.create(storagePrototype);
storageStores.set(localStorage, new Map<string, string>());
Object.defineProperty(localStorage, "length", {
configurable: true,
get() {
return getStore(localStorage).size;
},
});
Object.defineProperty(window, "localStorage", {
configurable: true,
value: localStorage,
});
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: localStorage,
});
};
ensureTestLocalStorage();
// Global mock so every test can assert on toast calls; toast.test.ts opts back in with vi.unmock
vi.mock("@/lib/toast", () => ({
toast: {
success: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
fromError: vi.fn(),
dismiss: vi.fn(),
},
}));
vi.mock("@tremor/react", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tremor/react")>();
return {
...actual,
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) =>
// Render as a native button to avoid Tremor-specific behaviors in tests
React.createElement("button", { ...props, ref }, children),
),
Tooltip: ({ children, ..._props }: { children?: React.ReactNode; [key: string]: unknown }) => {
// Return children directly without tooltip functionality to prevent flaky tests
// This avoids issues with hover states, positioning, and DOM queries in tests
return React.createElement(React.Fragment, null, children);
},
// Render as a plain checkbox so toggle interactions are testable without Tremor internals
Switch: ({
checked,
onChange,
className,
}: {
checked?: boolean;
onChange?: (v: boolean) => void;
className?: string;
}) =>
React.createElement("input", {
type: "checkbox",
role: "switch",
checked,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => onChange?.(e.target.checked),
className,
}),
};
});
// Global mock for useAuthorized hook to avoid repeating the same mock in every test file
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({
token: "123",
accessToken: "123",
userId: "user-1",
userEmail: "user@example.com",
userRole: "Admin",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
}),
}));
const pendingRefWarnings: string[] = [];
const consumePendingRefWarnings = (): string[] => pendingRefWarnings.splice(0, pendingRefWarnings.length);
(globalThis as { __consumePendingRefWarnings?: () => string[] }).__consumePendingRefWarnings =
consumePendingRefWarnings;
const originalConsoleError = console.error.bind(console);
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
originalConsoleError(...args);
if (typeof args[0] === "string" && args[0].includes("Function components cannot be given refs")) {
pendingRefWarnings.push(args.map(String).join(" "));
}
});
afterEach(() => {
cleanup();
const refWarnings = consumePendingRefWarnings();
if (refWarnings.length > 0) {
throw new Error(
"A ref was passed to a plain function component and silently dropped under React 18, which breaks " +
"ref-based composition (Base UI render triggers, tooltips, focus). Wrap the component in React.forwardRef. " +
"This tripwire lives in tests/setupTests.ts and can be removed after the React 19 upgrade.\n\n" +
refWarnings.join("\n\n"),
);
}
});
// Make toLocaleString deterministic in tests; individual tests can override
// This returns ISO-like strings to keep assertions stable.
vi.spyOn(Date.prototype, "toLocaleString").mockImplementation(function (this: Date, ..._args: unknown[]) {
const d = this;
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
});
if (typeof window !== "undefined") {
// Fixed matchMedia not found error in tests: https://github.com/vitest-dev/vitest/issues/821
Object.defineProperty(window, "matchMedia", {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}),
});
// Silence jsdom "getComputedStyle with pseudo-elements" not implemented warnings
// by ignoring the second argument and delegating to the native implementation.
const realGetComputedStyle = window.getComputedStyle.bind(window);
window.getComputedStyle = ((elt: Element) => realGetComputedStyle(elt)) as any;
// Avoid "navigation to another Document" warnings when clicking <a> with blob: URLs
// used by download flows in tests.
Object.defineProperty(HTMLAnchorElement.prototype, "click", {
configurable: true,
writable: true,
value: vi.fn(),
});
if (!document.getAnimations) {
document.getAnimations = () => [];
}
// Stub URL.revokeObjectURL so vi.spyOn can intercept it in tests
if (!URL.revokeObjectURL) {
URL.revokeObjectURL = () => {};
}
// Mock ResizeObserver for components that use it (recharts, Tremor UI components).
// JSDOM has no layout, so for observers inside a shadcn ChartContainer ([data-slot="chart"])
// the mock immediately reports a fixed 800x400 box; recharts renders nothing until it
// observes a size. Scoped to chart subtrees only: firing for every observer re-enters
// React mid-effect for tremor/headlessui consumers whose tests assume the old no-op
// (chart text would duplicate getByText targets, popover clicks go stale). Widen or
// drop the scoping once tremor is gone.
const MOCK_RESIZE_BOX = { inlineSize: 800, blockSize: 400 };
const MOCK_RESIZE_RECT: DOMRectReadOnly = {
width: 800,
height: 400,
top: 0,
left: 0,
bottom: 400,
right: 800,
x: 0,
y: 0,
toJSON: () => ({}),
};
global.ResizeObserver = class ResizeObserver {
private readonly callback: ResizeObserverCallback;
constructor(callback: ResizeObserverCallback) {
this.callback = callback;
}
observe(target: Element) {
if (!target.closest('[data-slot="chart"]')) return;
const entry: ResizeObserverEntry = {
target,
contentRect: MOCK_RESIZE_RECT,
borderBoxSize: [MOCK_RESIZE_BOX],
contentBoxSize: [MOCK_RESIZE_BOX],
devicePixelContentBoxSize: [MOCK_RESIZE_BOX],
};
this.callback([entry], this);
}
unobserve() {}
disconnect() {}
};
}