Search code examples
reactjsjestjs

Test ReactJS custom hook to load images with Jest


I'm trying to test the onload function of a custom hook to load image, but when i run the test, it skips the onload.

Hook:

import { useEffect, useState } from "react";

const useImage = (src: string) => {
    const [loaded, setLoaded] = useState(false);

    useEffect(() => {
        const image = new Image();
        image.src = src;
        image.onload = () => {
            return setLoaded(true); //can't test it
        };
    }, [src]);

    return { loaded };
};

export default useImage;

Test:

import {
    renderHook,
    waitFor,
} from "@testing-library/react";
import useImage from "./useImage";

describe("useImage", () => {

    it("Loads a data url into an image element", async () => {

        const img = new Image();
        img.src = "foo";

        const { result } = renderHook(() => useImage("foo"));

        await waitFor(() =>
            expect(result.current).toStrictEqual({ loaded: true }) // loaded is always false
        );

    });
});

How can i make the test run the onload inside the useEffect? I'm using Jest and Typescript.


Solution

  • First of all, your useImage hook is not doing anything except loading the image, then set a state. If this is only for demonstration purposes, that's fine, otherwise, you should look into what you are trying to achieve with that hook.

    Regarding the test, if you are using jest and jsdom to test. Then jsdom only fire load events when they actually load something. Meaning you must use a real image in the src AND change some jest configuration like this:

    In package.json, if you use jest.config.js, then please refer to the official doc for the equivalent.

      "jest": {
        "preset": "ts-jest",
        "testEnvironment": "jsdom", // we need this line
        "transform": {
          "node_modules/variables/.+\\.(j|t)sx?$": "ts-jest"
        },
        "testEnvironmentOptions": {
          "resources": "usable" // we need this line
        },
        "transformIgnorePatterns": [
          "node_modules/(?!variables/.*)"
        ]
      },
    

    You'll also need to install canvas

     npm i canvas
    

    Then your test is going to pass now. But, if you find this is a bit problematic to install a new dependency and change how the default test framework works, then you can mock the Image object implementation instead, personally, I would prefer this way:

    const LOAD_FAILURE_SRC = 'LOAD_FAILURE_SRC';
    const LOAD_SUCCESS_SRC = 'LOAD_SUCCESS_SRC';
    beforeAll(() => {
        Object.defineProperty(global.Image.prototype, 'src', {
            set(src) {
                if (src === LOAD_FAILURE_SRC) {
                    setTimeout(() => this.onerror(new Error('mocked error')));
                } else if (src === LOAD_SUCCESS_SRC) {
                    setTimeout(() => this.onload());
                }
            },
        });
    });