diff --git a/CHANGELOG.md b/CHANGELOG.md index f69c2ac2754a..8ac5f03a5a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Fixes - `[expect]` Fix `bigint` error ([#15702](https://github.com/jestjs/jest/pull/15702)) +- `[pretty-format]` Fix support for React 19 ([#15729](https://github.com/jestjs/jest/pull/15729)) ## 30.0.4 diff --git a/packages/pretty-format/package.json b/packages/pretty-format/package.json index ad6a15a9d39c..1b612394f8e6 100644 --- a/packages/pretty-format/package.json +++ b/packages/pretty-format/package.json @@ -22,12 +22,10 @@ "author": "James Kyle ", "dependencies": { "@jest/schemas": "workspace:*", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "ansi-styles": "^5.2.0" }, "devDependencies": { "@types/react": "^18.3.23", - "@types/react-is": "^18.3.1", "@types/react-test-renderer": "^18.3.1", "immutable": "^5.1.2", "jest-util": "workspace:*", diff --git a/packages/pretty-format/src/plugins/ReactElement.ts b/packages/pretty-format/src/plugins/ReactElement.ts index 808ee12114d6..d797ff740902 100644 --- a/packages/pretty-format/src/plugins/ReactElement.ts +++ b/packages/pretty-format/src/plugins/ReactElement.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import * as ReactIs from 'react-is'; import type {Config, NewPlugin, Printer, Refs} from '../types'; +import * as ReactIs from './lib/ReactIs'; import { printChildren, printElement, diff --git a/packages/pretty-format/src/plugins/lib/ReactIs.ts b/packages/pretty-format/src/plugins/lib/ReactIs.ts new file mode 100644 index 000000000000..f41ce17480d3 --- /dev/null +++ b/packages/pretty-format/src/plugins/lib/ReactIs.ts @@ -0,0 +1,170 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +// This file is mostly copy-pasted from the original react-is library, +// with some modifications to support React versions < 19. +// https://github.com/facebook/react/blob/main/packages/react-is/src/ReactIs.js + +// Customizations: +// - Added support for REACT_LEGACY_ELEMENT_TYPE wherever REACT_ELEMENT_TYPE is used. +// - Added support for REACT_PROVIDER_TYPE wherever REACT_CONTEXT_TYPE is used. +// - Added support for REACT_SERVER_CONTEXT_TYPE in typeOf function with the same logic as REACT_CONTEXT_TYPE. +// - Added support for REACT_OFFSCREEN_TYPE in isValidElementType function. +// - TypeScript type `mixed` replaced with `any`. + +// Types collected from https://github.com/facebook/react/blob/main/packages/shared/ReactSymbols.js +const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element'); +const REACT_ELEMENT_TYPE: symbol = Symbol.for('react.transitional.element'); +const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal'); +const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment'); +const REACT_STRICT_MODE_TYPE: symbol = Symbol.for('react.strict_mode'); +const REACT_PROFILER_TYPE: symbol = Symbol.for('react.profiler'); +const REACT_CONSUMER_TYPE: symbol = Symbol.for('react.consumer'); +const REACT_CONTEXT_TYPE: symbol = Symbol.for('react.context'); +const REACT_FORWARD_REF_TYPE: symbol = Symbol.for('react.forward_ref'); +const REACT_SUSPENSE_TYPE: symbol = Symbol.for('react.suspense'); +const REACT_SUSPENSE_LIST_TYPE: symbol = Symbol.for('react.suspense_list'); +const REACT_MEMO_TYPE: symbol = Symbol.for('react.memo'); +const REACT_LAZY_TYPE: symbol = Symbol.for('react.lazy'); +const REACT_VIEW_TRANSITION_TYPE: symbol = Symbol.for('react.view_transition'); +const REACT_CLIENT_REFERENCE: symbol = Symbol.for('react.client.reference'); + +// Legacy types not present in React 19+ +const REACT_PROVIDER_TYPE: symbol = Symbol.for('react.provider'); +const REACT_SERVER_CONTEXT_TYPE: symbol = Symbol.for('react.server_context'); +const REACT_OFFSCREEN_TYPE: symbol = Symbol.for('react.offscreen'); + +// Below is a copy of ReactIs.js with customizations listed above. + +export function typeOf(object: any): any { + if (typeof object === 'object' && object !== null) { + const $$typeof = object.$$typeof; + switch ($$typeof) { + case REACT_LEGACY_ELEMENT_TYPE: + case REACT_ELEMENT_TYPE: + const type = object.type; + + switch (type) { + case REACT_FRAGMENT_TYPE: + case REACT_PROFILER_TYPE: + case REACT_STRICT_MODE_TYPE: + case REACT_SUSPENSE_TYPE: + case REACT_SUSPENSE_LIST_TYPE: + case REACT_VIEW_TRANSITION_TYPE: + return type; + default: + const $$typeofType = type && type.$$typeof; + + switch ($$typeofType) { + case REACT_PROVIDER_TYPE: + case REACT_CONTEXT_TYPE: + case REACT_SERVER_CONTEXT_TYPE: + case REACT_FORWARD_REF_TYPE: + case REACT_LAZY_TYPE: + case REACT_MEMO_TYPE: + return $$typeofType; + case REACT_CONSUMER_TYPE: + return $$typeofType; + // Fall through + default: + return $$typeof; + } + } + case REACT_PORTAL_TYPE: + return $$typeof; + } + } + + return undefined; +} + +export const ContextConsumer: symbol = REACT_CONSUMER_TYPE; +export const ContextProvider: symbol = REACT_CONTEXT_TYPE; +export const Element = REACT_ELEMENT_TYPE; +export const ForwardRef = REACT_FORWARD_REF_TYPE; +export const Fragment = REACT_FRAGMENT_TYPE; +export const Lazy = REACT_LAZY_TYPE; +export const Memo = REACT_MEMO_TYPE; +export const Portal = REACT_PORTAL_TYPE; +export const Profiler = REACT_PROFILER_TYPE; +export const StrictMode = REACT_STRICT_MODE_TYPE; +export const Suspense = REACT_SUSPENSE_TYPE; +export const SuspenseList = REACT_SUSPENSE_LIST_TYPE; + +export function isValidElementType(type: any): boolean { + if (typeof type === 'string' || typeof type === 'function') { + return true; + } + + // Note: typeof might be other than 'symbol' or 'number' (e.g. if it's a polyfill). + if ( + type === REACT_FRAGMENT_TYPE || + type === REACT_PROFILER_TYPE || + type === REACT_STRICT_MODE_TYPE || + type === REACT_SUSPENSE_TYPE || + type === REACT_SUSPENSE_LIST_TYPE || + type === REACT_OFFSCREEN_TYPE + ) { + return true; + } + + if (typeof type === 'object' && type !== null) { + if ( + type.$$typeof === REACT_LAZY_TYPE || + type.$$typeof === REACT_MEMO_TYPE || + type.$$typeof === REACT_PROVIDER_TYPE || + type.$$typeof === REACT_CONTEXT_TYPE || + type.$$typeof === REACT_CONSUMER_TYPE || + type.$$typeof === REACT_FORWARD_REF_TYPE || + // This needs to include all possible module reference object + // types supported by any Flight configuration anywhere since + // we don't know which Flight build this will end up being used + // with. + type.$$typeof === REACT_CLIENT_REFERENCE || + type.getModuleId !== undefined + ) { + return true; + } + } + + return false; +} + +export function isContextConsumer(object: any): boolean { + return typeOf(object) === REACT_CONSUMER_TYPE; +} +export function isContextProvider(object: any): boolean { + return [REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE].includes(typeOf(object)); +} +export function isElement(object: any): boolean { + return ( + typeof object === 'object' && + object !== null && + [REACT_ELEMENT_TYPE, REACT_LEGACY_ELEMENT_TYPE].includes(object.$$typeof) + ); +} +export function isForwardRef(object: any): boolean { + return typeOf(object) === REACT_FORWARD_REF_TYPE; +} +export function isFragment(object: any): boolean { + return typeOf(object) === REACT_FRAGMENT_TYPE; +} +export function isLazy(object: any): boolean { + return typeOf(object) === REACT_LAZY_TYPE; +} +export function isMemo(object: any): boolean { + return typeOf(object) === REACT_MEMO_TYPE; +} +export function isPortal(object: any): boolean { + return typeOf(object) === REACT_PORTAL_TYPE; +} +export function isProfiler(object: any): boolean { + return typeOf(object) === REACT_PROFILER_TYPE; +} +export function isStrictMode(object: any): boolean { + return typeOf(object) === REACT_STRICT_MODE_TYPE; +} +export function isSuspense(object: any): boolean { + return typeOf(object) === REACT_SUSPENSE_TYPE; +} +export function isSuspenseList(object: any): boolean { + return typeOf(object) === REACT_SUSPENSE_LIST_TYPE; +} diff --git a/yarn.lock b/yarn.lock index 5b0b6c123f91..ffaea3cdb43a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6390,15 +6390,6 @@ __metadata: languageName: node linkType: hard -"@types/react-is@npm:^18.3.1": - version: 18.3.1 - resolution: "@types/react-is@npm:18.3.1" - dependencies: - "@types/react": "npm:^18" - checksum: 10/ccb79d6e196a5232cde8ccb255ec97e062801a3dafeff3816130fb5ad6b9a87f7c0806ab35bc00890a229773228ef217d0390839b68c705d3add2f798b5fcf82 - languageName: node - linkType: hard - "@types/react-router-config@npm:*, @types/react-router-config@npm:^5.0.7": version: 5.0.11 resolution: "@types/react-router-config@npm:5.0.11" @@ -18234,14 +18225,12 @@ __metadata: dependencies: "@jest/schemas": "workspace:*" "@types/react": "npm:^18.3.23" - "@types/react-is": "npm:^18.3.1" "@types/react-test-renderer": "npm:^18.3.1" ansi-styles: "npm:^5.2.0" immutable: "npm:^5.1.2" jest-util: "workspace:*" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-is: "npm:^18.3.1" react-test-renderer: "npm:18.3.1" languageName: unknown linkType: soft