skip to Main Content
function App() {
  let [ctr, setCtr] = useState(0);
  useEffect(() => {
    setCtr(1);
  }, []);
              //<-------------------------put a debugger here, only hit once
  return ctr;
}
// debug this test
it("should render 1", () => {
  const el = document.createElement("div");
  ReactDOM.render(<App />, el);
  expect(el.innerHTML).toBe("1"); // this fails!
});

currently this test fails because the test’s expect(...) finishes before useEffect trigger another update. So I want to put something into the test to make it wait for another update finished so I can see the debugger hit twice. I have tried:

it("should render 1", () => {
   const el = document.createElement("div");
   ReactDOM.render(<App />, el);

   for (let i = 0; i < 2000; i++) {   // add a long loop so it could have a chance for the `useEffect` trigged update finishes
      // ...
   }
   expect(el.innerHTML).toBe("1"); // this fails!
});

but I still cannot control js runtime that the next job to be executed and looks like expect(el.innerHTML).toBe("1") is executed immediately after the loop finishes. So is it a way for me to add something like a trick so I can see the debugger hit twice, i.e let useEffect’s update executes before the expect(...) statement

2

Answers


  1. You may need useLayoutEffect hook.

    function App() {
      let [ctr, setCtr] = useState(0);
      useEffect(() => {
        setCtr(1);
        console.log("Executed.")
      }, []);
    
      useLayoutEffect(() => {
        console.log("Executed before useEffect called.")
      })
    
      return ctr;
    }
    
    Login or Signup to reply.
  2. Your test won’t work because you aren’t waiting for the re-render to happen — there isn’t anything in your test to trigger the re-render after the useEffect calls setCtr. TBH, I can’t even tell if this code would actually render the DOM to assert tests against…

    I reckon a better approach would be to use react-testing-library which gives you the tooling to test your React components without trying to re-invent the wheel.

    Here’s a Code Sandbox with react-testing-library setup and a simple assertion that the value 1 is in the DOM. You can run the test in the Tests tab in Code Sandbox. Below are the relevant code snippets.

    // App.tsx
    export default function App() {
      const [ctr, setCtr] = useState(0);
    
      useEffect(() => {
        setCtr(1);
      }, []);
    
      return (
        <div className="App">
          <Typography variant="h4" component="h1" gutterBottom>
            SO Help: React Testing Library
          </Typography>
          <Typography>{ctr}</Typography>
        </div>
      );
    }
    
    // __tests__/App.test.tsx
    import { render, screen, waitFor } from "../test-utils";
    import App from "../App";
    
    test("loads and displays greeting", async () => {
      // Render your APP to the DOM
      render(<App />);
    
      // Wait for the DOM to update
      await waitFor(() => {
        expect(screen.getByText("1")).toBeInTheDocument();
      });
    });
    

    Hope this helps!

    Update: Solving for OP’s original issue

    All you need to do is wrap your render in act from react-dom/test-utils.

    To prepare a component for assertions, wrap the code rendering it and performing updates inside an act() call. This makes your test run closer to how React works in the browser.

    Docs for act

    import { render } from "react-dom";
    import { act } from "react-dom/test-utils"
    import App from "./App"
    
    it("should render 1", async () => {
      const el = document.createElement("div");
      await act(async () => {
        render(<App />, el);
      })
      
      expect(el.innerHTML).toBe("1"); 
    });
    

    Updated the Code Sandbox as well. Checkout __tests__/SO.test.tsx

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search