jest: [Bug]: jest.mock not working with ESM support activated

Version

28.1.3

Steps to reproduce

  1. Clone my repo: https://github.com/iamWing/jest-esm-mock-issue
  2. npm I
  3. npm run start to verify Electron app shows up without issue
  4. npm t to run jest for the error

Expected behavior

Expecting Jest to mock functions app.on, app.whenReady and component BrowserWindow from electron, then execute the test cases with the mocked functions.

Actual behavior

Encountered TypeError: Cannot read properties of undefined (reading 'whenReady') when trying to run the test cases.

One possible reason I can think of is the imported module is being executed before jest.mock('electron', ...) in main.spec.js, hence Node is executing the line app.whenReady() in main.cjs with the non-mocked version.

Additional context

I originally tested with TypeScript & ts-jest with ESM support activated. The error I encountered there is very similar, so that I believe the error is from jest instead of ts-jest. See my comment on #10025 (https://github.com/facebook/jest/issues/10025#issuecomment-1214297747)

Environment

System:
    OS: macOS 12.5
    CPU: (16) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
  Binaries:
    Node: 16.16.0 - /usr/local/opt/node@16/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 8.13.2 - /usr/local/bin/npm
  npmPackages:
    jest: ^28.1.3 => 28.1.3

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 18 (11 by maintainers)

Most upvoted comments

@mildmojo

Did you try like this:

- import { createRequire } from "node:module";
  import { jest } from "@jest/globals";
- const require = createRequire(import.meta.url);

- jest.mock("node:child_process");
+ jest.unstable_mockModule("node:child_process", () => ({
+   execSync: jest.fn(),
+ }));

- const { execSync } = require("node:child_process");
+ const { execSync } = await import("node:child_process");
  const myFunc = (await import("../src/myModule.js")).default;

// ...

Thanks for repo. Easy to fix. Change your test file like this:

  import { jest } from "@jest/globals";
- import { BrowserWindow } from "electron";
- import { exportedForTests } from "../src/main.cjs";

  jest.mock("electron", () => ({
    app: {
      on: jest.fn(),
      whenReady: jest.fn(() => Promise.resolve()),
    },
    BrowserWindow: jest.fn().mockImplementation(() => ({
      loadFile: jest.fn(() => Promise.resolve()),
      on: jest.fn(),
    })),
  }));

+ const { BrowserWindow } = await import("electron");
+ const exportedForTests = await import("../src/main.cjs");

  // ...

As you suspected – ESM evaluates import statements before looking at the code. So you have to mock before importing. Same applies for for modules which have to load mocked modules (in this case it is the main.cjs which loads BrowserWindow).

This pattern worked with CJS, because babel-jest transformer is moving jest.mock calls at the top of the file (above any require statement). Hoisting magic does not work with ESM, because any static import will be always evaluated first. Solution: use dynamic import().