Repository: nodejs/node
Pull request: test_runner: support mocking json modules


Changed files:
filename: doc/api/test.md
patch: @@ -2168,6 +2168,11 @@ test('spies on an object method', (t) => {
 added:
   - v22.3.0
   - v20.18.0
+changes:
+  - version:
+    - REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/58007
+    description: Support JSON modules.
 -->
 
 > Stability: 1.0 - Early development
@@ -2191,10 +2196,10 @@ added:
     mock will throw an exception when used as a CJS or builtin module.
 * Returns: {MockModuleContext} An object that can be used to manipulate the mock.
 
-This function is used to mock the exports of ECMAScript modules, CommonJS
-modules, and Node.js builtin modules. Any references to the original module
-prior to mocking are not impacted. In order to enable module mocking, Node.js must
-be started with the [`--experimental-test-module-mocks`][] command-line flag.
+This function is used to mock the exports of ECMAScript modules, CommonJS modules, JSON modules, and
+Node.js builtin modules. Any references to the original module prior to mocking are not impacted. In
+order to enable module mocking, Node.js must be started with the
+[`--experimental-test-module-mocks`][] command-line flag.
 
 The following example demonstrates how a mock is created for a module.
 

filename: lib/internal/test_runner/mock/loader.js
patch: @@ -118,10 +118,17 @@ async function load(url, context, nextLoad) {
   // Treat builtins as commonjs because customization hooks do not allow a
   // core module to be replaced.
   // Also collapse 'commonjs-sync' and 'require-commonjs' to 'commonjs'.
-  const format = (
-    original.format === 'builtin' ||
-    original.format === 'commonjs-sync' ||
-    original.format === 'require-commonjs') ? 'commonjs' : original.format;
+  let format = original.format;
+  switch (original.format) {
+    case 'builtin': // Deliberate fallthrough
+    case 'commonjs-sync': // Deliberate fallthrough
+    case 'require-commonjs':
+      format = 'commonjs';
+      break;
+    case 'json':
+      format = 'module';
+      break;
+  }
 
   const result = {
     __proto__: null,

filename: lib/internal/test_runner/mock/mock.js
patch: @@ -70,7 +70,14 @@ const kMockUnknownMessage = 3;
 const kWaitTimeout = 5_000;
 const kBadExportsMessage = 'Cannot create mock because named exports ' +
   'cannot be applied to the provided default export.';
-const kSupportedFormats = ['builtin', 'commonjs', 'module', 'module-typescript', 'commonjs-typescript'];
+const kSupportedFormats = [
+  'builtin',
+  'commonjs-typescript',
+  'commonjs',
+  'json',
+  'module-typescript',
+  'module',
+];
 let sharedModuleState;
 
 class MockFunctionContext {

filename: test/fixtures/module-mocking/basic.json
patch: @@ -0,0 +1 @@
+{"foo":"bar"}

filename: test/parallel/test-runner-module-mocking.js
patch: @@ -365,6 +365,34 @@ test('ESM mocking with namedExports option', async (t) => {
   });
 });
 
+test('JSON mocking', async (t) => {
+  await t.test('with defaultExport', async (t) => {
+    const fixturePath = fixtures.path('module-mocking', 'basic.json');
+    const fixture = pathToFileURL(fixturePath);
+    const { default: original } = await import(fixture, { with: { type: 'json' } });
+
+    assert.deepStrictEqual(original, { foo: 'bar' });
+
+    const defaultExport = { qux: 'zed' };
+
+    t.mock.module(fixture, { defaultExport });
+
+    const { default: mocked } = await import(fixture, { with: { type: 'json' } });
+
+    assert.deepStrictEqual(mocked, defaultExport);
+  });
+
+  await t.test('throws without appropriate import attributes', async (t) => {
+    const fixturePath = fixtures.path('module-mocking', 'basic.json');
+    const fixture = pathToFileURL(fixturePath);
+
+    const defaultExport = { qux: 'zed' };
+    t.mock.module(fixture, { defaultExport });
+
+    await assert.rejects(() => import(fixture), /import attribute/);
+  });
+});
+
 test('modules cannot be mocked multiple times at once', async (t) => {
   await t.test('CJS', async (t) => {
     const fixture = fixtures.path('module-mocking', 'basic-cjs.js');