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 (