From 11fadebb8796e81315d1064519dbdbbdb81480ec Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 8 Jul 2026 17:06:25 +0200 Subject: [PATCH] Fix test failures due to scoped class names --- locales/en/app.json | 1 + .../MediaMuteAndSwitchButton.test.tsx | 46 ++++++++++++------- src/components/MediaMuteAndSwitchButton.tsx | 18 +++++++- src/room/LobbyView.test.tsx | 6 +-- src/tile/TileAvatar.test.tsx | 10 ++-- src/tile/TileAvatar.tsx | 4 +- 6 files changed, 57 insertions(+), 28 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 8656502cf..3ff956a6a 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -205,6 +205,7 @@ "blur_not_supported_by_browser": "(Background blur is not supported by this device.)", "developer_tab_title": "Developer", "devices": { + "activating": "Activating…", "camera": "Camera", "camera_numbered": "Camera {{n}}", "change_device_button": "Change audio device", diff --git a/src/components/MediaMuteAndSwitchButton.test.tsx b/src/components/MediaMuteAndSwitchButton.test.tsx index d9bcee1e9..fcc9f0b4e 100644 --- a/src/components/MediaMuteAndSwitchButton.test.tsx +++ b/src/components/MediaMuteAndSwitchButton.test.tsx @@ -180,12 +180,12 @@ describe("MediaMuteAndSwitchButton", () => { ); await user.click(screen.getByRole("button", { name: "Microphone" })); - screen.getByRole("menuitem", { name: "Microphone 1" }); - screen.getByRole("menuitem", { name: "Microphone 2" }); + screen.getByRole("menuitemradio", { name: "Microphone 1" }); + screen.getByRole("menuitemradio", { name: "Microphone 2" }); await user.keyboard("[Escape]"); await user.click(screen.getByRole("button", { name: "Camera" })); - screen.getByRole("menuitem", { name: "Camera 1" }); - screen.getByRole("menuitem", { name: "Camera 2" }); + screen.getByRole("menuitemradio", { name: "Camera 1" }); + screen.getByRole("menuitemradio", { name: "Camera 2" }); }); test("calls select callback on menu click", async () => { @@ -206,7 +206,9 @@ describe("MediaMuteAndSwitchButton", () => { ); await user.click(getByRole("button", { name: "Microphone" })); - await user.click(screen.getByRole("menuitem", { name: "Microphone 2" })); + await user.click( + screen.getByRole("menuitemradio", { name: "Microphone 2" }), + ); expect(onSelect).toHaveBeenCalledWith("mic2"); }); @@ -228,7 +230,9 @@ describe("MediaMuteAndSwitchButton", () => { ); await user.click(getByRole("button", { name: "Microphone" })); - await user.click(screen.getByRole("menuitem", { name: "Microphone 1" })); + await user.click( + screen.getByRole("menuitemradio", { name: "Microphone 1" }), + ); expect(onSelect).not.toHaveBeenCalled(); }); @@ -264,18 +268,24 @@ describe("MediaMuteAndSwitchButton", () => { const { getByRole } = renderComponent(); await user.click(getByRole("button", { name: "Microphone" })); - await user.click(screen.getByRole("menuitem", { name: "Microphone 2" })); + await user.click( + screen.getByRole("menuitemradio", { name: "Microphone 2" }), + ); expect(onSelectPressed).toHaveBeenCalled(); expect(onOptionUpdated).not.toHaveBeenCalled(); // After clicking, plannedSelection="mic2" but selectedOption is still "mic1", - // so a spinner should appear on the mic2 item - const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" }); - expect(mic2Item.querySelector(".rotate")).toBeTruthy(); + // so mic2 should be in an activating state + screen.getByRole("menuitemradio", { + name: "Microphone 2 Activating…", + checked: false, + }); - // The currently-selected mic1 item should not have a spinner - const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" }); - expect(mic1Item.querySelector(".rotate")).toBeNull(); + // The currently-selected mic1 item should not be activating + screen.getByRole("menuitemradio", { + name: "Microphone 1", + checked: true, + }); await act(async () => { // resolve the promise that acutally updates the select option. resolve(); @@ -284,7 +294,7 @@ describe("MediaMuteAndSwitchButton", () => { expect(onOptionUpdated).toHaveBeenCalled(); // Spinner should now be gone since the selection has caught up - const mic2ItemAfter = screen.getByRole("menuitem", { + const mic2ItemAfter = screen.getByRole("menuitemradio", { name: "Microphone 2", }); expect(mic2ItemAfter.querySelector(".rotate")).toBeNull(); @@ -336,11 +346,15 @@ describe("MediaMuteAndSwitchButton", () => { await user.click(getByRole("button", { name: "Microphone" })); // The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG - const mic1Item = screen.getByRole("menuitem", { name: "Microphone 2" }); + const mic1Item = screen.getByRole("menuitemradio", { + name: "Microphone 2", + }); expect(mic1Item.querySelectorAll("svg").length).toBe(2); // The unselected item (mic1) only renders its IconOptions SVG - const mic2Item = screen.getByRole("menuitem", { name: "Microphone 1" }); + const mic2Item = screen.getByRole("menuitemradio", { + name: "Microphone 1", + }); expect(mic2Item.querySelectorAll("svg").length).toBe(1); }); }); diff --git a/src/components/MediaMuteAndSwitchButton.tsx b/src/components/MediaMuteAndSwitchButton.tsx index bd220330f..3e344dd3a 100644 --- a/src/components/MediaMuteAndSwitchButton.tsx +++ b/src/components/MediaMuteAndSwitchButton.tsx @@ -191,6 +191,7 @@ export const MediaMuteAndSwitchButton: FC = ({ width={24} height={24} className={styles.itemIcon} + aria-hidden /> ) } @@ -201,10 +202,23 @@ export const MediaMuteAndSwitchButton: FC = ({ onSelect?.(id); }} key={id} + role="menuitemradio" + aria-checked={selectedOption === id} > - {selectedOption === id && } + {selectedOption === id && ( + + )} {selectedOption !== id && plannedSelection === id && ( - + )} ); diff --git a/src/room/LobbyView.test.tsx b/src/room/LobbyView.test.tsx index e2bb06ccb..8cbe6be14 100644 --- a/src/room/LobbyView.test.tsx +++ b/src/room/LobbyView.test.tsx @@ -145,8 +145,7 @@ describe("LobbyView", () => { .querySelector("path")! .getAttribute("d"); const primaryButtonSvgPath = container - .querySelector(".primaryButton") - ?.querySelector("path") + .querySelector("path") ?.getAttribute("d"); expect(primaryButtonSvgPath).toBe(expectedSvgPath); expect(container).toMatchSnapshot(); @@ -169,8 +168,7 @@ describe("LobbyView", () => { .querySelector("path")! .getAttribute("d"); const primaryButtonSvgPath = container - .querySelector(".primaryButton") - ?.querySelector("path") + .querySelector("path") ?.getAttribute("d"); expect(primaryButtonSvgPath).toBe(expectedSvgPath); expect(container).toMatchSnapshot(); diff --git a/src/tile/TileAvatar.test.tsx b/src/tile/TileAvatar.test.tsx index 248e6ff76..85977a715 100644 --- a/src/tile/TileAvatar.test.tsx +++ b/src/tile/TileAvatar.test.tsx @@ -6,22 +6,22 @@ Please see LICENSE in the repository root for full details. */ import { expect, describe, it } from "vitest"; -import { render } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { TileAvatar } from "./TileAvatar"; describe("TileAvatar", () => { it("should show loading spinner when loading", () => { - const { container } = render( + render( , ); - expect(container.querySelector(".loading")).toBeInTheDocument(); + screen.getByLabelText("Loading…"); }); it("should not show loading spinner when not loading", () => { - const { container } = render( + render( , ); - expect(container.querySelector(".loading")).not.toBeInTheDocument(); + expect(screen.queryByLabelText("Loading…")).toBe(null); }); }); diff --git a/src/tile/TileAvatar.tsx b/src/tile/TileAvatar.tsx index 910a031dd..88208ff0c 100644 --- a/src/tile/TileAvatar.tsx +++ b/src/tile/TileAvatar.tsx @@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details. import { type FC } from "react"; import { InlineSpinner } from "@vector-im/compound-web"; +import { useTranslation } from "react-i18next"; import styles from "./TileAvatar.module.css"; import { Avatar, type Props as AvatarProps } from "../Avatar"; @@ -17,11 +18,12 @@ interface Props extends AvatarProps { } export const TileAvatar: FC = ({ size, loading, ...props }) => { + const { t } = useTranslation(); return (
{loading && (
- +
)}