0

I am having trouble testing one of my components.

I think I understand what's happening. When I enter a new character and the upstream is updated then the component is rendered again but now the component does not have the prior data.

Here is the code:

export const MyTest = (
    {value, updateCallback}: {
        value: string,
        updateCallback: (value: string) => void
    }) => {
    return (
        <>
            <label htmlFor="my-test">My Test</label>
            <input value={value} type="text" id="my-test" onChange={(e) => {
                updateCallback(e.target.value);
                console.log(e.target.value);
            }}/>
        </>
    );
}

And here is the test:

test("test", async () => {
    const user = userEvent.setup();

    const updateCallback = vi.fn()
    render(<MyTest value="" updateCallback={updateCallback}/>)

    const input = screen.getByLabelText("My Test");

    await act(async () => await user.type(input, "my-value"));

    expect(updateCallback).toHaveBeenLastCalledWith("my-value");
})

My Test fails with the following output

m
y
-
v
a
l
u
e

AssertionError: expected last "spy" call to have been called with [ 'my-value' ]
Sparkmuse
  • 61
  • 9
  • What's the problem with that test - does it fail? – jonrsharpe Jun 30 '23 at 13:51
  • I have updated the question. I forgot about to include the output. – Sparkmuse Jun 30 '23 at 14:35
  • The `value` of `MyTest` is _always_ `""`, so that's what each typed character is added to. Presumably this is managed in some state _outside_ the component being tested, but that's not its responsibility so outside the scope of the test. – jonrsharpe Jun 30 '23 at 14:48
  • Have you tried without the acsync/await in `act()`? Also I wonder if `act()` works with functional components in React... – paddotk Jun 30 '23 at 14:48
  • @paddotk that's not relevant (although user event already applies act to is actions so it shouldn't be needed - if that was added to suppress an error note you get issues if you have multiple versions of `@testing-library/dom` installed). – jonrsharpe Jun 30 '23 at 14:55
  • @jonrsharpe Why would you install multiple versions of the same library? Also I don't see why it's not relevant. If the code doesn't wait for the callback in act() to finish, I can imagine that would be the problem (the expect would trigger too soon). Not sure if that's how it works though. – paddotk Jun 30 '23 at 14:58
  • @paddotk you likely wouldn't do it on purpose, but e.g. `@testing-library/user-event@13.50` -> `@testing-library/dom@9.3.1` and `@testing-library/react@13.4.0` -> `@testing-library/dom@8.20.1` if you create a new CRA app right now. And the expect isn't triggering too soon; the callback _has_ been invoked, as many times as it's ever going to be for that input. The test would pass for a single-character input. – jonrsharpe Jun 30 '23 at 15:15

1 Answers1

0

This is how I solved my problem.

test("test", async () => {
    const user = userEvent.setup();

    const updateCallback = vi.fn()
    const useStateMock: any = (initialState: any) => {
        const mockState = useState(initialState);
        const setState = (value) => {
            mockState[1](value);
            updateCallback(value);
        };
        return [mockState[0], setState];
    };
    React.useState = vi.fn().mockImplementation(useStateMock)

    const TestWrapper = () => {
        const [value, setValue] = React.useState("");
        return <CustomInput value={value} updateCallback={setValue}/>
    }
    render(<TestWrapper/>)

    const input = screen.getByLabelText("My Test");

    // Supress: Warning: An update to TestWrapper inside a test was not wrapped in act(...).
    await act(async () => await user.type(input, "my-value"));

    expect(updateCallback).toHaveBeenLastCalledWith("my-value");
})

With some help from:

Mocking React hooks: useState and useEffect

Stackoverflow question

Sparkmuse
  • 61
  • 9