From 05b1a7c8e28d2659acbd7da6639dc0a5ac5ca79c Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 5 Oct 2025 12:06:26 +0700 Subject: [PATCH 1/6] fix: support custom reporters in legacy module path `$HOME/.node_libraries` Signed-off-by: hainenber --- e2e/__tests__/customReporters.test.ts | 24 ++++++++++++++++++++++++ packages/jest-config/src/normalize.ts | 18 +++++++++++++++--- packages/jest-util/src/specialChars.ts | 2 +- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/e2e/__tests__/customReporters.test.ts b/e2e/__tests__/customReporters.test.ts index bdd4b6c2063f..b5ec90970de3 100644 --- a/e2e/__tests__/customReporters.test.ts +++ b/e2e/__tests__/customReporters.test.ts @@ -179,4 +179,28 @@ describe('Custom Reporters Integration', () => { expect(stderr).toMatch(/ON_RUN_START_ERROR/); expect(exitCode).toBe(1); }); + + test('supports custom reporter stored in legacy module path, i.e. $HOME/.node_libraries', () => { + writeFiles(DIR, { + '__tests__/test.test.js': "test('test', () => {});", + 'package.json': JSON.stringify({ + jest: { + reporters: ['@org/custom-reporter'], + }, + }), + 'fakeHome/.node_libraries/@org/custom-reporter/index.js': ` + export default class Reporter { + onRunStart() { + throw new Error('ON_RUN_START_ERROR'); + } + }; + `, + }); + + const {stderr, exitCode} = runJest(DIR, undefined, { + env: {HOME: path.resolve(DIR, 'fakeHome')}, + }); + expect(stderr).toMatch(/ON_RUN_START_ERROR/); + expect(exitCode).toBe(1); + }); }); diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index e2ed840cf020..e9d7c8568db2 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -27,6 +27,7 @@ import { replacePathSepForGlob, requireOrImportModule, tryRealpath, + specialChars, } from 'jest-util'; import {ValidationError, validate} from 'jest-validate'; import DEFAULT_CONFIG from './Defaults'; @@ -169,7 +170,9 @@ const setupPreset = async ( ); } throw createConfigError( - ` Preset ${chalk.bold(presetPath)} not found relative to rootDir ${chalk.bold(options.rootDir)}.`, + ` Preset ${chalk.bold( + presetPath, + )} not found relative to rootDir ${chalk.bold(options.rootDir)}.`, ); } throw createConfigError( @@ -378,8 +381,17 @@ const normalizeReporters = ({ ); if (!['default', 'github-actions', 'summary'].includes(reporterPath)) { + // There's a chance that users store custom reporter in legacy module paths + // such as $HOME/.node_libraries + const homeDir = specialChars.isWindows + ? process.env.USERPROFILE + : process.env.HOME; + const legacyModulePaths = homeDir + ? [path.resolve(homeDir, '.node_libraries')] + : undefined; const reporter = Resolver.findNodeModule(reporterPath, { basedir: rootDir, + paths: legacyModulePaths, }); if (!reporter) { throw new Resolver.ModuleNotFoundError( @@ -1087,8 +1099,8 @@ export default async function normalize( newOptions.ci && !argv.updateSnapshot ? 'none' : argv.updateSnapshot - ? 'all' - : 'new'; + ? 'all' + : 'new'; newOptions.maxConcurrency = Number.parseInt( newOptions.maxConcurrency as unknown as string, diff --git a/packages/jest-util/src/specialChars.ts b/packages/jest-util/src/specialChars.ts index e3dc1f561313..580777af26cb 100644 --- a/packages/jest-util/src/specialChars.ts +++ b/packages/jest-util/src/specialChars.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -const isWindows = process.platform === 'win32'; +export const isWindows = process.platform === 'win32'; export const ARROW = ' \u203A '; export const ICONS = { From f0c67cc8a01ffe85c6f5ec79356ba7a97c3b7c1c Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 5 Oct 2025 12:14:22 +0700 Subject: [PATCH 2/6] chore: add CHANGELOG entry Signed-off-by: hainenber --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 917db93ebb8c..1bc7bedde190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Fixes - `[jest-runtime]` Fix issue where user cannot utilize dynamic import despite specifying `--experimental-vm-modules` Node option ([#15842](https://github.com/jestjs/jest/pull/15842)) +- `[jest-config]` Fix issue where custom reporters not loaded from legacy global module folder, i.e. `$HOME/.node_libraries` ([#15852](https://github.com/jestjs/jest/pull/15852)) ## 30.2.0 From 1d82a5eda6d9562db8cd880baa3921d36472759b Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 5 Oct 2025 13:14:22 +0700 Subject: [PATCH 3/6] chore: use CJS reporter content to work with Node18 + fix lint issues Signed-off-by: hainenber --- e2e/__tests__/customReporters.test.ts | 22 +++++++++++++++------- packages/jest-config/src/normalize.ts | 6 +++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/e2e/__tests__/customReporters.test.ts b/e2e/__tests__/customReporters.test.ts index b5ec90970de3..22082da35b19 100644 --- a/e2e/__tests__/customReporters.test.ts +++ b/e2e/__tests__/customReporters.test.ts @@ -183,22 +183,30 @@ describe('Custom Reporters Integration', () => { test('supports custom reporter stored in legacy module path, i.e. $HOME/.node_libraries', () => { writeFiles(DIR, { '__tests__/test.test.js': "test('test', () => {});", - 'package.json': JSON.stringify({ - jest: { - reporters: ['@org/custom-reporter'], - }, - }), 'fakeHome/.node_libraries/@org/custom-reporter/index.js': ` - export default class Reporter { + 'use strict'; + module.exports = class Reporter { onRunStart() { throw new Error('ON_RUN_START_ERROR'); } }; + 'package.json': JSON.stringify({ + jest: { + reporters: ['@org/custom-reporter'], + }, + }), `, }); const {stderr, exitCode} = runJest(DIR, undefined, { - env: {HOME: path.resolve(DIR, 'fakeHome')}, + env: { + HOME: path.resolve(DIR, 'fakeHome'), + // For Windows testing + USERPROFILE: + process.platform === 'win32' + ? path.resolve(DIR, 'fakeHome') + : undefined, + }, }); expect(stderr).toMatch(/ON_RUN_START_ERROR/); expect(exitCode).toBe(1); diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index e9d7c8568db2..936f84374d53 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -26,8 +26,8 @@ import { clearLine, replacePathSepForGlob, requireOrImportModule, - tryRealpath, specialChars, + tryRealpath, } from 'jest-util'; import {ValidationError, validate} from 'jest-validate'; import DEFAULT_CONFIG from './Defaults'; @@ -1099,8 +1099,8 @@ export default async function normalize( newOptions.ci && !argv.updateSnapshot ? 'none' : argv.updateSnapshot - ? 'all' - : 'new'; + ? 'all' + : 'new'; newOptions.maxConcurrency = Number.parseInt( newOptions.maxConcurrency as unknown as string, From 12fedd602ec7a3739b2bd45c5d5424bf2a19052c Mon Sep 17 00:00:00 2001 From: hainenber Date: Sun, 5 Oct 2025 13:17:44 +0700 Subject: [PATCH 4/6] chore: fix accidentally assimilated package.json Signed-off-by: hainenber --- e2e/__tests__/customReporters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/customReporters.test.ts b/e2e/__tests__/customReporters.test.ts index 22082da35b19..3f8db443d1c9 100644 --- a/e2e/__tests__/customReporters.test.ts +++ b/e2e/__tests__/customReporters.test.ts @@ -190,12 +190,12 @@ describe('Custom Reporters Integration', () => { throw new Error('ON_RUN_START_ERROR'); } }; + `, 'package.json': JSON.stringify({ jest: { reporters: ['@org/custom-reporter'], }, }), - `, }); const {stderr, exitCode} = runJest(DIR, undefined, { From 720610faf26b500a0e3d90241a38ab0281befb69 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 18 Oct 2025 16:04:52 +0700 Subject: [PATCH 5/6] chore: add test suite to reach required code coverage Signed-off-by: hainenber --- .../src/__tests__/normalize.test.ts | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/jest-config/src/__tests__/normalize.test.ts b/packages/jest-config/src/__tests__/normalize.test.ts index 3939302c890a..d3646b0c498b 100644 --- a/packages/jest-config/src/__tests__/normalize.test.ts +++ b/packages/jest-config/src/__tests__/normalize.test.ts @@ -14,6 +14,7 @@ import {escapeStrForRegex} from 'jest-regex-util'; import Defaults from '../Defaults'; import {DEFAULT_JS_PATTERN} from '../constants'; import normalize, {type AllOptions} from '../normalize'; +import {FindNodeModuleConfig} from 'jest-resolve'; const DEFAULT_CSS_PATTERN = '\\.(css)$'; @@ -83,7 +84,16 @@ beforeEach(() => { jest.spyOn(console, 'warn'); }); +const oldUserProfile = process.env.USERPROFILE; +const originalPlatform = process.platform; + afterEach(() => { + // Restore the original process.* value after each test. + process.env.USERPROFILE = oldUserProfile; + Object.defineProperty(process, 'platform', { + value: originalPlatform, + }); + jest.mocked(console.warn).mockRestore(); }); @@ -293,10 +303,14 @@ describe('roots', () => { describe('reporters', () => { let Resolver: typeof import('jest-resolve').default; + let spiedFindNodeModuleFunction = jest.fn( + (name: string, _: FindNodeModuleConfig) => name, + ); + beforeEach(() => { Resolver = (require('jest-resolve') as typeof import('jest-resolve')) .default; - Resolver.findNodeModule = jest.fn((name: string) => name); + Resolver.findNodeModule = spiedFindNodeModuleFunction; }); it('allows empty list', async () => { @@ -390,6 +404,29 @@ describe('reporters', () => { ), ).rejects.toThrowErrorMatchingSnapshot(); }); + + it('normalizes the path and options object for custom reporter on Windows', async () => { + // Simulate running in Windows env with USERPROFILE env var undefined. + process.env.USERPROFILE = undefined; + Object.defineProperty(process, 'platform', { + value: 'win32', + }); + + const {options} = await normalize( + { + reporters: [ + ['/custom-reporter.js', {banana: 'yes', pineapple: 'no'}], + ], + rootDir: '/root/', + }, + {} as Config.Argv, + ); + + expect(options.reporters).toEqual([ + ['/root/custom-reporter.js', {banana: 'yes', pineapple: 'no'}], + ]); + expect(spiedFindNodeModuleFunction.mock.calls[0][1].paths).toBeUndefined(); + }); }); describe('transform', () => { From 1c7167187c1743dd1ddc410b69fd041a89e309c5 Mon Sep 17 00:00:00 2001 From: hainenber Date: Sat, 18 Oct 2025 16:29:15 +0700 Subject: [PATCH 6/6] chore: fix lint issues Signed-off-by: hainenber --- packages/jest-config/src/__tests__/normalize.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-config/src/__tests__/normalize.test.ts b/packages/jest-config/src/__tests__/normalize.test.ts index d3646b0c498b..8462a5d3d432 100644 --- a/packages/jest-config/src/__tests__/normalize.test.ts +++ b/packages/jest-config/src/__tests__/normalize.test.ts @@ -14,7 +14,7 @@ import {escapeStrForRegex} from 'jest-regex-util'; import Defaults from '../Defaults'; import {DEFAULT_JS_PATTERN} from '../constants'; import normalize, {type AllOptions} from '../normalize'; -import {FindNodeModuleConfig} from 'jest-resolve'; +import type {FindNodeModuleConfig} from 'jest-resolve'; const DEFAULT_CSS_PATTERN = '\\.(css)$'; @@ -303,7 +303,7 @@ describe('roots', () => { describe('reporters', () => { let Resolver: typeof import('jest-resolve').default; - let spiedFindNodeModuleFunction = jest.fn( + const spiedFindNodeModuleFunction = jest.fn( (name: string, _: FindNodeModuleConfig) => name, );