chore: make jest a peer dependency with v29/v30 support
Move jest from dependencies to peer dependencies, allowing users to
choose between Jest 29 and Jest 30.
The CLI now detects the Jest version at runtime and uses the
appropriate environment:
- Jest 29: Uses standard jest-environment-jsdom
- Jest 30: Uses a custom environment based on @jest/environment-jsdom-abstract
with fixes for Web API globals (fetch, streams, Error, etc.)
The cross-fetch polyfill is only injected for Jest 29, as with Jest 30+
our patched Jest environment is used. The network request blocker is made
MSW-compatible by checking if fetch was wrapped before blocking.
Jest 30 (with jsdom v27) fixes `Could not parse CSS stylesheet`
warnings/errors when testing components from @backstage/ui or other
packages using CSS `@layer` declarations.
New peer dependencies (install based on your Jest version):
- jest (required, ^29 or ^30)
- Jest 29 requires: jest-environment-jsdom
- Jest 30 requires: @jest/environment-jsdom-abstract, jsdom
Production code changes for jsdom 27 testability:
- AppIdentityProxy: extract navigateToUrl method for spying
- LiveReloadAddon: export utils.reloadPage for spying
- collect.ts: export internal.resolvePackagePath for mocking
MockFetchApi: evaluate global.fetch at call time instead of construction
time, allowing MSW to patch fetch after MockFetchApi is constructed.
Test adaptations for jsdom 27:
- Use RGB values instead of named colors in CSS assertions
- Update error format expectations (hyphenated type names, SyntaxError
instead of FetchError for JSON parse errors)
- Simplify URL error assertions for cross-version compatibility
- Fix accessible name whitespace handling for external links
- Use history.replaceState for location mocking (non-configurable)
- Use fireEvent.blur for contentEditable elements
- Move async assertions inside waitFor for race conditions
- Remove Blob.prototype.text polyfill (now native)
- Remove test case using credentials in plugin:// URLs
Test adaptations for Jest 30:
- Replace `expect.objectContaining([...])` with direct array equality
- Replace `expect.objectContaining({ length: N })` with
`expect.any(Array)` + separate `toHaveLength()` assertions
- Use child process for native Node.js module resolution in
collect.test.ts to work around Jest 30's resolver behavior
- Update snapshot headers for new Jest format
Also removes the jest-haste-map patch which is no longer needed.
Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user