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
I'm not sure if the issue was
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 usedrender
to render that component instead of doingrenderHook()() => useFunnel(["step1", "step2"])
. The screen now successfully shows the updated UI to Step 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 toact
and skip using awaitFor
around your expectations entirely.