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.
useEffectfor 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 thesetActiveDatafunction, what on earth are you doing that you expect that function to change constantly?activeDatamay change from user interactions and other api calls. E.g. clicking on a page will updateactiveData.page, clicking on "Add page" will trigger a POST request and the returned page will be saved toactiveData.pageas wellsetActiveDataas a dependency. I'm not sure why either, but I figured I'd just follow ituseInitialDatadoes in your post, because it's a good bet that's returningundefinedinstead of always returning real content or if there isn't any, an empty object{}.