From 80024991aac4aca9baa7855c4c4a8aa3344c7b79 Mon Sep 17 00:00:00 2001 From: rbondesson Date: Thu, 21 May 2026 14:30:05 +0200 Subject: [PATCH] Stabilise React useId values in DOM snapshots in element-web (#33579) * Stabilise React useId values in DOM snapshots - app/web * Stabilise React useId values in DOM snapshots - shared components * Update additional snapshots * Added comments and changed replace pattern to 'react-use-id-N' --- apps/web/test/setupTests.ts | 37 + .../__snapshots__/FilePanel-test.tsx.snap | 2 +- .../__snapshots__/RoomView-test.tsx.snap | 94 +- .../SpaceHierarchy-test.tsx.snap | 50 +- .../__snapshots__/ThreadPanel-test.tsx.snap | 4 +- .../DecoratedRoomAvatar-test.tsx.snap | 4 +- .../RoomAvatarView-test.tsx.snap | 14 +- .../WithPresenceIndicator-test.tsx.snap | 6 +- .../BeaconListItem-test.tsx.snap | 2 +- .../__snapshots__/DialogSidebar-test.tsx.snap | 2 +- .../ShareLatestLocation-test.tsx.snap | 2 +- .../ConfirmRejectInviteDialog-test.tsx.snap | 12 +- .../CreateRoomDialog-test.tsx.snap | 36 +- .../CreateSectionDialog-test.tsx.snap | 4 +- .../DevtoolsDialog-test.tsx.snap | 8 +- .../__snapshots__/LogoutDialog-test.tsx.snap | 4 +- ...nageRestrictedJoinRuleDialog-test.tsx.snap | 4 +- .../MessageEditHistoryDialog-test.tsx.snap | 4 +- .../ReportRoomDialog-test.tsx.snap | 6 +- .../ServerPickerDialog-test.tsx.snap | 2 +- .../VerificationRequestDialog-test.tsx.snap | 2 +- ...idgetOpenIDPermissionsDialog-test.tsx.snap | 4 +- .../__snapshots__/Users-test.tsx.snap | 4 +- .../RestoreKeyBackupDialog-test.tsx.snap | 6 +- .../__snapshots__/AppTile-test.tsx.snap | 14 +- .../__snapshots__/FacePile-test.tsx.snap | 2 +- .../__snapshots__/ImageView-test.tsx.snap | 2 +- .../__snapshots__/InfoTooltip-test.tsx.snap | 2 +- .../LabelledCheckbox-test.tsx.snap | 4 +- .../__snapshots__/RoomFacePile-test.tsx.snap | 4 +- .../SettingsDropdown-test.tsx.snap | 8 +- .../__snapshots__/SettingsField-test.tsx.snap | 4 +- .../LocationShareMenu-test.tsx.snap | 4 +- .../LocationViewDialog-test.tsx.snap | 2 +- .../__snapshots__/MLocationBody-test.tsx.snap | 4 +- .../__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 | 38 +- .../RoomSummaryCardView-test.tsx.snap | 12 +- .../__snapshots__/UserInfo-test.tsx.snap | 4 +- .../E2eMessageSharedIcon-test.tsx.snap | 4 +- .../__snapshots__/RoomHeader-test.tsx.snap | 8 +- .../VideoRoomChatButton-test.tsx.snap | 2 +- .../PinnedEventTile-test.tsx.snap | 24 +- .../PinnedMessageBanner-test.tsx.snap | 36 +- .../ReadReceiptGroup-test.tsx.snap | 8 +- .../ThirdPartyMemberInfo-test.tsx.snap | 4 +- .../MemberTileView-test.tsx.snap | 4 +- .../LayoutSwitcher-test.tsx.snap | 20 +- .../__snapshots__/Notifications-test.tsx.snap | 6 +- .../__snapshots__/SetIdServer-test.tsx.snap | 4 +- .../SetIntegrationManager-test.tsx.snap | 4 +- .../ThemeChoicePanel-test.tsx.snap | 76 +- .../FilteredDeviceListHeader-test.tsx.snap | 4 +- .../__snapshots__/AdvancedPanel-test.tsx.snap | 8 +- .../ChangeRecoveryKey-test.tsx.snap | 24 +- .../Notifications2-test.tsx.snap | 16 +- .../AppearanceUserSettingsTab-test.tsx.snap | 32 +- .../PreferencesUserSettingsTab-test.tsx.snap | 22 +- .../SecurityUserSettingsTab-test.tsx.snap | 8 +- .../SessionManagerTab-test.tsx.snap | 2 +- .../SidebarUserSettingsTab-test.tsx.snap | 32 +- .../__snapshots__/SpacePanel-test.tsx.snap | 8 +- .../SpaceSettingsVisibilityTab-test.tsx.snap | 20 +- .../ThreadsActivityCentre-test.tsx.snap | 18 +- .../__snapshots__/link-tooltip-test.tsx.snap | 2 +- .../PlayPauseButton.test.tsx.snap | 4 +- .../Pill/__snapshots__/Pill.test.tsx.snap | 6 +- .../__snapshots__/RichList.test.tsx.snap | 6 +- .../__snapshots__/UserMenu.test.tsx.snap | 26 +- .../__snapshots__/GroupView.test.tsx.snap | 18 +- .../LeftResizablePanelView.test.tsx.snap | 12 +- .../__snapshots__/SeparatorView.test.tsx.snap | 52 +- .../RoomListHeaderView.test.tsx.snap | 26 +- .../ComposeMenuView.test.tsx.snap | 4 +- .../OptionMenuView.test.tsx.snap | 4 +- .../__snapshots__/SpaceMenuView.test.tsx.snap | 2 +- .../RoomListPrimaryFilters.test.tsx.snap | 18 +- .../__snapshots__/RoomListToast.test.tsx.snap | 4 +- .../__snapshots__/RoomListView.test.tsx.snap | 1124 ++++++++--------- .../RoomListItemView.test.tsx.snap | 88 +- .../RoomListSectionHeaderView.test.tsx.snap | 4 +- .../VirtualizedRoomListView.test.tsx.snap | 80 +- .../RoomStatusBarView.test.tsx.snap | 28 +- .../__snapshots__/WidgetPipView.test.tsx.snap | 8 +- .../__snapshots__/UploadButton.test.tsx.snap | 10 +- .../WidgetContextMenuView.test.tsx.snap | 8 +- .../DateSeparatorView.test.tsx.snap | 4 +- .../__snapshots__/E2ePadlock.test.tsx.snap | 6 +- .../AudioPlayerView.test.tsx.snap | 8 +- .../ReactionsRowButtonTooltip.test.tsx.snap | 4 +- .../shared-components/src/test/setupTests.ts | 39 +- 95 files changed, 1247 insertions(+), 1173 deletions(-) diff --git a/apps/web/test/setupTests.ts b/apps/web/test/setupTests.ts index f514c10482..bcfede9805 100644 --- a/apps/web/test/setupTests.ts +++ b/apps/web/test/setupTests.ts @@ -22,6 +22,43 @@ declare global { globalThis.IS_REACT_ACT_ENVIRONMENT = true; +const REACT_USE_ID = /_r_[a-z0-9]+_/g; + +function normaliseReactUseIds(snapshot: string): string { + // React useId values can vary between runs and make snapshots flaky: + // https://github.com/element-hq/element-web/issues/31765 + // Avoid running the regex for DOM snapshots without React useId output. + if (!snapshot.includes("_r_")) return snapshot; + + const ids = new Map(); + let nextId = 1; + + return snapshot.replace(REACT_USE_ID, (id) => { + let replacement = ids.get(id); + if (!replacement) { + replacement = `react-use-id-${nextId++}`; + ids.set(id, replacement); + } + return replacement; + }); +} + +// Prevent this serializer from recursively matching the same DOM node when it calls serialize(). +let isSerializingDomSnapshot = false; + +expect.addSnapshotSerializer({ + test: (value: unknown): value is Element | DocumentFragment => + !isSerializingDomSnapshot && (value instanceof Element || value instanceof DocumentFragment), + print: (value: unknown, serialize: (value: unknown) => string): string => { + isSerializingDomSnapshot = true; + try { + return normaliseReactUseIds(serialize(value)); + } finally { + isSerializingDomSnapshot = false; + } + }, +}); + // Fake random strings to give a predictable snapshot for IDs jest.mock("matrix-js-sdk/src/randomstring"); beforeEach(() => { diff --git a/apps/web/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap b/apps/web/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap index 07c557e9d0..6a45b1d723 100644 --- a/apps/web/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap +++ b/apps/web/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap @@ -19,7 +19,7 @@ exports[`FilePanel renders empty state 1`] = `

@@ -13,7 +13,7 @@ exports[`ReactionsRowButtonTooltip > renders the tooltip with formatted senders exports[`ReactionsRowButtonTooltip > renders the tooltip with many senders 1`] = `
diff --git a/packages/shared-components/src/test/setupTests.ts b/packages/shared-components/src/test/setupTests.ts index 614dd85da3..f21999885c 100644 --- a/packages/shared-components/src/test/setupTests.ts +++ b/packages/shared-components/src/test/setupTests.ts @@ -7,11 +7,48 @@ Please see LICENSE files in the repository root for full details. import fetchMock from "@fetch-mock/vitest"; import { cleanup } from "@test-utils"; -import { afterEach } from "vitest"; +import { afterEach, expect } from "vitest"; import { setLanguage } from "../../src/core/i18n/i18n"; import en from "../i18n/strings/en_EN.json"; +const REACT_USE_ID = /_r_[a-z0-9]+_/g; + +function normaliseReactUseIds(snapshot: string): string { + // React useId values can vary between runs and make snapshots flaky: + // https://github.com/element-hq/element-web/issues/31765 + // Avoid running the regex for DOM snapshots without React useId output. + if (!snapshot.includes("_r_")) return snapshot; + + const ids = new Map(); + let nextId = 1; + + return snapshot.replace(REACT_USE_ID, (id) => { + let replacement = ids.get(id); + if (!replacement) { + replacement = `react-use-id-${nextId++}`; + ids.set(id, replacement); + } + return replacement; + }); +} + +// Prevent this serializer from recursively matching the same DOM node when it calls serialize(). +let isSerializingDomSnapshot = false; + +expect.addSnapshotSerializer({ + test: (value: unknown): value is Element | DocumentFragment => + !isSerializingDomSnapshot && (value instanceof Element || value instanceof DocumentFragment), + print: (value: unknown, serialize: (value: unknown) => string): string => { + isSerializingDomSnapshot = true; + try { + return normaliseReactUseIds(serialize(value)); + } finally { + isSerializingDomSnapshot = false; + } + }, +}); + function setupLanguageMock(): void { fetchMock .get("end:/i18n/languages.json", {