0

I am trying to write unit tests for my React application, built with react-query and ky. I am constantly faced with the error Error: Uncaught [TypeError: Cannot read properties of undefined (reading 'name')] when running vitest, but when I run my application on the browser (not in test mode) everything works fine, there are zero errors thrown.

The error is thrown in the following lines (in the JSX markup):

          <input
            type='text'
            value={activeData.page.name}    <== this line
            onInput={onPageNameChange}
            className='w-full h-full px-4 text-2xl'
            data-testid='page-name-input'
          />

This is my full code:

export default function NotebookPage() {
  const [activeData, setActiveData] = useImmer<ActiveData>({
    notebook: { id: -1, name: '', sections: [] },
    section: { id: -1, name: '', pages: [] },
    page: { id: -1, name: '', content: '' },
  });

  const initialDataQuery = useInitialData();

  useEffect(() => {
    if (initialDataQuery.data) {
      const data = initialDataQuery.data;
      setActiveData({
        notebook: data.notebook,
        section: data.section,
        page: data.page,
      });
    }
  }, [initialDataQuery.data]);

  const onContentChange = (content: string) => {
    setActiveData((draft) => {
      draft.page.content = content;
    });
  };

  const onPageNameChange = (e: FormEvent<HTMLInputElement>) => {
    const name = e.currentTarget.value;
    setActiveData((draft) => {
      draft.page.name = name;
    });
  };

  if (initialDataQuery.isFetching) {
    return <p>Loading...</p>;
  }

  if (initialDataQuery.isError) {
    return <p>An error has occurred: {initialDataQuery.error.message}</p>;
  }

  return (
    <div className='h-screen flex'>
      <div className='w-1/5'>
        <NavPane activeData={activeData} setActiveData={setActiveData} />
      </div>

      <div className='flex-1 flex flex-col'>
        <div className='h-16 border-b border-slate-300'>
          <input
            type='text'
            value={activeData.page.name}
            onInput={onPageNameChange}
            className='w-full h-full px-4 text-2xl'
            data-testid='page-name-input'
          />
        </div>

        <div className='h-[calc(100%-4rem)] flex'>
          <div id='editor-container' className='flex-1 border-r border-slate-300'>
            <EditorPane content={activeData.page.content} onContentChange={onContentChange} />
          </div>
          <div id='preview-container' className='flex-1'>
            <PreviewPane rawText={activeData.page.content} />
          </div>
        </div>
      </div>
    </div>
  );
}

It does not look like there is any way for activeData.page to be undefined; in fact activeData.section is also undefined when I inspected the state further (only activeData.notebook is normal). useInitialData returns a useQuery hook, with the queryFn being a simple ky.get(<url>) that returns { notebook, section, page }. I am using MSW to mock this endpoint, so it is not possible for it to return undefined.

This is the test I am writing:

test.only('can parse to markdown', async () => {
  const user = userEvent.setup();
  render(<NotebookPage />);

  await user.clear(await screen.findByTestId('editor'));
  await user.type(await screen.findByTestId('editor'), '# hello world');

  const h1 = (await screen.findByTestId('preview')).querySelector('h1');
  expect(h1).toBeTruthy();
  expect(h1).toHaveTextContent('hello world');
});

I have tried adding the following waitFor before doing my actions thinking maybe the DOM needs more time to update, but the same error happens.

  await waitFor(async () => {
    expect(await screen.findByTestId('editor')).toBeInTheDocument();
  });

If I keep re-running the test, it passes like ~30% of the time, which is puzzling. I have also checked all calls to setActiveData, and unless I am missing something, none of the calls were made with any undefined data.

9
  • Why are you using a useEffect for what is obviously direct state updating? Just run that code, it's not a "side effect of rendering the component", it does not belong in a useEffect. And that useEffect should definitely not have an update dependency on the setActiveData function, what on earth are you doing that you expect that function to change constantly? Commented Jun 15, 2024 at 15:56
  • @Mike'Pomax'Kamermans because after the initial data has loaded, activeData may change from user interactions and other api calls. E.g. clicking on a page will update activeData.page, clicking on "Add page" will trigger a POST request and the returned page will be saved to activeData.page as well Commented Jun 15, 2024 at 15:59
  • @Mike'Pomax'Kamermans eslint flags to pass setActiveData as a dependency. I'm not sure why either, but I figured I'd just follow it Commented Jun 15, 2024 at 16:03
  • Don't. Instead, (also) talk about that error in your post, because "just following it" makes no sense: that function will never change. Commented Jun 15, 2024 at 16:17
  • 1
    On that note, also show what useInitialData does in your post, because it's a good bet that's returning undefined instead of always returning real content or if there isn't any, an empty object {}. Commented Jun 15, 2024 at 16:33

1 Answer 1

0

I figured out the problem... Turns out it was the -1 ids when I defined my activeData state. In my child components I use activeData.notebook.id and activeData.section.id to fetch more data, but after useInitialData, the set state call hasn't fully resolved so -1 was used in my api calls. Since -1 isn't a proper id my api resolves to an empty array, which I did not check for and immediately tried to do array[0], hence causing undefined in my activeData state.

My next question now is how come in the development server when developing on the browser, this error isn't caught...

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.