Merge pull request #31933 from backstage/upgrade-jsdom

chore: upgrade jsdom to v27 and Jest to v30
This commit is contained in:
Patrik Oldsberg
2025-12-15 16:01:01 +01:00
committed by GitHub
57 changed files with 1988 additions and 1452 deletions
+4 -5
View File
@@ -324,7 +324,7 @@ Options:
### `backstage-cli package test`
```
Usage: backstage-cli [--config=<pathToConfigFile>] [TestPathPattern]
Usage: backstage-cli [--config=<pathToConfigFile>] [TestPathPatterns]
Options:
--all
@@ -349,7 +349,6 @@ Options:
--debug
--detectLeaks
--detectOpenHandles
--env
--errorOnDeprecated
--filter
--findRelatedTests
@@ -359,7 +358,6 @@ Options:
--globals
--haste
--ignoreProjects
--init
--injectGlobals
--json
--lastCommit
@@ -400,13 +398,13 @@ Options:
--silent
--skipFilter
--snapshotSerializers
--testEnvironment
--testEnvironment, --env
--testEnvironmentOptions
--testFailureExitCode
--testLocationInResults
--testMatch
--testPathIgnorePatterns
--testPathPattern
--testPathPatterns
--testRegex
--testResultsProcessor
--testRunner
@@ -418,6 +416,7 @@ Options:
--useStderr
--verbose
--version
--waitForUnhandledRejections
--watch
--watchAll
--watchPathIgnorePatterns
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function getJestMajorVersion() {
const jestVersion = require('jest/package.json').version;
const majorVersion = parseInt(jestVersion.split('.')[0], 10);
return majorVersion;
}
function getJestEnvironment() {
const majorVersion = getJestMajorVersion();
if (majorVersion >= 30) {
try {
require.resolve('@jest/environment-jsdom-abstract');
require.resolve('jsdom');
} catch {
throw new Error(
'Jest 30+ requires @jest/environment-jsdom-abstract and jsdom. ' +
'Please install them as dev dependencies.',
);
}
return require.resolve('./jest-environment-jsdom');
}
try {
require.resolve('jest-environment-jsdom');
} catch {
throw new Error(
'Jest 29 requires jest-environment-jsdom. ' +
'Please install it as a dev dependency.',
);
}
return require.resolve('jest-environment-jsdom');
}
module.exports = { getJestMajorVersion, getJestEnvironment };
@@ -0,0 +1,61 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const JSDOMEnvironment = require('@jest/environment-jsdom-abstract').default;
const jsdom = require('jsdom');
/**
* A custom JSDOM environment that extends the abstract base and applies
* fixes for Web API globals that are missing or incorrectly implemented
* in JSDOM.
*
* Based on https://github.com/mswjs/jest-fixed-jsdom
*/
class FixedJSDOMEnvironment extends JSDOMEnvironment {
constructor(config, context) {
super(config, context, jsdom);
// Fix Web API globals that JSDOM doesn't properly expose
this.global.TextDecoder = TextDecoder;
this.global.TextEncoder = TextEncoder;
this.global.TextDecoderStream = TextDecoderStream;
this.global.TextEncoderStream = TextEncoderStream;
this.global.ReadableStream = ReadableStream;
this.global.Blob = Blob;
this.global.Headers = Headers;
this.global.FormData = FormData;
this.global.Request = Request;
this.global.Response = Response;
this.global.fetch = fetch;
this.global.AbortController = AbortController;
this.global.AbortSignal = AbortSignal;
this.global.structuredClone = structuredClone;
this.global.URL = URL;
this.global.URLSearchParams = URLSearchParams;
this.global.BroadcastChannel = BroadcastChannel;
this.global.TransformStream = TransformStream;
this.global.WritableStream = WritableStream;
// Needed to ensure `e instanceof Error` works as expected with errors thrown from
// any of the native APIs above. Without this, the JSDOM `Error` is what the test
// code will use for comparison with `e`, which fails the instanceof check.
this.global.Error = Error;
}
}
module.exports = FixedJSDOMEnvironment;
+9 -2
View File
@@ -20,6 +20,10 @@ const crypto = require('crypto');
const glob = require('util').promisify(require('glob'));
const { version } = require('../package.json');
const paths = require('@backstage/cli-common').findPaths(process.cwd());
const {
getJestEnvironment,
getJestMajorVersion,
} = require('./getJestEnvironment');
const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs'];
@@ -211,7 +215,7 @@ function getRoleConfig(role, pkgJson) {
};
if (FRONTEND_ROLES.includes(role)) {
return {
testEnvironment: require.resolve('jest-environment-jsdom'),
testEnvironment: getJestEnvironment(),
// The caching module loader is only used to speed up frontend tests,
// as it breaks real dynamic imports of ESM modules.
runtime: envOptions.oldTests
@@ -279,7 +283,10 @@ async function getProjectConfig(targetPath, extraConfig, extraOptions) {
);
}
if (options.testEnvironment === require.resolve('jest-environment-jsdom')) {
if (
options.testEnvironment === getJestEnvironment() &&
getJestMajorVersion() < 30 // Only needed when not running the custom env for Jest 30+
) {
// FIXME https://github.com/jsdom/jsdom/issues/1724
options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill'));
}
@@ -14,6 +14,8 @@
* limitations under the License.
*/
// 'jest-runtime' is included with jest and should be kept in sync with the installed jest version
// eslint-disable-next-line @backstage/no-undeclared-imports
const { default: JestRuntime } = require('jest-runtime');
module.exports = class CachingJestRuntime extends JestRuntime {
@@ -22,7 +22,7 @@ const errorMessage = 'Network requests are not allowed in tests';
const origHttpAgent = http.globalAgent;
const origHttpsAgent = https.globalAgent;
const origFetch = global.fetch;
const origXMLHttpRequest = global.fetch;
const origXMLHttpRequest = global.XMLHttpRequest;
http.globalAgent = new http.Agent({
lookup() {
@@ -36,10 +36,21 @@ https.globalAgent = new https.Agent({
},
});
const BLOCKING_FETCH_SYMBOL = Symbol.for(
'backstage.jestRejectNetworkRequests.blockingFetch',
);
if (global.fetch) {
global.fetch = async () => {
throw new Error(errorMessage);
const blockingFetch = async (input, init) => {
// If global.fetch still has our marker, block the request
if (global.fetch[BLOCKING_FETCH_SYMBOL]) {
throw new Error(errorMessage);
}
// MSW (or something else) wrapped us - pass through
return origFetch(input, init);
};
blockingFetch[BLOCKING_FETCH_SYMBOL] = true;
global.fetch = blockingFetch;
}
if (global.XMLHttpRequest) {
+17 -5
View File
@@ -73,7 +73,6 @@
"@swc/core": "^1.3.46",
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.22",
"@types/jest": "^29.5.11",
"@types/webpack-env": "^1.15.2",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.16.0",
@@ -109,11 +108,7 @@
"handlebars": "^4.7.3",
"html-webpack-plugin": "^5.6.3",
"inquirer": "^8.2.0",
"jest": "^29.7.0",
"jest-cli": "^29.7.0",
"jest-css-modules": "^2.1.0",
"jest-environment-jsdom": "^29.0.2",
"jest-runtime": "^29.0.2",
"json-schema": "^0.4.0",
"lodash": "^4.17.21",
"minimatch": "^9.0.0",
@@ -167,6 +162,7 @@
"@backstage/plugin-scaffolder-node-test-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@backstage/theme": "workspace:^",
"@jest/environment-jsdom-abstract": "^30.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@types/cross-spawn": "^6.0.2",
"@types/ejs": "^3.1.3",
@@ -174,6 +170,7 @@
"@types/fs-extra": "^11.0.0",
"@types/http-proxy": "^1.17.4",
"@types/inquirer": "^8.1.3",
"@types/jest": "^30.0.0",
"@types/node": "^22.13.14",
"@types/npm-packlist": "^3.0.0",
"@types/recursive-readdir": "^2.2.0",
@@ -188,6 +185,8 @@
"esbuild-loader": "^4.0.0",
"eslint-webpack-plugin": "^4.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.0",
"jest": "^30.2.0",
"jsdom": "^27.1.0",
"mini-css-extract-plugin": "^2.4.2",
"msw": "^1.0.0",
"nodemon": "^3.0.1",
@@ -196,17 +195,24 @@
"webpack-dev-server": "^5.0.0"
},
"peerDependencies": {
"@jest/environment-jsdom-abstract": "^30.0.0",
"@module-federation/enhanced": "^0.9.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"esbuild-loader": "^4.0.0",
"eslint-webpack-plugin": "^4.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.0",
"jest": "^29.0.0 || ^30.0.0",
"jest-environment-jsdom": "*",
"jsdom": "^27.1.0",
"mini-css-extract-plugin": "^2.4.2",
"terser-webpack-plugin": "^5.1.3",
"webpack": "~5.103.0",
"webpack-dev-server": "^5.0.0"
},
"peerDependenciesMeta": {
"@jest/environment-jsdom-abstract": {
"optional": true
},
"@module-federation/enhanced": {
"optional": true
},
@@ -222,6 +228,12 @@
"fork-ts-checker-webpack-plugin": {
"optional": true
},
"jest-environment-jsdom": {
"optional": true
},
"jsdom": {
"optional": true
},
"mini-css-extract-plugin": {
"optional": true
},
@@ -91,5 +91,23 @@ export default async (_opts: OptionValues, cmd: Command) => {
(process.stdout as any)._handle.setBlocking(true);
}
// Because of the ongoing migration to v30 of jest, jest is no longer hard-depended to allow
// opt-in migration. Users instead need to add jest as a devDependency themselves and specify
// the version they want. This prints a helpful error message if jest is not found, i.e. they
// forgot/didn't know they had to add jest themselves.
try {
require.resolve('jest');
} catch {
console.error(
[
'No Jest installation found in this project.',
'',
'To support opt-in migration to Jest v30, the Backstage CLI now expects Jest to be installed as a devDependency.',
'See the migration guide in the changelog for version options and their consequences.',
].join('\n'),
);
process.exit(1);
}
await require('jest').run(args);
};
@@ -17,6 +17,8 @@
import os from 'os';
import crypto from 'node:crypto';
import yargs from 'yargs';
// 'jest-cli' is included with jest and should be kept in sync with the installed jest version
// eslint-disable-next-line @backstage/no-undeclared-imports
import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli';
import { relative as relativePath } from 'path';
import { Command, OptionValues } from 'commander';