skip to Main Content

I’m testing a custom react hook.
At any given point in time, the Funnel component displays only one child (a Funnel.Step component). It first displays the first Funnel.Step component (<div>Step 1 UI</div>).

I used act to change the Funnel.Step component. To wait for it to re-render first, I used await waitFor for my assertions. This makes the test fail. However, when I just do waitFor, the test succeeds.

This to me is confusing because the fact that the assertions passed when doing waitFor means that the update (<div>Step 1 UI</div> unmounts and <div>Step 2 UI</div> mounts) did indeed apply. So why does await waitFor fail the test?

PS: the reason I wanted to do await waitFor was to be able to print the changed screen (console.log(screen.debug(undefined, Infinity))).

describe("useFunnel hook", () => {
  it("should successfully render the second step in the funnel", () => {
    const { result } = renderHook(() => useFunnel(["step1", "step2"]));
    const [Funnel, changeStep] = result.current;

    const { getByText, queryByText } = render(
      <React.Fragment>
        <Funnel>
          <Funnel.Step name="step1">Step 1 UI</Funnel.Step>
          <Funnel.Step name="step2">Step 2 UI</Funnel.Step>
        </Funnel>
      </React.Fragment>
    );

    act(() => {
      changeStep("step2");
    });

    waitFor(() => { // `await waitFor` fails
      expect(getByText("Step 2 UI")).toBeDefined();
      expect(queryByText("Step 1 UI")).toBeNull();
    });
  });
});
import { Children, ReactNode, isValidElement, useState } from "react";

export default function useFunnel<S extends string>(steps: [S, ...S[]]) {
  const [currentStep, setCurrentStep] = useState<S>(steps[0]);

  const changeStep = (step: S) => {
    setCurrentStep(step);
  };

  function Step({ children }: { name: S; children: ReactNode }) {
    return <>{children}</>;
  }

  function Funnel({ children }: { children: ReactNode }) {
    const targetStep = Children.toArray(children).find((child) => {
      if (!isValidElement(child) || child.type !== Step) {
        throw new Error(
          `${
            isValidElement(child) ? child.type : child
          } is not a <Funnel.Step> component. All component children of <Funnel> must be a <Funnel.Step>.`
        );
      }
      return child.props.name === currentStep;
    });

    return <>{targetStep}</>;
  }

  const FunnelComponent = Object.assign(Funnel, { Step });

  return [FunnelComponent, changeStep] as const;
}

2

Answers


  1. Chosen as BEST ANSWER

    I'm not sure if the issue was

    waitFor is causing renderHook to create an entirely new instance of your hook which would default it back to the first step and lose the state update you made inside act

    or something else. But the assertions were passing when the screen was showing it shouldn't.

    So, I just instantiated a component that uses the useFunnel hook and then used render to render that component instead of doing renderHook()() => useFunnel(["step1", "step2"]). The screen now successfully shows the updated UI to Step 2.

    function TestComponent() {
      const [Funnel, changeStep] = useFunnel(["step1", "step2"]);
    
      return (
        <React.Fragment>
          <Funnel>
            <Funnel.Step name="step1">Step 1 UI</Funnel.Step>
            <Funnel.Step name="step2">Step 2 UI</Funnel.Step>
          </Funnel>
          <button onClick={() => changeStep("step2")}>Next Step</button>
        </React.Fragment>
      );
    }
    
    describe("useFunnel hook", () => {
      it("should successfully unmount the first step and render the second step in the funnel", async () => {
        const { getByText, queryByText } = render(<TestComponent />);
    
        fireEvent.click(getByText("Next Step"));
    
        expect(getByText("Step 2 UI")).toBeDefined();
        expect(queryByText("Step 1 UI")).toBeNull();
      });
    });
    

  2. It’s hard to tell what timing issue you may be encountering without seeing the implementation details of changeStep, but more than likely, you should be able await the call to act and skip using a waitFor around your expectations entirely.

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