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.
- 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).
- 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).
- 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.
- 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.
- 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
});