Import referencing store used in context causes createRoot error

52 views Asked by At

I am writing unit tests and integration tests that test my MobX stores. These stores are provided in the app by a single context provider (using the root store pattern) in index.tsx.

import "typeface-roboto";
import { createRoot } from "react-dom/client";
import "./index.css";
import { store, StoreContext } from "./stores/store";
import CacheBuster from "./components/CacheBuster";
import { ToastContainer } from "react-toastify";
import { ThemeProvider } from "@mui/material/styles";
import theme from "./themes/AppFrameHeader";
import packageJson from "../../package.json";
import { RouterProvider } from "react-router";
import { router } from "./components/Routing/Router";

export const appVersion: string = packageJson.version;

const container = document.getElementById("root");
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
root.render(
  <CacheBuster>
    <ThemeProvider theme={theme}>
      <StoreContext.Provider value={store}>
        <ToastContainer position="top-right" autoClose={500} />
        <RouterProvider router={router} />
      </StoreContext.Provider>
    </ThemeProvider>
  </CacheBuster>
);

However, when I try to test these stores or any function file that imports them, I get the error:

FAIL  src/components/__tests__/deserializeFilterSearchParams.test.ts [ src/components/__tests__/deserializeFilterSearchParams.test.ts ]
Error: createRoot(...): Target container is not a DOM element.
 ❯ createRoot node_modules/react-dom/cjs/react-dom.development.js:29345:11
 ❯ Object.createRoot$1 [as createRoot] node_modules/react-dom/cjs/react-dom.development.js:29800:10
 ❯ Proxy.createRoot node_modules/react-dom/client.js:12:16
 ❯ src/index.tsx:25:14
     23|
     24| const container = document.getElementById("root");
     25| const root = createRoot(container!); // createRoot(container!) if you use TypeScript
       |              ^
     26| root.render(
     27|   <CacheBuster>
 ❯ src/components/Settings/Settings.tsx:8:31

My configs are as follows: vite.config.ts

import svgrPlugin from "vite-plugin-svgr";
import eslint from "vite-plugin-eslint";
/// <reference types="vitest" />

export default defineConfig(() => {
  return {
    build: {
      outDir: "build"
    },
    server: {
      port: 3000
    },
    plugins: [react(), viteTsconfigPaths(), svgrPlugin(), eslint()],
    assetsInclude: ["**/*.md"],
    optimizeDeps: {
      include: [
        "@emotion/styled",
        "@arcgis/core/WebMap",
        "@arcgis/core/views/MapView",
        "@arcgis/core/widgets/Bookmarks",
        "@arcgis/core/layers/GraphicsLayer",
        "@arcgis/core/layers/FeatureLayer",
        "@arcgis/core/Graphic",
        "@arcgis/core/geometry/Point.js",
        "@arcgis/core/geometry/Polygon",
        "@arcgis/core/widgets/Expand",
        "@arcgis/core/layers/GeoJSONLayer.js",
        "@arcgis/core/widgets/support/widgetUtils.js"
      ]
    },
    base: "./",
    test: {
      globals: true,
      environment: "jsdom",
      setupFiles: "./tests/setup.ts",
      deps: {
        optimizer: {
          web: {
            enabled: true
          },
          ssr: {
            enabled: true
          }
        }
      },
    }
  };
});

/tests/setup.ts

import { vi, afterEach } from "vitest";
import { cleanup } from "@testing-library/react";

// runs a cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
  cleanup();
});

// Mock the ResizeObserver - necessary for MapStore and potentially others since 
// ResizeObserver is not available in the testing environment
const ResizeObserverMock = vi.fn(() => ({
  observe: vi.fn(),
  unobserve: vi.fn(),
  disconnect: vi.fn(),
}));

// Stub the global ResizeObserver
vi.stubGlobal('ResizeObserver', ResizeObserverMock);

// The below commented mock didn't work
// vi.mock("../stores/store", () => {
//   const store = new Store();
//   return {
//     store,
//     StoreContext: React.createContext(store),
//     useStore: () => store,
//   };
// });

Versions (not exhaustive):

react: 18.2.0
@types/react: ^18.2.14
react-dom: 18.2.0
@babel/core: 7.22.5
@jest/globals: 29.6.0
@testing-library/dom: 9.3.1
@testing-library/jest-dom: ^6.1.4,
@testing-library/react: ^14.0.0,
@testing-library/user-event: ^14.5.1,
@vitejs/plugin-react-swc: ^3.3.2,
jsdom: ^22.1.0,
vite: 4.4.9,
vitest: 0.34.3

These tests were working a few weeks ago, but I'm guessing that due to an update of our npm packages, something changed slightly and now the rendering pipeline within vitest is different.

  1. I have tried to mock away the store, so that it's pulling a store object not related to the one provided in the context. This produces a different error about not referencing the store before it is initialized (though it is built as a global in the test - but these errors are occurring in the import phase).
  2. I've tried separating the definition of the store from the definition of the context and hook in separate files (saw someone reference that as a possible solution here on SO).
  3. I've tried isolating a sub-store from the parent/root store by creating a separate instance of it, this worked for the unit test that tested the store itself, but a function that referenced the same store fails with the error.
  4. I've tried moving the bulk of the code from index.tsx to a sub-component i.e. , then call a render from react-testing-environment on beforeAll tests.
  5. I've tried running the failing tests in debug mode, but they fail on instantiation, so never reach a breakpoint.

The below test works (from #3 above):

import { describe, it, expect, beforeEach } from "vitest";

import MyFiltersStore from "../myFiltersStore";

describe("Class MyFiltersStore", () => {
    let myFiltersStore = new MyFiltersStore();
    beforeEach(() => {
        myFiltersStore = new MyFiltersStore();
    });

    it("should properly instantiate the class properties", () => {
        expect(myFiltersStore.selectedOrg).toBe(null);
        expect(myFiltersStore.searchString).toBe("");
        // ...
    });

    // more tests...
})

But this one doesn't:

import { describe, it, expect, beforeEach } from "vitest";
import { serializeCopSearchValues } from "../serializeCopSearchValues";
import MyFiltersStore from "../../../../../stores/myFiltersStore";
import { MyStatus } from "../../../../../api/MyStatus";


describe("serializeCopSearchValues()", () => {
  let myFiltersStore = new MyFiltersStore();
  beforeEach(() => {
    myFiltersStore = new MyFiltersStore();
  });

  it("should successfully serialize statuses if they are defined", () => {
    const mockStatuses = [MyStatus.APPROVED, MyStatus.COMPLETED];
    myFiltersStore.setStatuses(mockStatuses);
    const result = serializeCopSearchValues(new Date(Date.now()), false, false);
    expect(result.statuses).toEqual([
      MyStatus[OaiStatus.APPROVED],
      MyStatus[OaiStatus.COMPLETED]
    ]);
  });

  // .. more tests
});
0

There are 0 answers