From d8800ef987fa48de2005c5aa3a7b88589f1fd0fd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 21 Oct 2024 14:50:06 +0100 Subject: [PATCH] Update to React 18 (#24763) * Upgrade target to es2021 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Upgrade target to es2021 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Upgrade to es2022 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Upgrade to es2022 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix babel config Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix babel config Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix React contexts Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix React state Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update to React 18 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update to React 18 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Install @testing-library/dom Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update lockfile Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Yarn lock update * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 23 +- playwright/element-web-test.ts | 2 +- playwright/pages/ElementAppPage.ts | 2 +- scripts/make-react-component.js | 2 +- src/@types/react.d.ts | 3 + src/NodeAnimator.tsx | 9 +- src/async-components/structures/ErrorView.tsx | 1 + src/components/views/auth/VectorAuthPage.tsx | 2 +- src/components/views/dialogs/BaseDialog.tsx | 1 - src/languageHandler.tsx | 2 +- test/setupTests.ts | 7 + test/test-utils/jest-matrix-react.tsx | 1 + .../security/NewRecoveryMethodDialog-test.tsx | 19 +- .../components/structures/MatrixChat-test.tsx | 13 +- .../__snapshots__/FilePanel-test.tsx.snap | 2 +- .../__snapshots__/RoomView-test.tsx.snap | 50 +- .../SpaceHierarchy-test.tsx.snap | 2 +- .../__snapshots__/ThreadPanel-test.tsx.snap | 4 +- .../DecoratedRoomAvatar-test.tsx.snap | 4 +- .../WithPresenceIndicator-test.tsx.snap | 6 +- .../BeaconListItem-test.tsx.snap | 2 +- .../__snapshots__/DialogSidebar-test.tsx.snap | 6 +- .../ShareLatestLocation-test.tsx.snap | 2 +- .../views/dialogs/SpotlightDialog-test.tsx | 190 +- .../DevtoolsDialog-test.tsx.snap | 2 +- .../__snapshots__/LogoutDialog-test.tsx.snap | 2 - .../MessageEditHistoryDialog-test.tsx.snap | 2 - .../ServerPickerDialog-test.tsx.snap | 2 +- .../views/elements/SearchWarning-test.tsx | 2 +- .../__snapshots__/AppTile-test.tsx.snap | 6 +- .../__snapshots__/FacePile-test.tsx.snap | 2 +- .../__snapshots__/ImageView-test.tsx.snap | 2 +- .../__snapshots__/InfoTooltip-test.tsx.snap | 2 +- .../__snapshots__/RoomFacePile-test.tsx.snap | 4 +- .../LocationViewDialog-test.tsx.snap | 2 +- .../__snapshots__/MLocationBody-test.tsx.snap | 4 +- .../__snapshots__/MPollBody-test.tsx.snap | 48 +- .../pollHistory/PollListItemEnded-test.tsx | 36 +- .../__snapshots__/PollHistory-test.tsx.snap | 4 +- .../__snapshots__/PollListItem-test.tsx.snap | 2 +- .../PollListItemEnded-test.tsx.snap | 2 +- .../__snapshots__/BaseCard-test.tsx.snap | 2 +- .../ExtensionsCard-test.tsx.snap | 4 +- .../PinnedMessagesCard-test.tsx.snap | 22 +- .../RoomSummaryCard-test.tsx.snap | 12 +- .../__snapshots__/UserInfo-test.tsx.snap | 4 +- .../VideoRoomChatButton-test.tsx.snap | 2 +- .../PinnedEventTile-test.tsx.snap | 16 +- .../ReadReceiptGroup-test.tsx.snap | 9 +- .../__snapshots__/RoomHeader-test.tsx.snap | 8 +- .../ThirdPartyMemberInfo-test.tsx.snap | 4 +- .../hooks/usePlainTextListeners-test.tsx | 3 +- .../settings/AddRemoveThreepids-test.tsx | 2 +- .../views/settings/CryptographyPanel-test.tsx | 8 +- .../LayoutSwitcher-test.tsx.snap | 20 +- .../SecureBackupPanel-test.tsx.snap | 3 - .../ThemeChoicePanel-test.tsx.snap | 76 +- .../FilteredDeviceListHeader-test.tsx.snap | 4 +- .../tabs/user/SessionManagerTab-test.tsx | 6 +- .../tabs/user/VoiceUserSettingsTab-test.tsx | 7 +- .../AppearanceUserSettingsTab-test.tsx.snap | 32 +- .../SecurityUserSettingsTab-test.tsx.snap | 1 - .../SessionManagerTab-test.tsx.snap | 2 +- .../__snapshots__/SpacePanel-test.tsx.snap | 13 +- .../ThreadsActivityCentre-test.tsx.snap | 72 +- .../spaces/useUnreadThreadRooms-test.tsx | 3 +- .../components/views/voip/VideoFeed-test.tsx | 2 +- .../room/useRoomThreadNotifications-test.tsx | 2 +- .../hooks/useDebouncedCallback-test.tsx | 2 +- .../unit-tests/hooks/useLatestResult-test.tsx | 4 +- .../hooks/useNotificationSettings-test.tsx | 3 +- test/unit-tests/hooks/useProfileInfo-test.tsx | 3 +- .../hooks/usePublicRoomDirectory-test.tsx | 3 +- test/unit-tests/hooks/useRoomMembers-test.tsx | 3 +- .../hooks/useSlidingSyncRoomSearch-test.tsx | 3 +- .../hooks/useUnreadNotifications-test.ts | 2 +- .../hooks/useUserDirectory-test.tsx | 3 +- .../hooks/useUserOnboardingTasks-test.tsx | 3 +- test/unit-tests/hooks/useWindowWidth-test.ts | 3 +- webpack.config.js | 1 + yarn.lock | 1608 +++++++---------- 81 files changed, 1056 insertions(+), 1403 deletions(-) diff --git a/package.json b/package.json index 44e1a73f00..4ab26bb1df 100644 --- a/package.json +++ b/package.json @@ -74,13 +74,9 @@ "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" }, "resolutions": { - "@types/react-dom": "17.0.25", - "@types/react": "17.0.83", "@types/seedrandom": "3.0.8", "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", - "@floating-ui/react": "0.26.11", - "@radix-ui/react-id": "1.1.0", "caniuse-lite": "1.0.30001668", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" @@ -94,7 +90,6 @@ "@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", - "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^1.8.0", "@vector-im/compound-web": "^7.1.0", "@zxcvbn-ts/core": "^3.0.4", @@ -141,10 +136,10 @@ "posthog-js": "1.157.2", "qrcode": "1.5.4", "re-resizable": "6.9.17", - "react": "17.0.2", + "react": "^18.3.1", "react-beautiful-dnd": "^13.1.0", "react-blurhash": "^0.3.0", - "react-dom": "17.0.2", + "react-dom": "^18.3.1", "react-focus-lock": "^2.5.1", "react-transition-group": "^4.4.1", "rfc4648": "^1.4.0", @@ -186,10 +181,10 @@ "@sentry/webpack-plugin": "^2.7.1", "@stylistic/eslint-plugin": "^2.9.0", "@svgr/webpack": "^8.0.0", - "@testing-library/dom": "^9.0.0", - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^12.1.5", - "@testing-library/user-event": "^14.4.3", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.4.8", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/commonmark": "^0.27.4", "@types/content-type": "^1.1.5", "@types/counterpart": "^0.18.1", @@ -211,9 +206,9 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "17.0.83", + "@types/react": "18.3.3", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "17.0.25", + "@types/react-dom": "18.3.0", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.13.0", "@types/sdp-transform": "^2.4.6", @@ -260,7 +255,7 @@ "husky": "^9.0.0", "jest": "^29.6.2", "jest-canvas-mock": "^2.5.2", - "jest-environment-jsdom": "^29.6.2", + "jest-environment-jsdom": "^29.7.0", "jest-mock": "^29.6.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 66dd666389..93b119ee7a 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -224,7 +224,7 @@ export const test = base.extend<{ }, axe: async ({ page }, use) => { - await use(new AxeBuilder({ page }).exclude("[id^='floating-ui-']")); + await use(new AxeBuilder({ page }).exclude("[data-floating-ui-portal]")); }, checkA11y: async ({ axe }, use, testInfo) => use(async () => { diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index 81c57cc6f7..4cff3c72ea 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -188,6 +188,6 @@ export class ElementAppPage { "Element has no aria-labelledby or aria-describedy attributes! The tooltip should have added either one of these.", ); } - return this.page.locator(`#${labelledById ?? describedById}`); + return this.page.locator(`id=${labelledById ?? describedById}`); } } diff --git a/scripts/make-react-component.js b/scripts/make-react-component.js index 40eb331785..69d2957d42 100755 --- a/scripts/make-react-component.js +++ b/scripts/make-react-component.js @@ -32,7 +32,7 @@ export default %%ComponentName%%; `, TEST: ` import React from "react"; -import { render } from "@testing-library/react"; +import { render } from "jest-matrix-react"; import %%ComponentName%% from '%%RelativeComponentPath%%'; diff --git a/src/@types/react.d.ts b/src/@types/react.d.ts index 3579e0cec0..ba97b2bba6 100644 --- a/src/@types/react.d.ts +++ b/src/@types/react.d.ts @@ -13,4 +13,7 @@ declare module "react" { function forwardRef( render: (props: PropsWithChildren

, ref: React.ForwardedRef) => React.ReactElement | null, ): (props: P & React.RefAttributes) => React.ReactElement | null; + + // Fix lazy types - https://stackoverflow.com/a/71017028 + function lazy>(factory: () => Promise<{ default: T }>): T; } diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx index bb6885e427..b5abcd2440 100644 --- a/src/NodeAnimator.tsx +++ b/src/NodeAnimator.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { Key, MutableRefObject, ReactElement, ReactFragment, ReactInstance, ReactPortal } from "react"; +import React, { Key, MutableRefObject, ReactElement, ReactInstance } from "react"; import ReactDom from "react-dom"; interface IChildProps { @@ -24,7 +24,7 @@ interface IProps { innerRef?: MutableRefObject; } -function isReactElement(c: ReactElement | ReactFragment | ReactPortal): c is ReactElement { +function isReactElement(c: ReturnType<(typeof React.Children)["toArray"]>[number]): c is ReactElement { return typeof c === "object" && "type" in c; } @@ -99,7 +99,8 @@ export default class NodeAnimator extends React.Component { } private collectNode(k: Key, node: React.ReactInstance, restingStyle: React.CSSProperties): void { - if (node && this.nodes[k] === undefined && this.props.startStyles.length > 0) { + const key = typeof k === "bigint" ? Number(k) : k; + if (node && this.nodes[key] === undefined && this.props.startStyles.length > 0) { const startStyles = this.props.startStyles; const domNode = ReactDom.findDOMNode(node); // start from startStyle 1: 0 is the one we gave it @@ -113,7 +114,7 @@ export default class NodeAnimator extends React.Component { this.applyStyles(domNode as HTMLElement, restingStyle); }, 0); } - this.nodes[k] = node; + this.nodes[key] = node; if (this.props.innerRef) { this.props.innerRef.current = node; diff --git a/src/async-components/structures/ErrorView.tsx b/src/async-components/structures/ErrorView.tsx index f2713e00af..5e60b8e67c 100644 --- a/src/async-components/structures/ErrorView.tsx +++ b/src/async-components/structures/ErrorView.tsx @@ -25,6 +25,7 @@ interface IProps { title: string; messages?: string[]; footer?: ReactNode; + children?: ReactNode; } export const ErrorView: React.FC = ({ title, messages, footer, children }) => { diff --git a/src/components/views/auth/VectorAuthPage.tsx b/src/components/views/auth/VectorAuthPage.tsx index 55cc76fa45..969cc560a3 100644 --- a/src/components/views/auth/VectorAuthPage.tsx +++ b/src/components/views/auth/VectorAuthPage.tsx @@ -10,7 +10,7 @@ import * as React from "react"; import SdkConfig from "../../../SdkConfig"; import VectorAuthFooter from "./VectorAuthFooter"; -export default class VectorAuthPage extends React.PureComponent { +export default class VectorAuthPage extends React.PureComponent { private static welcomeBackgroundUrl?: string; // cache the url as a static to prevent it changing without refreshing diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 73c46635b1..00da8b4f52 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -120,7 +120,6 @@ export default class BaseDialog extends React.Component { onClick={this.onCancelClick} className="mx_Dialog_cancelButton" aria-label={_t("dialog_close_label")} - title={_t("action|close")} placement="bottom" /> ); diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index dedcb60dcd..c30ca94922 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -436,7 +436,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri } if (shouldWrapInSpan) { - return React.createElement("span", null, ...output); + return React.createElement("span", null, ...(output as Array)); } else { return output.join(""); } diff --git a/test/setupTests.ts b/test/setupTests.ts index 64b8fa2909..f0067a5d23 100644 --- a/test/setupTests.ts +++ b/test/setupTests.ts @@ -13,6 +13,13 @@ import { mocked } from "jest-mock"; import { PredictableRandom } from "./test-utils/predictableRandom"; // https://github.com/jsdom/jsdom/issues/2555 +declare global { + // eslint-disable-next-line no-var + var IS_REACT_ACT_ENVIRONMENT: boolean; +} + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + // Fake random strings to give a predictable snapshot for IDs jest.mock("matrix-js-sdk/src/randomstring"); beforeEach(() => { diff --git a/test/test-utils/jest-matrix-react.tsx b/test/test-utils/jest-matrix-react.tsx index 2aad5d45ff..4fbb0dc77d 100644 --- a/test/test-utils/jest-matrix-react.tsx +++ b/test/test-utils/jest-matrix-react.tsx @@ -27,6 +27,7 @@ const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => { const customRender = (ui: ReactElement, options: RenderOptions = {}) => { return render(ui, { + legacyRoot: true, ...options, wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"], }) as ReturnType; diff --git a/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx b/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx index fc964e57bf..f6d9a59b51 100644 --- a/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx +++ b/test/unit-tests/async-components/dialogs/security/NewRecoveryMethodDialog-test.tsx @@ -7,10 +7,9 @@ import React from "react"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { render, screen } from "jest-matrix-react"; +import { render, screen, act } from "jest-matrix-react"; import { waitFor } from "@testing-library/dom"; import userEvent from "@testing-library/user-event"; -import { act } from "@testing-library/react-hooks/dom"; import NewRecoveryMethodDialog from "../../../../../src/async-components/views/dialogs/security/NewRecoveryMethodDialog"; import { createTestClient } from "../../../../test-utils"; @@ -55,15 +54,13 @@ describe("", () => { const onFinished = jest.fn(); - await act(async () => { - const { asFragment } = renderComponent(onFinished); - await waitFor(() => - expect( - screen.getByText("This session is encrypting history using the new recovery method."), - ).toBeInTheDocument(), - ); - expect(asFragment()).toMatchSnapshot(); - }); + const { asFragment } = renderComponent(onFinished); + await waitFor(() => + expect( + screen.getByText("This session is encrypting history using the new recovery method."), + ).toBeInTheDocument(), + ); + expect(asFragment()).toMatchSnapshot(); await userEvent.click(screen.getByRole("button", { name: "Set up Secure Messages" })); expect(onFinished).toHaveBeenCalled(); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index def72a11f8..af0453af2a 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -55,7 +55,7 @@ import * as Lifecycle from "../../../../src/Lifecycle"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../../src/BasePlatform"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; -import { MatrixClientPeg, MatrixClientPeg as peg } from "../../../../src/MatrixClientPeg"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; import { ReleaseAnnouncementStore } from "../../../../src/stores/ReleaseAnnouncementStore"; import { DRAFT_LAST_CLEANUP_KEY } from "../../../../src/DraftCleaner"; @@ -933,17 +933,13 @@ describe("", () => { // but as the exception was swallowed, the test was passing (see in `initClientCrypto`). // There are several uses of the peg in the app, so during all these tests you might end-up // with a real client instead of the mocked one. Not sure how reliable all these tests are. - const originalReplace = peg.replaceUsingCreds; - peg.replaceUsingCreds = jest.fn().mockResolvedValue(mockClient); - // @ts-ignore - need to mock this for the test - peg.matrixClient = mockClient; + jest.spyOn(MatrixClientPeg, "replaceUsingCreds"); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); const result = getComponent(); await result.findByText("You're signed out"); expect(result.container).toMatchSnapshot(); - - peg.replaceUsingCreds = originalReplace; }); }); @@ -1492,8 +1488,6 @@ describe("", () => { action: "start_mobile_registration", }); - await flushPromises(); - return renderResult; }; @@ -1514,6 +1508,7 @@ describe("", () => { enabledMobileRegistration(); await getComponentAndWaitForReady(); + await flushPromises(); expect(screen.getByTestId("mobile-register")).toBeInTheDocument(); }); diff --git a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap index ace762cd23..6a58ac9811 100644 --- a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap @@ -19,7 +19,7 @@ exports[`FilePanel renders empty state 1`] = `

+