Merge pull request #31933 from backstage/upgrade-jsdom
chore: upgrade jsdom to v27 and Jest to v30
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user