Async Testing Patterns
Patterns for testing asynchronous behavior in Langflow with Jest and React Testing Library.
waitFor
Use waitFor when you need to wait for an asynchronous operation to complete before making assertions.
import { render, screen, waitFor } from "@testing-library/react";
it("should load and display data", async () => {
jest.mocked(api.get).mockResolvedValueOnce({
data: { items: [{ id: "1", name: "Test Item" }] },
});
render(<ItemList />);
// Wait for the async data to appear
await waitFor(() => {
expect(screen.getByText("Test Item")).toBeInTheDocument();
});
});waitFor Options
await waitFor(
() => {
expect(screen.getByText("loaded")).toBeInTheDocument();
},
{
timeout: 3000, // Max time to wait (default: 1000ms)
interval: 50, // Polling interval (default: 50ms)
},
);waitFor Best Practices
- Put only ONE assertion inside
waitFor. Multiple assertions can cause misleading failures. - Use
waitForfor appearance, not disappearance. UsewaitForElementToBeRemovedfor that. - Do not use
waitForfor synchronous operations -- it adds unnecessary delay.
// GOOD: Single assertion in waitFor
await waitFor(() => {
expect(screen.getByText("Data loaded")).toBeInTheDocument();
});
// BAD: Multiple assertions -- if second fails, first may have been true
await waitFor(() => {
expect(screen.getByText("Data loaded")).toBeInTheDocument();
expect(screen.getByText("5 items")).toBeInTheDocument(); // unreliable
});
// GOOD: Chain waitFor calls
await waitFor(() => {
expect(screen.getByText("Data loaded")).toBeInTheDocument();
});
expect(screen.getByText("5 items")).toBeInTheDocument();waitForElementToBeRemoved
it("should hide loading spinner after data loads", async () => {
jest.mocked(api.get).mockResolvedValueOnce({ data: [] });
render(<ItemList />);
// Spinner appears immediately
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
// Wait for it to disappear
await waitForElementToBeRemoved(() =>
screen.queryByTestId("loading-spinner"),
);
// Now assert the loaded state
expect(screen.getByText("No items found")).toBeInTheDocument();
});findBy Queries
findBy* queries return a promise that resolves when the element appears. They are shorthand for waitFor + getBy*.
it("should display async content", async () => {
jest.mocked(api.get).mockResolvedValueOnce({ data: { name: "Test" } });
render(<AsyncComponent />);
// findByText = waitFor(() => getByText(...))
const element = await screen.findByText("Test");
expect(element).toBeInTheDocument();
});findBy with Custom Timeout
const element = await screen.findByText("Slow content", {}, { timeout: 5000 });Fake Timers
Use fake timers for components that use setTimeout, setInterval, or Date.now.
describe("TimerComponent", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
it("should update after interval", () => {
render(<TimerComponent interval={1000} />);
expect(screen.getByText("0 seconds")).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(3000);
});
expect(screen.getByText("3 seconds")).toBeInTheDocument();
});
it("should clean up interval on unmount", () => {
const clearIntervalSpy = jest.spyOn(global, "clearInterval");
const { unmount } = render(<TimerComponent interval={1000} />);
unmount();
expect(clearIntervalSpy).toHaveBeenCalled();
clearIntervalSpy.mockRestore();
});
});Combining Fake Timers with Async Operations
When a component uses both timers and async operations (API calls), you need special handling:
it("should poll for updates", async () => {
jest.useFakeTimers();
const mockGet = jest.mocked(api.get);
mockGet
.mockResolvedValueOnce({ data: { status: "pending" } })
.mockResolvedValueOnce({ data: { status: "complete" } });
render(<PollingComponent />);
// First fetch happens immediately
await waitFor(() => {
expect(screen.getByText("pending")).toBeInTheDocument();
});
// Advance past the polling interval
act(() => {
jest.advanceTimersByTime(5000);
});
// Second fetch resolves
await waitFor(() => {
expect(screen.getByText("complete")).toBeInTheDocument();
});
jest.useRealTimers();
});Mocking Date.now
For components that use Date.now() for elapsed time:
it("should track elapsed time", async () => {
const realDateNow = Date.now;
let mockTime = 1000000;
Date.now = jest.fn(() => mockTime);
jest.useFakeTimers();
render(<ElapsedTimer />);
// Advance mock time by 2 seconds
mockTime += 2000;
act(() => {
jest.advanceTimersByTime(100); // Trigger interval callback
});
await waitFor(() => {
expect(screen.getByText("2.0s")).toBeInTheDocument();
});
Date.now = realDateNow;
jest.useRealTimers();
});Testing Promise Rejection
it("should display error message on API failure", async () => {
jest.mocked(api.get).mockRejectedValueOnce(new Error("Server error"));
render(<DataComponent />);
await waitFor(() => {
expect(screen.getByText(/server error/i)).toBeInTheDocument();
});
});Testing Debounced Functions
it("should debounce search input", async () => {
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
render(<SearchInput onSearch={mockSearch} debounceMs={300} />);
const input = screen.getByRole("textbox");
await user.type(input, "hello");
// Should not have called search yet
expect(mockSearch).not.toHaveBeenCalled();
// Advance past debounce delay
act(() => {
jest.advanceTimersByTime(300);
});
expect(mockSearch).toHaveBeenCalledWith("hello");
jest.useRealTimers();
});Important: When using userEvent with fake timers, pass advanceTimers: jest.advanceTimersByTime to the setup options. This allows userEvent to advance fake timers during its internal delays.
Testing Event Streams / WebSockets
For components that consume SSE or WebSocket events:
it("should handle streaming messages", async () => {
const mockEventSource = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
close: jest.fn(),
};
jest.spyOn(global, "EventSource" as any).mockImplementation(
() => mockEventSource,
);
render(<StreamingChat />);
// Simulate incoming message
const onMessage = mockEventSource.addEventListener.mock.calls.find(
([event]: [string]) => event === "message",
)?.[1];
act(() => {
onMessage?.({ data: JSON.stringify({ text: "Hello from stream" }) });
});
expect(screen.getByText("Hello from stream")).toBeInTheDocument();
});Testing React Query Mutations
it("should save and show success", async () => {
const user = userEvent.setup();
jest.mocked(api.post).mockResolvedValueOnce({ data: { id: "new-1" } });
render(
<QueryClientProvider client={createTestQueryClient()}>
<CreateForm />
</QueryClientProvider>,
);
await user.type(screen.getByLabelText("Name"), "New Item");
await user.click(screen.getByRole("button", { name: /save/i }));
await waitFor(() => {
expect(screen.getByText(/saved successfully/i)).toBeInTheDocument();
});
expect(api.post).toHaveBeenCalledWith("/api/v1/items", {
name: "New Item",
});
});Testing Loading States
it("should show loading then content", async () => {
let resolvePromise: (value: any) => void;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
jest.mocked(api.get).mockReturnValueOnce(promise as any);
render(<DataDisplay />);
// Loading state
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
expect(screen.queryByText("Data content")).not.toBeInTheDocument();
// Resolve the promise
await act(async () => {
resolvePromise!({ data: { content: "Data content" } });
});
// Loaded state
expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
expect(screen.getByText("Data content")).toBeInTheDocument();
});Common Pitfalls
Forgetting
act()with timers: Always wrapjest.advanceTimersByTime()inact()when it triggers React state updates.Not cleaning up timers: Always call
jest.runOnlyPendingTimers()thenjest.useRealTimers()inafterEachto prevent timer leaks between tests.Using
waitForfor sync assertions: If the element is already in the DOM after render, usegetBy*directly.waitForadds unnecessary polling.Missing
awaiton async operations: ForgettingawaitonwaitFor,findBy*, oruserEventmethods causes tests to pass vacuously.Fake timers breaking promises: If promises are not resolving with fake timers, try
jest.advanceTimersByTime()insideact(async () => { ... }).