jeudi 8 avril 2021

Testing Asynchronous Code (useEffect + fetch) In React Components

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: "Jesus",
      }),
  })
);

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.

Aucun commentaire:

Enregistrer un commentaire