1

This is probably more of a JavaScript/TypeScript question then it is about React/Testing.

But I'll give the complete story. So I have a test app with basic routing functionality and tests to verify that the routing works.

App.tsx https://github.com/Leejjon/pwa-seo/blob/6f621968de1184b03744a262a68d291b4571c5c1/src/App.tsx

App.test.tsx https://github.com/Leejjon/pwa-seo/blob/6f621968de1184b03744a262a68d291b4571c5c1/src/App.test.tsx

Everything worked fine. Then I added an useEffect hook to initialize my internationalization library:

useEffect(() => {
    async function initMessages() {
        await intl.init({
            currentLocale: "en-US",
            locales
        });
    }

    initMessages().then(() => setLoading(false));
}, [loading]);

This loads all my text assets in English. This works fine, but broke all my tests with the following error message: Warning: An update to App inside a test was not wrapped in act(...).

After some reading up on the internet I managed to fix my tests by adding this 'act' function, here is one example:

import React from 'react';
import {act, render, fireEvent, waitForElement} from '@testing-library/react';
import "@testing-library/jest-dom/extend-expect";
import App from './App';

test('Verify home page header', async() => {
    let app: HTMLElement;
    await act(async () => {
        const {container} = render(<App/>);
        app = container;
    });
    // @ts-ignore
    if (app) {
        const pageHeaderContent = app.querySelector("#pageHeader")?.firstChild?.textContent;
        expect(pageHeaderContent).toMatch('Home page');
    } else {
        fail("The app should have been initialized.");
    }
});

Right now I'm suppressing the TS2454: Variable 'app' is used before being assigned. warning with the @ts-ignore. This is ugly. If I move my assertions into the act function, I get the same Warning: An update to App inside a test was not wrapped in act(...). error again.

Is there a way to obtain the container object destructured from the render function without having to use the @ts-ignore and the if clause to do null checking?

I created a tag for the current code related to this question: https://github.com/Leejjon/pwa-seo/releases/tag/uglylines Link to last commit: https://github.com/Leejjon/pwa-seo/commit/2434f78c0619be2d55f9de965149f6bd6d1a0b90

Leejjon
  • 680
  • 2
  • 7
  • 24
  • It can be undefined but you declare it to be HTMLElement only? – Estradiaz Dec 18 '19 at 23:29
  • 1
    TypeScript can't tell if `act` is going to invoke the callback, and cannot guarantee that `app` is going to be defined. You can try wrapping `act` in another promise that will resolve with `app` from inside the callback. You'll also probably need to wait for `act` to resolve as well before proceeding. – Alexey Lebedev Dec 18 '19 at 23:32
  • What @alexey said. You could move your test code inside your callback. That would get TS to shut up, since it would guarantee app is defined. – colefner Dec 18 '19 at 23:36
  • @colefner he tried that, but that defeats the purpose of `act`, which is to allow React to finish all lifecycle hooks before making assertions with `expect`. – Alexey Lebedev Dec 18 '19 at 23:39
  • Ah yeah, I missed that part. Good catch there, @AlexeyLebedev – colefner Dec 18 '19 at 23:41
  • Another idea: `render()` renders into `document.body`, which means you can replace `app.querySelector` with `document.querySelector()`. It's not an exact equivalent, but for this specific test it would work. You can also provide a container of your choice to the `render` function. – Alexey Lebedev Dec 18 '19 at 23:48

2 Answers2

1

Typescript is complaining about the app variable to not have been initialised when you access it in the if-statement. You can simply fix that by assigning null to it.

let app: HTMLElement = null;

In case you use strict null checks you have to allow null on the type:

let app: HTMLElement | null = null;
Thomas Preißler
  • 613
  • 4
  • 13
0

After puzzling this is my result

test('Verify home page header', async() => {
    let app: HTMLElement | undefined = undefined;
    await act(async () => {
        const {container} = render(<App/>);
        app = container;
    });
    let appAsHtmlElement = (app as unknown as HTMLElement);
    const pageHeaderContent = appAsHtmlElement.querySelector("#pageHeader")?.firstChild?.textContent;
    expect(pageHeaderContent).toMatch('Home page');
});

Better suggestions (if there is some way of not having to use the 'act' function) are still welcome.

Leejjon
  • 680
  • 2
  • 7
  • 24