Files
zeekay d650dd60f0 refactor(rebrand): langflow->flow across docs, starter-project data, CI, tests
Docs corpus (Langflow->Hanzo Flow, upstream URLs langflow-ai/langflow->hanzoai/
flow, docs.langflow.org->docs.hanzo.ai), starter-project template help-URLs, CI
workflows, and test files. Remaining 'langflow' repo-wide is now dominated by
LANGFLOW_* env-var names + langflow_ settings fields — the config-boundary layer
deliberately preserved pending a coordinated FLOW_/HANZO_ alias-with-fallback
(renaming them blind breaks every deployment).
2026-07-16 10:00:51 -07:00

9.0 KiB

Async Testing Patterns

Patterns for testing asynchronous behavior in Hanzo Flow 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 waitFor for appearance, not disappearance. Use waitForElementToBeRemoved for that.
  • Do not use waitFor for 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

  1. Forgetting act() with timers: Always wrap jest.advanceTimersByTime() in act() when it triggers React state updates.

  2. Not cleaning up timers: Always call jest.runOnlyPendingTimers() then jest.useRealTimers() in afterEach to prevent timer leaks between tests.

  3. Using waitFor for sync assertions: If the element is already in the DOM after render, use getBy* directly. waitFor adds unnecessary polling.

  4. Missing await on async operations: Forgetting await on waitFor, findBy*, or userEvent methods causes tests to pass vacuously.

  5. Fake timers breaking promises: If promises are not resolving with fake timers, try jest.advanceTimersByTime() inside act(async () => { ... }).