diff --git a/libs/payments/ui/src/lib/client/components/CouponForm/index.test.tsx b/libs/payments/ui/src/lib/client/components/CouponForm/index.test.tsx index e3d83803719..4e4b60ef2e9 100644 --- a/libs/payments/ui/src/lib/client/components/CouponForm/index.test.tsx +++ b/libs/payments/ui/src/lib/client/components/CouponForm/index.test.tsx @@ -6,16 +6,15 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; jest.mock('@radix-ui/react-form'); -jest.mock('react-dom', () => { - const React = jest.requireActual('react'); - const actual = jest.requireActual('react-dom'); +jest.mock('react', () => { + const actual = jest.requireActual('react'); return { ...actual, - useFormState: ( + useActionState: ( action: (state: S, payload: P) => Promise | S, initial: S ) => { - const [state, setState] = React.useState(initial); + const [state, setState] = actual.useState(initial); const formAction = async (payload: P) => { const result = await action(state, payload); setState(result); @@ -23,6 +22,13 @@ jest.mock('react-dom', () => { }; return [state, formAction]; }, + }; +}); + +jest.mock('react-dom', () => { + const actual = jest.requireActual('react-dom'); + return { + ...actual, useFormStatus: () => ({ pending: false, data: null, @@ -107,7 +113,9 @@ describe('CouponForm', () => { expect(screen.getByTestId('coupon-error')).toBeInTheDocument(); }); - expect(screen.getByText(/code you entered is invalid/i)).toBeInTheDocument(); + expect( + screen.getByText(/code you entered is invalid/i) + ).toBeInTheDocument(); expect(screen.getByTestId('coupon-input')).toBeInTheDocument(); expect(screen.queryByTestId('coupon-hascoupon')).not.toBeInTheDocument(); }); diff --git a/libs/payments/ui/src/lib/client/components/CouponForm/index.tsx b/libs/payments/ui/src/lib/client/components/CouponForm/index.tsx index a0a2e4b29ce..8db8ff59df9 100644 --- a/libs/payments/ui/src/lib/client/components/CouponForm/index.tsx +++ b/libs/payments/ui/src/lib/client/components/CouponForm/index.tsx @@ -8,8 +8,8 @@ import { Localized } from '@fluent/react'; import * as Form from '@radix-ui/react-form'; import classNames from 'classnames'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { forwardRef, useEffect, useState } from 'react'; -import { useFormState, useFormStatus } from 'react-dom'; +import { forwardRef, useActionState, useEffect, useState } from 'react'; +import { useFormStatus } from 'react-dom'; import { ButtonVariant } from '../BaseButton'; import { SubmitButton } from '../SubmitButton'; import { @@ -157,7 +157,7 @@ const WithoutCoupon = ({ return result; } const routeCoupon = useSearchParams().get('coupon') || undefined; - const [, formAction] = useFormState(applyCoupon, null); + const [, formAction] = useActionState(applyCoupon, null); useEffect(() => { if (routeCoupon) { const formData = new FormData(); diff --git a/package.json b/package.json index 3bbbe336c4f..a7bb8ce9e20 100644 --- a/package.json +++ b/package.json @@ -139,8 +139,8 @@ "path-browserify": "^1.0.1", "pm2": "^7.0.0", "prettier": "^3.5.3", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "19.1.0", + "react-dom": "19.1.0", "react-ga4": "^2.1.0", "rxjs": "^7.8.1", "semver": "^7.7.1", @@ -221,7 +221,7 @@ "@swc/cli": "0.6.0", "@swc/core": "1.11.11", "@swc/jest": "^0.2.36", - "@testing-library/react": "15.0.6", + "@testing-library/react": "^16.3.0", "@types/babel__core": "^7", "@types/babel__preset-env": "^7", "@types/bn.js": "^5", @@ -238,9 +238,8 @@ "@types/passport": "^1.0.6", "@types/passport-http-bearer": "^1.0.36", "@types/passport-jwt": "^4", - "@types/react": "18.3.1", - "@types/react-dom": "18.3.0", - "@types/react-test-renderer": "^18", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", "@types/set-value": "^4", "@types/superagent": "4.1.11", "@types/twilio": "^3.19.3", @@ -260,7 +259,7 @@ "eslint-plugin-jest": "^27.6.0", "eslint-plugin-jsx-a11y": "6.10.0", "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "5.0.0", + "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-testing-library": "^7.3.0", "grunt": "^1.6.1", "grunt-cli": "^1.4.3", @@ -280,7 +279,6 @@ "nx": "21.2.4", "nx-cloud": "19.1.0", "postcss": "8.5.12", - "react-test-renderer": "^18.3.1", "reflect-metadata": "^0.2.1", "server-only": "^0.0.1", "storybook": "^8.0.0", @@ -301,7 +299,6 @@ "resolutions": { "@react-pdf/layout": "3.9.1", "@react-pdf/textkit": "4.3.0", - "@types/react": "18.2.14", "asn1.js": ">=5.4.1", "axios": "^1.13.4", "bn.js": "^5.2.1", diff --git a/packages/fxa-admin-panel/package.json b/packages/fxa-admin-panel/package.json index ec27467f2db..0c0591547d4 100644 --- a/packages/fxa-admin-panel/package.json +++ b/packages/fxa-admin-panel/package.json @@ -47,6 +47,7 @@ "helmet": "^8.0.0", "mozlog": "^3.0.2", "on-headers": "^1.1.0", + "react-hook-form": "^7.54.0", "react-router": "7.18.0", "react-scripts": "^5.0.1", "serve-static": "^1.16.0" diff --git a/packages/fxa-admin-panel/src/components/PageRateLimiting/index.tsx b/packages/fxa-admin-panel/src/components/PageRateLimiting/index.tsx index 000c2970559..0cd19956db9 100644 --- a/packages/fxa-admin-panel/src/components/PageRateLimiting/index.tsx +++ b/packages/fxa-admin-panel/src/components/PageRateLimiting/index.tsx @@ -117,11 +117,10 @@ export const PageRateLimiting = () => { ); diff --git a/packages/fxa-admin-panel/src/components/TableXHeaders/index.tsx b/packages/fxa-admin-panel/src/components/TableXHeaders/index.tsx index 7b10dfb7529..4a5fce93bf5 100644 --- a/packages/fxa-admin-panel/src/components/TableXHeaders/index.tsx +++ b/packages/fxa-admin-panel/src/components/TableXHeaders/index.tsx @@ -33,7 +33,12 @@ export const TableRowXHeader = ({ children, testId }: TableRowXHeaderProps) => { return ( {arrayElements.map((element, i) => { - if (React.isValidElement(element) && element.type === 'td') { + if ( + React.isValidElement>( + element + ) && + element.type === 'td' + ) { const { className: elementClassNames, children: elementChildren, diff --git a/packages/fxa-react/configs/rescripts.js b/packages/fxa-react/configs/rescripts.js index c54317994fb..a48067fa6af 100644 --- a/packages/fxa-react/configs/rescripts.js +++ b/packages/fxa-react/configs/rescripts.js @@ -99,7 +99,10 @@ const configureDevServerCompression = (devServerConfig) => { setupMiddlewares: (middlewares, devServer) => { middlewares.unshift(compression({ threshold: '4kb' })); - return devServerConfig.setupMiddlewares?.(middlewares, devServer) || middlewares; + return ( + devServerConfig.setupMiddlewares?.(middlewares, devServer) || + middlewares + ); }, }; }; @@ -108,6 +111,13 @@ const setModuleNameMapper = (tsconfigBase) => (config) => { config.transform = { ...config.transform, '^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }], + // Override CRA's fileTransform for non-JS assets (SVGs, images). + // react-scripts' built-in transform hardcodes Symbol.for('react.element') + // which is incompatible with React 19's element format. + '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': resolve( + __dirname, + '../../fxa-settings/config/jest/fileTransform.js' + ), }; // ts-jest - Paths mapping - With helper diff --git a/packages/fxa-react/jest.config.js b/packages/fxa-react/jest.config.js index f8eae891bfa..bde470cb534 100644 --- a/packages/fxa-react/jest.config.js +++ b/packages/fxa-react/jest.config.js @@ -7,7 +7,7 @@ const { compilerOptions } = require('../../tsconfig.base.json'); module.exports = { transform: { '^.+\\.(ts|tsx)?$': ['ts-jest', { isolatedModules: true }], - '^.+\\.svg$': '/svg-transform.js', + '^.+\\.svg$': '/../fxa-settings/config/jest/fileTransform.js', }, // ts-jest - Paths mapping - With helper // https://kulshekhar.github.io/ts-jest/docs/getting-started/paths-mapping#jest-config-with-helper diff --git a/packages/fxa-react/lib/hooks.tsx b/packages/fxa-react/lib/hooks.tsx index 27795bff09a..3862e03bf47 100644 --- a/packages/fxa-react/lib/hooks.tsx +++ b/packages/fxa-react/lib/hooks.tsx @@ -132,7 +132,7 @@ export function useAwait< initialState || resetPromiseState() ); - const promise = useRef | undefined>(); + const promise = useRef | undefined>(undefined); const reset = useCallback(() => { setState(resetPromiseState()); diff --git a/packages/fxa-react/lib/test-utils/localizationProvider.tsx b/packages/fxa-react/lib/test-utils/localizationProvider.tsx index 91daac92974..e595bbfc444 100644 --- a/packages/fxa-react/lib/test-utils/localizationProvider.tsx +++ b/packages/fxa-react/lib/test-utils/localizationProvider.tsx @@ -9,7 +9,7 @@ import AppLocalizationProvider from 'fxa-react/lib/AppLocalizationProvider'; const reportError = () => {}; export function renderWithLocalizationProvider( - children: JSX.Element, + children: React.ReactElement, messages = { en: ['testo: lol'] } ): ReturnType { return render( @@ -20,7 +20,7 @@ export function renderWithLocalizationProvider( } export function withLocalizationProvider( - children: JSX.Element, + children: React.ReactElement, baseDir = '/locales', userLocales = navigator.languages || ['en'] ) { diff --git a/packages/fxa-react/svg-transform.js b/packages/fxa-react/svg-transform.js deleted file mode 100644 index 39f40f62a20..00000000000 --- a/packages/fxa-react/svg-transform.js +++ /dev/null @@ -1,36 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// This transformer was lifted directly from create-react-app and is -// needed so we can write components that import SVGs as ReactComponents -// https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/config/jest/fileTransform.js - -const path = require('path'); -const camelcase = require('camelcase'); - -module.exports = { - process(src, filename) { - const assetFilename = JSON.stringify(path.basename(filename)); - const pascalCaseFilename = camelcase(path.parse(filename).name, { - pascalCase: true, - }); - const componentName = `Svg${pascalCaseFilename}`; - return `const React = require('react'); - module.exports = { - __esModule: true, - default: ${assetFilename}, - ReactComponent: React.forwardRef(function ${componentName}(props, ref) { - return { - $$typeof: Symbol.for('react.element'), - type: 'svg', - ref: ref, - key: null, - props: Object.assign({}, props, { - children: ${assetFilename} - }) - }; - }), - };`; - }, -}; diff --git a/packages/fxa-settings/config/jest/fileTransform.js b/packages/fxa-settings/config/jest/fileTransform.js index c4734d7bdb0..03ba400b57e 100644 --- a/packages/fxa-settings/config/jest/fileTransform.js +++ b/packages/fxa-settings/config/jest/fileTransform.js @@ -1,13 +1,14 @@ -// This file was created by react-scripts' (create-react-app) eject script. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; +// Custom Jest transformer turning file imports into filenames. +// For SVG files, generates a React component matching SVGR's naming convention. +// http://facebook.github.io/jest/docs/en/webpack.html const path = require('path'); const camelcase = require('camelcase'); -// This is a custom Jest transformer turning file imports into filenames. -// http://facebook.github.io/jest/docs/en/webpack.html - module.exports = { process(src, filename) { const assetFilename = JSON.stringify(path.basename(filename)); @@ -25,15 +26,7 @@ module.exports = { __esModule: true, default: ${assetFilename}, ReactComponent: React.forwardRef(function ${componentName}(props, ref) { - return { - $$typeof: Symbol.for('react.element'), - type: 'svg', - ref: ref, - key: null, - props: Object.assign({}, props, { - children: ${assetFilename} - }) - }; + return React.createElement('svg', Object.assign({}, props, { ref: ref }), ${assetFilename}); }), };`, }; diff --git a/packages/fxa-settings/package.json b/packages/fxa-settings/package.json index dac616175a9..5fdba9f9d13 100644 --- a/packages/fxa-settings/package.json +++ b/packages/fxa-settings/package.json @@ -71,7 +71,7 @@ ] }, "transformIgnorePatterns": [ - "[/\\\\]node_modules[/\\\\](?!sinon|@mozilla/glean)", + "[/\\\\]node_modules[/\\\\](?!sinon|@mozilla/glean|react-markdown|rehype-raw|unified|remark-.+|rehype-.+|hast-util-.+|mdast-util-.+|micromark.+|unist-util-.+|vfile|decode-named-character-reference|character-entities|devlop|property-information|comma-separated-tokens|space-separated-tokens|zwitch|web-namespaces|ccount|bail|trough|is-plain-obj|trim-lines)", "^.+\\.module\\.(css|sass|scss)$" ], "modulePaths": [], @@ -130,7 +130,6 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@material-ui/core": "v5.0.0-alpha.24", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", "@react-pdf/renderer": "3.2.1", "@svgr/webpack": "^8.1.0", @@ -174,12 +173,12 @@ "react-async-hook": "^4.0.0", "react-dev-utils": "^12.0.1", "react-easy-crop": "^5.2.0", - "react-hook-form": "^6.15.8", - "react-markdown": "^8.0.5", + "react-hook-form": "^7.54.0", + "react-markdown": "^9.0.3", "react-refresh": "^0.16.0", "react-router": "7.18.0", "react-webcam": "^7.2.0", - "rehype-raw": "^6.1.1", + "rehype-raw": "^7.0.0", "resolve": "^1.22.8", "resolve-url-loader": "^5.0.0", "sass-loader": "^16.0.3", @@ -203,7 +202,6 @@ "@storybook/addon-links": "^8.0.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.1.3", - "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.6.0", "@types/file-saver": "^2.0.5", "@types/jest": "26.0.23", diff --git a/packages/fxa-settings/src/components/FormChoice/index.tsx b/packages/fxa-settings/src/components/FormChoice/index.tsx index 641958344ee..993e8a8a08b 100644 --- a/packages/fxa-settings/src/components/FormChoice/index.tsx +++ b/packages/fxa-settings/src/components/FormChoice/index.tsx @@ -44,7 +44,7 @@ const FormChoice = ({ formChoices, onSubmit, isSubmitting, - cmsButton + cmsButton, }: FormChoiceProps) => { const { register, handleSubmit, watch } = useForm(); const selectedOption = watch('choice'); @@ -63,9 +63,8 @@ const FormChoice = ({ className="input-radio" type="radio" id={choice.id} - name="choice" value={choice.value} - ref={register({ required: true })} + {...register('choice', { required: true })} />