diff --git a/CHANGELOG.md b/CHANGELOG.md index 564e7585c84f..d1192e691629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,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)) - `[jest-test-sequencer]` Fix issue where failed tests due to compilation errors not getting re-executed even with `--onlyFailures` CLI option ([#15851](https://github.com/jestjs/jest/pull/15851)) ### Chore & Maintenance diff --git a/e2e/__tests__/customReporters.test.ts b/e2e/__tests__/customReporters.test.ts index bdd4b6c2063f..3f8db443d1c9 100644 --- a/e2e/__tests__/customReporters.test.ts +++ b/e2e/__tests__/customReporters.test.ts @@ -179,4 +179,36 @@ 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', () => {});", + 'fakeHome/.node_libraries/@org/custom-reporter/index.js': ` + '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'), + // 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/__tests__/normalize.test.ts b/packages/jest-config/src/__tests__/normalize.test.ts index 3939302c890a..8462a5d3d432 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 type {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; + const 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', () => { diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index e2ed840cf020..936f84374d53 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -26,6 +26,7 @@ import { clearLine, replacePathSepForGlob, requireOrImportModule, + specialChars, tryRealpath, } from 'jest-util'; import {ValidationError, validate} from 'jest-validate'; @@ -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( 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 = {