I'm trying to figure out how to test components that update state using useEffect to make an API call to get data. There are several things I think are important to know before I can talk anymore, and that is the files/packages I'm using.
First, I have a main component called App.tsx, inside App.tsx, inside of useEffect, I make a fetch call to an external API to fetch an array of songs by Queen. I also render out a <Song /> component using .map to iterate over each song and .filter to filter songs on UI based on text input. I'm using a custom hook. Here is the code I have for that component and its custom hook.
// App.tsx
type ISong = {
id: number;
title: string;
lyrics: string;
album: string;
};
export default function App() {
const { songs, songError } = useSongs();
const { formData, handleFilterSongs } = useForm();
return (
<Paper>
<h1>Queen Songs</h1>
<FilterSongs handleFilterSongs={handleFilterSongs} />
<section>
{songError ? (
<p>Error loading songs...</p>
) : !songs ? (
<>
<p data-testid="loadingText">Loading...</p>
<Loader />
</>
) : (
<Grid container>
{songs
.filter(
(song: ISong) =>
song.title
.toLowerCase()
.includes(formData.filter.toLowerCase()) ||
song.album
.toLowerCase()
.includes(formData.filter.toLowerCase()) ||
song.lyrics
.toLowerCase()
.split(" ")
.join(" ")
.includes(formData.filter.toLowerCase())
)
.map((song: ISong) => (
<Grid key={song.id} item>
<Song song={song} />
</Grid>
))}
</Grid>
)}
</section>
</Paper>
);
}
// useSongs.tsx
type ISongs = {
id: number;
title: string;
lyrics: string;
album: string;
}[];
type IError = {
message: string;
};
export default function useSongs() {
const [songs, setSongs] = useState<ISongs | null>(null);
const [songError, setSongError] = useState<IError | null>(null);
useEffect(() => {
fetch("https://queen-songs.herokuapp.com/songs")
.then(res => res.json())
.then(songs => setSongs(songs))
.catch(err => setSongError(err));
}, []);
return {songs, songError}
}
Next up is my App.test.tsx file. I am using react-testing-library and jest-dom/extend-expect for my testing coverage. Here is my testing file code. I've been watching a youtube tutorial on the matter and I've read a bunch of articles, but I still can't figure this out.
// App.test.tsx
import * as rctl from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import App from "./App";
// @ts-ignore
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve({
value: [{title: "title1", album: "album1", lyrics: "asdf", id: 1}, {title: "title2", album: "album2", lyrics: "zxcv", id: 2}, etc...],
}),
})
);
describe.only("The App component should", () => {
it("load songs from an API call after initial render", async () => {
await rctl.act(async () => {
await rctl.render(<App />).debug();
rctl.screen.debug();
});
});
});
This code gives me the following error message
FAIL src/pages/App/App.test.tsx
App
× loads the songs on render (117 ms)
● App › loads the songs on render
TypeError: Cannot read property 'then' of undefined
17 |
18 | useEffect(() => {
> 19 | fetch("https://queen-songs.herokuapp.com/songs")
| ^
20 | .then(res => res.json())
21 | .then(songs => {
22 | setSongs(songs)
at src/pages/App/useSongs.ts:19:7
at invokePassiveEffectCreate (node_modules/react-dom/cjs/react-dom.development.js:23487:20)
at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)
at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:338:25)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:3994:16)
at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (node_modules/react-dom/cjs/react-dom.development.js:23574:9)
at unstable_runWithPriority (node_modules/scheduler/cjs/scheduler.development.js:468:12)
at runWithPriority$1 (node_modules/react-dom/cjs/react-dom.development.js:11276:10)
at flushPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:23447:14)
at Object.<anonymous>.flushWork (node_modules/react-dom/cjs/react-dom-test-utils.development.js:992:10)
at flushWorkAndMicroTasks (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1001:5)
at node_modules/react-dom/cjs/react-dom-test-utils.development.js:1080:11
console.log
<body>
<div>
<div
class="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style="text-align: center; overflow: hidden; min-height: 100vh;"
>
<h1>
Queen Songs
</h1>
<div
style="display: flex; flex-flow: column; justify-content: center; text-align: center;"
>
<input
data-testid="input"
id="filter"
name="filter"
placeholder="Search by title, album name, or lyrics here..."
style="width: 18.75rem; height: 1.875rem; align-self: center; text-align: center; font-style: italic;"
type="text"
/>
</div>
<section
style="display: flex; flex-flow: row wrap; justify-content: center; align-items: center;"
>
<p
data-testid="loadingText"
>
Loading...
</p>
<div
class="line-container"
>
<div
class="line"
data-testid="loader-line"
/>
</div>
</section>
</div>
</div>
</body>
at Object.debug (node_modules/@testing-library/react/dist/pure.js:107:13)
console.log
<body>
<div>
<div
class="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style="text-align: center; overflow: hidden; min-height: 100vh;"
>
<h1>
Queen Songs
</h1>
<div
style="display: flex; flex-flow: column; justify-content: center; text-align: center;"
>
<input
data-testid="input"
id="filter"
name="filter"
placeholder="Search by title, album name, or lyrics here..."
style="width: 18.75rem; height: 1.875rem; align-self: center; text-align: center; font-style: italic;"
type="text"
/>
</div>
<section
style="display: flex; flex-flow: row wrap; justify-content: center; align-items: center;"
>
<p
data-testid="loadingText"
>
Loading...
</p>
<div
class="line-container"
>
<div
class="line"
data-testid="loader-line"
/>
</div>
</section>
</div>
</div>
</body>
at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:82:13)
console.error
Error: Uncaught [TypeError: Cannot read property 'then' of undefined]
at reportException (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:62:24)
at innerInvokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:341:9)
at invokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3994:16)
at invokeGuardedCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23574:9)
at unstable_runWithPriority (C:\Users\brian\Code\cra-queen-api-fe\node_modules\scheduler\cjs\scheduler.development.js:468:12) TypeError: Cannot read property 'then' of undefined
at C:\Users\brian\Code\cra-queen-api-fe\src\pages\App\useSongs.ts:19:7
at invokePassiveEffectCreate (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23487:20)
at HTMLUnknownElement.callCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3945:14)
at HTMLUnknownElement.callTheUserObjectsOperation (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventListener.js:26:30)
at innerInvokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:338:25)
at invokeEventListeners (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (C:\Users\brian\Code\cra-queen-api-fe\node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:3994:16)
at invokeGuardedCallback (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23574:9)
at unstable_runWithPriority (C:\Users\brian\Code\cra-queen-api-fe\node_modules\scheduler\cjs\scheduler.development.js:468:12)
at runWithPriority$1 (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:11276:10)
at flushPassiveEffects (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom.development.js:23447:14)
at Object.<anonymous>.flushWork (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:992:10)
at flushWorkAndMicroTasks (C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:1001:5)
at C:\Users\brian\Code\cra-queen-api-fe\node_modules\react-dom\cjs\react-dom-test-utils.development.js:1080:11
at processTicksAndRejections (node:internal/process/task_queues:94:5)
at VirtualConsole.<anonymous> (node_modules/jsdom/lib/jsdom/virtual-console.js:29:45)
at reportException (node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:28)
at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:341:9)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:94:17)
console.error
The above error occurred in the <App> component:
at App (C:\Users\brian\Code\cra-queen-api-fe\src\pages\App\App.tsx:18:32)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
at logCapturedError (node_modules/react-dom/cjs/react-dom.development.js:20085:23)
at update.callback (node_modules/react-dom/cjs/react-dom.development.js:20118:5)
at callCallback (node_modules/react-dom/cjs/react-dom.development.js:12318:12)
at commitUpdateQueue (node_modules/react-dom/cjs/react-dom.development.js:12339:9)
at commitLifeCycles (node_modules/react-dom/cjs/react-dom.development.js:20736:11)
at commitLayoutEffects (node_modules/react-dom/cjs/react-dom.development.js:23426:7)
at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.59 s, estimated 3 s
Ran all test suites related to changed files.
I honestly am completely lost here, and I have no idea what to do next. My usual problem-solving skills are not helping, so I figured I'd turn to SO for some help. Thank you for reading through all of this and for any help you may be able to provide.
Edit: I stripped the code of most of the CSS in the snippets to make it slightly more readable, so that is why the screen.debug() log includes some CSS and the code doesn't.
Edit: I changed the useEffect method to use async/await and now my tests work, but I still have the same output as before. Here is the updated useEffect and the code output.
// Updated useSongs.tsx
export default function useSongs() {
const [songs, setSongs] = useState<ISongs | null>(null);
const [songError, setSongError] = useState<IError | null>(null);
useEffect(() => {
(async() => {
try {
const fetchSongs = await fetch("https://queen-songs.herokuapp.com/songs");
const data = await fetchSongs.json();
setSongs(data);
} catch (error) {
setSongError(error);
}
})()
}, []);
return {songs, songError}
}
// Updated testOutput
PASS src/pages/App/App.test.tsx
App
√ loads the songs on render (52 ms)
console.log
<body>
<div>
<div
class="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style="text-align: center; overflow: hidden; min-height: 100vh;"
>
<h1>
Queen Songs
</h1>
<section
style="display: flex; flex-flow: row wrap; justify-content: center; align-items: center;"
>
<p
data-testid="loadingText"
>
Loading...
</p>
<div
class="line-container"
>
<div
class="line"
data-testid="loader-line"
/>
</div>
</section>
</div>
</div>
</body>
at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:82:13)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.878 s, estimated 1 s
Ran all test suites related to changed files.
I want the test to show the HTML after the useEffect has run and the state has updated, the loading text should be gone.