reactjstestingdomjestjsjest-dom

Trouble testing React component render with Jest and Testing-Library because there's no Document available to Jest


Context/Setup:

I'm trying to use Jest and React-Testing-Library to test the render of a React component <Main/>, but when I run the test, the client that handles fetch throws an error because it's using document.querySelector() - but when Jest runs there's no document because there's no browser rendered.

My Goal Here: Get Jest and RTL set up so we can start writing tests for all the components. I would like to start with verifying that I can render <Main/> without errors.

Here's Client.js

class Client {
  constructor() {
    console.log("Client initializing")
    console.log(document.querySelector('meta[name="csrf-token"]'))
    console.log(document)

    this.token = document.querySelector('meta[name="csrf-token"]').content;
    
  }

  getData(path) {
    return (
      fetch(`${window.location.origin}${path}`, {
        headers: { "X-CSRF-Token": this.token }
      })
    )
  }

  submitData(path, method, body) {
    return (
      fetch(`${window.location.origin}${path}`, {
        method: method,
        headers: { "X-CSRF-Token": this.token },
        body: body
      })
    )
  }

  deleteData(path) {
    return (
      fetch(`${window.location.origin}${path}`, {
        method: "DELETE",
        headers: {
          "X-CSRF-Token": this.token,
          "Content-Type": "application/json"
        }
      })
    )
  }

}
export default Client;

Here's main.test.js:

/**
 * @jest-environment jsdom
 */

import React from 'react';
import { render, screen } from '@testing-library/react';
// import userEvent from '@testing-library/user-event';
import Main from '../../app/javascript/components/Main';

test("renders without errors", ()=> {
    render(<Main/>);

});

I've also set up a setupTests.js file:


require("jest-fetch-mock").enableMocks();
import '@testing-library/jest-dom';

And called it here in package.json:

"jest": {
        "roots": [
            "test/javascript"
        ],
        "moduleNameMapper": {
            "\\.(svg|png)": "<rootDir>/__mocks__/svgrMock.js"
        },
        "automock": false,
        "setupFilesAfterEnv": [
            "./test/javascript/setupTests.js"
        ]
    },

I have also set testEnvironment: 'jsdom' in the jest.config.js file.

Current Problem:

When I run yarn jest I get the following error: TypeError: Cannot read properties of null (reading 'content') which points to this.token = document.querySelector('meta[name="csrf-token"]').content; in Client.js

This makes sense to me because it's looking for a DOM element, but Jest runs in Node (no browser render) so there's no DOM to be found.

I think I need to:

  1. Mock the document so the app can run without being rendered in the browser. Not sure how to do this.
  2. Then mock the fetch calls (maybe?) not sure how to do this either tbh.

What I've Tried So Far:

1. I've tried various ways to mock the DOM elements globally (from setupTests.js) including many permutations of something like this:

import { TextDecoder, TextEncoder } from 'util'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder

//variables to mock a csrf token
const csrfToken = 'abcd1234';
const virtualDom = `
<!doctype html>
    <head>
        <meta name="csrf-token" content="${csrfToken}" />
    </head>
  <body>
    <form>
        <input type="hidden" name="csrf-token" value=${csrfToken}>
      </form>
  </body>
</html>
`;

const { JSDOM } = require("jsdom");
//mock a page passing virtualDom to JSDOM
const page = new JSDOM(virtualDom);

const { window } = page;

function copyProps(src, target) {
    const props = Object.getOwnPropertyNames(src)
      .filter(prop => typeof target[prop] === 'undefined')
      .map(prop => Object.getOwnPropertyDescriptor(src, prop));
    Object.defineProperties(target, props);
  }

global.window = window;
global.document = window.document;
global.navigator = {
  userAgent: 'node.js',
};
copyProps(window, global);

But global.window = window never seems to work because if I declare that and then immediately console.log(global.window, window) i get null and then a window.

2. I've tried temporarily downgrading React 18 to React 17 (based on some StackOverflow exchanges) - I know this isn't optimal, but it got me to the point where I would have to mock the fetch() calls at least.

I don't know how to do that properly either tbh, but I also know downgrading React is probably the wrong path here.

Other potentially-important context:


Solution

  • Turns out I had to mock all the data coming in to the components that render from Main. (In addition to the CSRF token mocking)

    Once I did that, everything else fell into place.