diff --git a/.eslintrc.js b/.eslintrc.js index 10e8238a77..bc7ab5ed50 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -51,6 +51,10 @@ module.exports = { "error", { paths: [ + { + name: "@testing-library/react", + message: "Please use jest-matrix-react instead", + }, { name: "matrix-js-sdk", message: "Please use matrix-js-sdk/src/matrix instead", diff --git a/jest.config.ts b/jest.config.ts index ae1051fd71..1408b5bb46 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -37,6 +37,7 @@ const config: Config = { coverageReporters: ["text-summary", "lcov"], testResultsProcessor: "@casualbot/jest-sonar-reporter", prettierPath: null, + moduleDirectories: ["node_modules", "test/test-utils"], }; // if we're running under GHA, enable the GHA reporter diff --git a/package.json b/package.json index ea3ab91b79..8f9e6fdc88 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,9 @@ "jwt-decode": "4.0.0", "@floating-ui/react": "0.26.11", "@radix-ui/react-id": "1.1.0", - "caniuse-lite": "1.0.30001655" + "caniuse-lite": "1.0.30001655", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", + "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, "dependencies": { "@babel/runtime": "^7.12.5", @@ -80,7 +82,7 @@ "@sentry/browser": "^8.0.0", "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^1.8.0", - "@vector-im/compound-web": "^5.5.0", + "@vector-im/compound-web": "^6.3.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index 0544a7c904..b5d3790aaa 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -70,7 +70,9 @@ test.describe("Cryptography", function () { const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/); await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("This message could not be decrypted"); + await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( + "This message could not be decrypted", + ); /* Should show a red padlock for an unencrypted message in an e2e room */ await bob.evaluate( @@ -90,7 +92,7 @@ test.describe("Cryptography", function () { await expect(last).toContainText("test unencrypted"); await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("Not encrypted"); + await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted"); /* Should show no padlock for an unverified user */ // bob sends a valid event @@ -123,7 +125,9 @@ test.describe("Cryptography", function () { await expect(lastTile).toContainText("test encrypted from unverified"); await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await lastTileE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner."); + await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText( + "Encrypted by a device not verified by its owner.", + ); /* In legacy crypto: should show a grey padlock for a message from a deleted device. * In rust crypto: should show a red padlock for a message from an unverified device. @@ -159,7 +163,7 @@ test.describe("Cryptography", function () { await expect(last).toContainText("test encrypted from unverified"); await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText( + await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( workerInfo.project.name === "Legacy Crypto" ? "Encrypted by an unknown or deleted device." : "Encrypted by a device not verified by its owner.", @@ -212,7 +216,7 @@ test.describe("Cryptography", function () { // The key is coming from backup, so it is not anymore possible to establish if the claimed device // creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device." // It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted. - await expect(page.getByRole("tooltip")).toContainText( + await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText( "The authenticity of this encrypted message can't be guaranteed on this device.", ); }); @@ -296,7 +300,9 @@ test.describe("Cryptography", function () { const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner."); + await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText( + "Encrypted by a device not verified by its owner.", + ); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 9d9da67a08..2973f88cda 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 })); + await use(new AxeBuilder({ page }).exclude("[id^='floating-ui-']")); }, checkA11y: async ({ axe }, use, testInfo) => use(async () => { diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index c769a935f9..81c57cc6f7 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -172,4 +172,22 @@ export class ElementAppPage { await this.page.getByRole("button", { name: "Room info" }).first().click(); return this.page.locator(".mx_RightPanel"); } + + /** + * Get a locator for the tooltip associated with an element + * @param e The element with the tooltip + * @returns Locator to the tooltip + */ + public async getTooltipForElement(e: Locator): Promise { + const [labelledById, describedById] = await Promise.all([ + e.getAttribute("aria-labelledby"), + e.getAttribute("aria-describedby"), + ]); + if (!labelledById && !describedById) { + throw new Error( + "Element has no aria-labelledby or aria-describedy attributes! The tooltip should have added either one of these.", + ); + } + return this.page.locator(`#${labelledById ?? describedById}`); + } } diff --git a/src/Modal.tsx b/src/Modal.tsx index 22e8a56923..53a1935294 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -12,7 +12,7 @@ import ReactDOM from "react-dom"; import classNames from "classnames"; import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils"; import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; -import { Glass } from "@vector-im/compound-web"; +import { Glass, TooltipProvider } from "@vector-im/compound-web"; import dis, { defaultDispatcher } from "./dispatcher/dispatcher"; import AsyncWrapper from "./AsyncWrapper"; @@ -416,16 +416,18 @@ export class ModalManager extends TypedEventEmitter - -
{this.staticModal.elem}
-
-
-
+ +
+ +
{this.staticModal.elem}
+
+
+
+ ); ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); @@ -441,16 +443,18 @@ export class ModalManager extends TypedEventEmitter - -
{modal.elem}
-
-
-
+ +
+ +
{modal.elem}
+
+
+
+ ); setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 9085898297..d589376610 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -12,6 +12,7 @@ import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } fro import ReactDOM from "react-dom"; import classNames from "classnames"; import FocusLock from "react-focus-lock"; +import { TooltipProvider } from "@vector-im/compound-web"; import { Writeable } from "../../@types/common"; import UIStore from "../../stores/UIStore"; @@ -621,15 +622,17 @@ export function createMenu( }; const menu = ( - - - + + + + + ); ReactDOM.render(menu, getOrCreateContainer()); diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 7689017856..cc0357a8f1 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -24,6 +24,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { throttle } from "lodash"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; +import { TooltipProvider } from "@vector-im/compound-web"; // what-input helps improve keyboard accessibility import "what-input"; @@ -2181,7 +2182,9 @@ export default class MatrixChat extends React.PureComponent { return ( - {view} + + {view} + ); } diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index b45401dd4c..eb20b05fdc 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -130,7 +130,7 @@ export const ThreadPanelHeader: React.FC<{ return (
- + diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index af563a8fa2..feca331894 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -58,7 +58,7 @@ export const CheckEmail: React.FC = ({
{_t("auth|check_email_resend_prompt")} - + {_t("action|resend")} diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index 238ddf2c3b..d883177d0c 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -57,7 +57,7 @@ export const VerifyEmailModal: React.FC = ({
{_t("auth|check_email_resend_prompt")} - + {_t("action|resend")} diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index e58b06717a..b8b5297384 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -212,7 +212,7 @@ const AccessibleButton = forwardRef(function { // Tooltip are forced on the right for a more natural feel to them on info icons return ( - +
{children} diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index a380723fb2..1b7b6543e9 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { MutableRefObject, ReactNode } from "react"; import ReactDOM from "react-dom"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { TooltipProvider } from "@vector-im/compound-web"; import dis from "../../../dispatcher/dispatcher"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -167,9 +168,11 @@ export default class PersistedElement extends React.Component { private renderApp(): void { const content = ( -
- {this.props.children} -
+ +
+ {this.props.children} +
+
); diff --git a/src/components/views/elements/Pill.tsx b/src/components/views/elements/Pill.tsx index 569dcdf313..12cec19a42 100644 --- a/src/components/views/elements/Pill.tsx +++ b/src/components/views/elements/Pill.tsx @@ -141,7 +141,7 @@ export const Pill: React.FC = ({ type: propType, url, inMessage, room ; return ( - +
{ } return ( - + {icon} {timestamp} diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx index fe59f537a3..9790356762 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx +++ b/src/components/views/messages/ReactionsRowButtonTooltip.tsx @@ -51,7 +51,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent + {children} ); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 890316fc02..eb1c94e20d 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details. import React, { createRef, SyntheticEvent, MouseEvent } from "react"; import ReactDOM from "react-dom"; import { MsgType } from "matrix-js-sdk/src/matrix"; +import { TooltipProvider } from "@vector-im/compound-web"; import * as HtmlUtils from "../../../HtmlUtils"; import { formatDate } from "../../../DateUtils"; @@ -335,7 +336,11 @@ export default class TextualBody extends React.Component { const reason = node.getAttribute("data-mx-spoiler") ?? undefined; node.removeAttribute("data-mx-spoiler"); // we don't want to recurse - const spoiler = ; + const spoiler = ( + + + + ); ReactDOM.render(spoiler, spoilerContainer); node.parentNode?.replaceChild(spoilerContainer, node); diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index fc879c3254..7b39ee0c57 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -373,9 +373,7 @@ const RoomSummaryCard: React.FC = ({ Icon={FavouriteIcon} label={_t("room|context_menu|favourite")} checked={isFavorite} - onChange={() => tagRoom(room, DefaultTagID.Favourite)} - // XXX: https://github.com/element-hq/compound/issues/288 - onSelect={() => {}} + onSelect={() => tagRoom(room, DefaultTagID.Favourite)} />
-
+
{nonCssBadge} diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 99e4507c15..e1677a179e 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -666,7 +666,7 @@ export class MessageComposer extends React.Component { }); return ( - +
{recordingTooltip}
diff --git a/src/components/views/rooms/ReadReceiptGroup.tsx b/src/components/views/rooms/ReadReceiptGroup.tsx index 8ac0db0e12..ea60518f4b 100644 --- a/src/components/views/rooms/ReadReceiptGroup.tsx +++ b/src/components/views/rooms/ReadReceiptGroup.tsx @@ -211,7 +211,7 @@ export function ReadReceiptPerson({ onAfterClick, }: ReadReceiptPersonProps): JSX.Element { return ( - +
- + diff --git a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx index 3e7c1531a6..ae8e7be16b 100644 --- a/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx +++ b/src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx @@ -80,7 +80,7 @@ export const CallGuestLinkButton: React.FC<{ room: Room }> = ({ room }) => { <> {canInviteGuests && ( - + diff --git a/src/toasts/IncomingCallToast.tsx b/src/toasts/IncomingCallToast.tsx index 5ecde7eb08..bdad2d4565 100644 --- a/src/toasts/IncomingCallToast.tsx +++ b/src/toasts/IncomingCallToast.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { useCallback, useEffect, useState } from "react"; import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; -import { Button, Tooltip } from "@vector-im/compound-web"; +import { Button, Tooltip, TooltipProvider } from "@vector-im/compound-web"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; import { _t } from "../languageHandler"; @@ -47,7 +47,7 @@ function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButt disTooltip = disabledTooltip ?? disabledBecauseFullTooltip ?? undefined; return ( - +