How do you stop tests from randomly failing when a component measures the DOM?

Yo Kirupa folks, I’m trying to make our React component tests less flaky and I think I’m shooting myself in the foot with DOM measurement + state updates (it fails like 1/20 runs, usually in CI).

import { useLayoutEffect, useRef, useState } from "react";

export function ClampTitle({ text }: { text: string }) {
  const ref = useRef<HTMLDivElement>(null);
  const [clamped, setClamped] = useState(false);

  useLayoutEffect(() => {
    const el = ref.current;
    if (!el) return;

    // measure -> update state -> re-render
    const next = el.scrollWidth > el.clientWidth;
    setClamped(next);
  }, [text]);

  return (
    <div
      ref={ref}
      style={{ width: 180, whiteSpace: "nowrap", overflow: "hidden" }}
      data-clamped={clamped}
    >
      {text}
    </div>
  );
}

What’s the most reliable way to test this kind of “measure then setState” UI without brittle timing hacks, and how do you keep it from turning into a CI-only flaky test?

This is JSDOM being fake about layout, not React being random.

scrollWidth and clientWidth need to be mocked before the effect reads them, because JSDOM won’t calculate real sizes for you. I usually do it on HTMLElement.prototype in the test setup, or I pull the measurement into a tiny helper/hook and mock that instead so the component test isn’t doing geometry cosplay.

import { render, screen, waitFor } from "@testing-library/react";
import { ClampTitle } from "./ClampTitle";

test("sets data-clamped when content overflows", async () => {
  Object.defineProperty(HTMLElement.prototype, "scrollWidth", {
    configurable: true,
    value: 500,
  });
  Object.defineProperty(HTMLElement.prototype, "clientWidth", {
    configurable: true,
    value: 180,
  });

  render(<ClampTitle text="hello world" />);

  await waitFor(() => {
    expect(screen.getByText("hello world")).toHaveAttribute("data-clamped", "true");
  });
});

waitFor here is just for the state update after useLayoutEffect runs. It’s not fixing the layout problem — the mock is.

1 Like