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:

  • This React front-end is part of a Rails app (webpack).
  • Using yarn
  • I have a lot of control over how we achieve this, so I'm looking for the simplest/cleanest way to do this.
2

There are 2 best solutions below

1
On BEST ANSWER

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.

0
On

Jest uses jsdom under the hood, so it's not true that "there is no DOM", however all the page is instantiated in the test - nothing comes from server(and jsdom neither supports navigation to request real page from the server and then continue testing).

Therefore we need to render element in the test:

render(<>
  <meta name="csrf-token" content="mocked-token" />
  <Main/>
</>);

Though I'm not sure why complete page replacement you tried in globalSetup did not work, maybe Jest does not allow to override JSDOM instance this way and binds earlier than globalSetup.js is ever run.