.agents/skills/frontend-testing/references/mocking.md
All mocking in Langflow uses Jest APIs. Never use Vitest (vi.*) APIs.
| Vitest (DO NOT USE) | Jest (USE THIS) |
|---|---|
vi.fn() | jest.fn() |
vi.mock() | jest.mock() |
vi.spyOn() | jest.spyOn() |
vi.mocked() | jest.mocked() |
vi.clearAllMocks() | jest.clearAllMocks() |
vi.useFakeTimers() | jest.useFakeTimers() |
vi.useRealTimers() | jest.useRealTimers() |
vi.advanceTimersByTime() | jest.advanceTimersByTime() |
These modules are already mocked in jest.setup.js. Do NOT re-mock them unless you need different behavior:
@radix-ui/react-form (all exports render children)react-markdown (renders null)lucide-react/dynamicIconImports (empty object)@/components/common/genericIconComponent (renders null)@/icons/BotMessageSquare (renders null)@/stores/darkStore (returns default state)localStorage and sessionStorage (jest.fn() stubs)To use the real implementation instead:
jest.unmock("@/stores/darkStore");
Langflow uses Axios via a configured API instance. Mock the API module:
import api from "@/controllers/API/api";
jest.mock("@/controllers/API/api", () => ({
__esModule: true,
default: {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
patch: jest.fn(),
delete: jest.fn(),
},
}));
describe("MyComponent", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("should fetch data on mount", async () => {
jest.mocked(api.get).mockResolvedValueOnce({
data: { items: [{ id: "1", name: "Test" }] },
});
render(<MyComponent />);
await waitFor(() => {
expect(screen.getByText("Test")).toBeInTheDocument();
});
expect(api.get).toHaveBeenCalledWith("/api/v1/items");
});
it("should handle API errors", async () => {
jest.mocked(api.get).mockRejectedValueOnce(new Error("Network error"));
render(<MyComponent />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
For components using React Query hooks from @/controllers/API/queries/:
jest.mock("@/controllers/API/queries/flows", () => ({
useGetFlowsQuery: jest.fn().mockReturnValue({
data: [{ id: "flow-1", name: "My Flow" }],
isLoading: false,
error: null,
refetch: jest.fn(),
}),
}));
jest.mock("@/controllers/API", () => ({
getFlows: jest.fn().mockResolvedValue([]),
saveFlow: jest.fn().mockResolvedValue({ id: "new-flow" }),
deleteFlow: jest.fn().mockResolvedValue(undefined),
}));
Langflow does NOT have a global Zustand auto-mock. You have two options:
setState() (Preferred)import useAlertStore from "@/stores/alertStore";
describe("Alert-dependent component", () => {
beforeEach(() => {
// Reset store to known state before each test
useAlertStore.setState({
errorData: { title: "", list: [] },
noticeData: { title: "", link: "" },
successData: { title: "" },
notificationCenter: false,
notificationList: [],
tempNotificationList: [],
});
});
it("should display error notification", () => {
// Pre-set store state
useAlertStore.setState({
errorData: { title: "Something went wrong", list: ["Detail"] },
});
render(<NotificationBanner />);
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
});
});
Use when the real store has complex initialization or import.meta dependencies:
const mockFlowStore = {
nodes: [],
edges: [],
setNodes: jest.fn(),
setEdges: jest.fn(),
onNodesChange: jest.fn(),
onEdgesChange: jest.fn(),
onConnect: jest.fn(),
};
jest.mock("@/stores/flowStore", () => ({
__esModule: true,
default: (selector?: (state: any) => any) =>
selector ? selector(mockFlowStore) : mockFlowStore,
}));
describe("FlowCanvas", () => {
beforeEach(() => {
jest.clearAllMocks();
mockFlowStore.nodes = [];
mockFlowStore.edges = [];
});
it("should render nodes from the store", () => {
mockFlowStore.nodes = [
{ id: "node-1", type: "genericNode", data: { node: { display_name: "OpenAI" } }, position: { x: 0, y: 0 } },
];
render(<FlowCanvas />);
expect(screen.getByText("OpenAI")).toBeInTheDocument();
});
});
renderHook for Store TestsFor testing the store itself:
import { act, renderHook } from "@testing-library/react";
import useMyStore from "../myStore";
describe("useMyStore", () => {
beforeEach(() => {
useMyStore.setState({ count: 0 });
});
it("should increment count", () => {
const { result } = renderHook(() => useMyStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Langflow uses react-router-dom v6 (NOT Next.js routing).
import { MemoryRouter } from "react-router-dom";
it("should render the page", () => {
render(
<MemoryRouter initialEntries={["/flows/flow-123"]}>
<FlowPage />
</MemoryRouter>,
);
});
const mockNavigate = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockNavigate,
}));
it("should navigate to flow page on click", async () => {
const user = userEvent.setup();
render(
<MemoryRouter>
<FlowCard flow={mockFlow} />
</MemoryRouter>,
);
await user.click(screen.getByText("Open Flow"));
expect(mockNavigate).toHaveBeenCalledWith("/flow/flow-123");
});
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useParams: () => ({ flowId: "flow-123" }),
}));
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useSearchParams: () => [new URLSearchParams("tab=settings"), jest.fn()],
}));
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
mutations: {
retry: false,
},
},
});
}
function renderWithQueryClient(ui: React.ReactElement) {
const queryClient = createTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
);
}
jest.mock("@tanstack/react-query", () => ({
...jest.requireActual("@tanstack/react-query"),
useMutation: jest.fn().mockReturnValue({
mutate: jest.fn(),
mutateAsync: jest.fn(),
isPending: false,
isError: false,
error: null,
data: undefined,
}),
}));
For components that depend on React context:
import { AuthContext } from "@/contexts/authContext";
const mockAuthContext = {
isAuthenticated: true,
userData: { id: "user-1", username: "testuser" },
login: jest.fn(),
logout: jest.fn(),
getAuthentication: jest.fn(),
autoLogin: false,
};
it("should show user name when authenticated", () => {
render(
<AuthContext.Provider value={mockAuthContext}>
<UserMenu />
</AuthContext.Provider>,
);
expect(screen.getByText("testuser")).toBeInTheDocument();
});
jest.mock("react", () => ({
...jest.requireActual("react"),
useContext: jest.fn().mockReturnValue({
isAuthenticated: true,
userData: { username: "testuser" },
}),
}));
When a child component is complex or has side effects:
jest.mock("@/components/core/chatView/ChatView", () => ({
__esModule: true,
default: ({ onSend }: any) => (
<div data-testid="mock-chat-view">
<button onClick={() => onSend("test message")}>Send</button>
</div>
),
}));
Never mock components from @/components/ui/:
Button, Input, Select, Dialog, Popover, etc.// Mock @xyflow/react for flow-related tests
jest.mock("@xyflow/react", () => ({
ReactFlow: ({ children }: any) => <div data-testid="react-flow">{children}</div>,
useReactFlow: () => ({
getNodes: jest.fn().mockReturnValue([]),
getEdges: jest.fn().mockReturnValue([]),
setNodes: jest.fn(),
setEdges: jest.fn(),
fitView: jest.fn(),
zoomIn: jest.fn(),
zoomOut: jest.fn(),
}),
useNodesState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]),
useEdgesState: jest.fn().mockReturnValue([[], jest.fn(), jest.fn()]),
Background: () => null,
Controls: () => null,
MiniMap: () => null,
Handle: ({ type, position }: any) => (
<div data-testid={`handle-${type}-${position}`} />
),
Position: { Top: "top", Bottom: "bottom", Left: "left", Right: "right" },
MarkerType: { ArrowClosed: "arrowclosed" },
}));
These are globally mocked in setupTests.ts -- do not re-mock:
ResizeObserverIntersectionObserverwindow.matchMediaconst originalLocation = window.location;
beforeEach(() => {
Object.defineProperty(window, "location", {
value: { ...originalLocation, href: "http://localhost:3000", assign: jest.fn() },
writable: true,
});
});
afterEach(() => {
Object.defineProperty(window, "location", {
value: originalLocation,
writable: true,
});
});
Object.assign(navigator, {
clipboard: {
writeText: jest.fn().mockResolvedValue(undefined),
readText: jest.fn().mockResolvedValue("clipboard content"),
},
});
Mocking before imports: jest.mock() calls are hoisted automatically by Jest. You do not need to place them before import statements (but it is conventional to do so for readability).
Forgetting __esModule: true: When mocking a module with default exports, include __esModule: true in the mock factory.
Over-mocking: Only mock what is necessary. If the real module works in jsdom, prefer using it.
Not resetting mocks: Always use jest.clearAllMocks() in beforeEach to prevent test contamination.
Mocking globally-mocked modules: Check jest.setup.js first. Duplicating a mock may cause unexpected behavior.