Fix test failures due to scoped class names

This commit is contained in:
Robin
2026-07-08 17:06:25 +02:00
parent 943ce5b853
commit 11fadebb87
6 changed files with 57 additions and 28 deletions

View File

@@ -205,6 +205,7 @@
"blur_not_supported_by_browser": "(Background blur is not supported by this device.)", "blur_not_supported_by_browser": "(Background blur is not supported by this device.)",
"developer_tab_title": "Developer", "developer_tab_title": "Developer",
"devices": { "devices": {
"activating": "Activating…",
"camera": "Camera", "camera": "Camera",
"camera_numbered": "Camera {{n}}", "camera_numbered": "Camera {{n}}",
"change_device_button": "Change audio device", "change_device_button": "Change audio device",

View File

@@ -180,12 +180,12 @@ describe("MediaMuteAndSwitchButton", () => {
); );
await user.click(screen.getByRole("button", { name: "Microphone" })); await user.click(screen.getByRole("button", { name: "Microphone" }));
screen.getByRole("menuitem", { name: "Microphone 1" }); screen.getByRole("menuitemradio", { name: "Microphone 1" });
screen.getByRole("menuitem", { name: "Microphone 2" }); screen.getByRole("menuitemradio", { name: "Microphone 2" });
await user.keyboard("[Escape]"); await user.keyboard("[Escape]");
await user.click(screen.getByRole("button", { name: "Camera" })); await user.click(screen.getByRole("button", { name: "Camera" }));
screen.getByRole("menuitem", { name: "Camera 1" }); screen.getByRole("menuitemradio", { name: "Camera 1" });
screen.getByRole("menuitem", { name: "Camera 2" }); screen.getByRole("menuitemradio", { name: "Camera 2" });
}); });
test("calls select callback on menu click", async () => { test("calls select callback on menu click", async () => {
@@ -206,7 +206,9 @@ describe("MediaMuteAndSwitchButton", () => {
); );
await user.click(getByRole("button", { name: "Microphone" })); 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"); expect(onSelect).toHaveBeenCalledWith("mic2");
}); });
@@ -228,7 +230,9 @@ describe("MediaMuteAndSwitchButton", () => {
); );
await user.click(getByRole("button", { name: "Microphone" })); 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(); expect(onSelect).not.toHaveBeenCalled();
}); });
@@ -264,18 +268,24 @@ describe("MediaMuteAndSwitchButton", () => {
const { getByRole } = renderComponent(<Wrapper />); const { getByRole } = renderComponent(<Wrapper />);
await user.click(getByRole("button", { name: "Microphone" })); 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(onSelectPressed).toHaveBeenCalled();
expect(onOptionUpdated).not.toHaveBeenCalled(); expect(onOptionUpdated).not.toHaveBeenCalled();
// After clicking, plannedSelection="mic2" but selectedOption is still "mic1", // After clicking, plannedSelection="mic2" but selectedOption is still "mic1",
// so a spinner should appear on the mic2 item // so mic2 should be in an activating state
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" }); screen.getByRole("menuitemradio", {
expect(mic2Item.querySelector(".rotate")).toBeTruthy(); name: "Microphone 2 Activating…",
checked: false,
});
// The currently-selected mic1 item should not have a spinner // The currently-selected mic1 item should not be activating
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" }); screen.getByRole("menuitemradio", {
expect(mic1Item.querySelector(".rotate")).toBeNull(); name: "Microphone 1",
checked: true,
});
await act(async () => { await act(async () => {
// resolve the promise that acutally updates the select option. // resolve the promise that acutally updates the select option.
resolve(); resolve();
@@ -284,7 +294,7 @@ describe("MediaMuteAndSwitchButton", () => {
expect(onOptionUpdated).toHaveBeenCalled(); expect(onOptionUpdated).toHaveBeenCalled();
// Spinner should now be gone since the selection has caught up // Spinner should now be gone since the selection has caught up
const mic2ItemAfter = screen.getByRole("menuitem", { const mic2ItemAfter = screen.getByRole("menuitemradio", {
name: "Microphone 2", name: "Microphone 2",
}); });
expect(mic2ItemAfter.querySelector(".rotate")).toBeNull(); expect(mic2ItemAfter.querySelector(".rotate")).toBeNull();
@@ -336,11 +346,15 @@ describe("MediaMuteAndSwitchButton", () => {
await user.click(getByRole("button", { name: "Microphone" })); await user.click(getByRole("button", { name: "Microphone" }));
// The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG // 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); expect(mic1Item.querySelectorAll("svg").length).toBe(2);
// The unselected item (mic1) only renders its IconOptions SVG // 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); expect(mic2Item.querySelectorAll("svg").length).toBe(1);
}); });
}); });

View File

@@ -191,6 +191,7 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
width={24} width={24}
height={24} height={24}
className={styles.itemIcon} className={styles.itemIcon}
aria-hidden
/> />
) )
} }
@@ -201,10 +202,23 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
onSelect?.(id); onSelect?.(id);
}} }}
key={id} key={id}
role="menuitemradio"
aria-checked={selectedOption === id}
> >
{selectedOption === id && <CheckIcon width={24} height={24} />} {selectedOption === id && (
<CheckIcon
width={24}
height={24}
aria-hidden // A label would be redundant to aria-checked above
/>
)}
{selectedOption !== id && plannedSelection === id && ( {selectedOption !== id && plannedSelection === id && (
<SpinnerIcon width={24} height={24} className={styles.rotate} /> <SpinnerIcon
width={24}
height={24}
className={styles.rotate}
aria-label={t("settings.devices.activating")}
/>
)} )}
</MenuItem> </MenuItem>
); );

View File

@@ -145,8 +145,7 @@ describe("LobbyView", () => {
.querySelector("path")! .querySelector("path")!
.getAttribute("d"); .getAttribute("d");
const primaryButtonSvgPath = container const primaryButtonSvgPath = container
.querySelector(".primaryButton") .querySelector("path")
?.querySelector("path")
?.getAttribute("d"); ?.getAttribute("d");
expect(primaryButtonSvgPath).toBe(expectedSvgPath); expect(primaryButtonSvgPath).toBe(expectedSvgPath);
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
@@ -169,8 +168,7 @@ describe("LobbyView", () => {
.querySelector("path")! .querySelector("path")!
.getAttribute("d"); .getAttribute("d");
const primaryButtonSvgPath = container const primaryButtonSvgPath = container
.querySelector(".primaryButton") .querySelector("path")
?.querySelector("path")
?.getAttribute("d"); ?.getAttribute("d");
expect(primaryButtonSvgPath).toBe(expectedSvgPath); expect(primaryButtonSvgPath).toBe(expectedSvgPath);
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();

View File

@@ -6,22 +6,22 @@ Please see LICENSE in the repository root for full details.
*/ */
import { expect, describe, it } from "vitest"; import { expect, describe, it } from "vitest";
import { render } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { TileAvatar } from "./TileAvatar"; import { TileAvatar } from "./TileAvatar";
describe("TileAvatar", () => { describe("TileAvatar", () => {
it("should show loading spinner when loading", () => { it("should show loading spinner when loading", () => {
const { container } = render( render(
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={true} />, <TileAvatar id="@a:example.org" name="Alice" size={96} loading={true} />,
); );
expect(container.querySelector(".loading")).toBeInTheDocument(); screen.getByLabelText("Loading");
}); });
it("should not show loading spinner when not loading", () => { it("should not show loading spinner when not loading", () => {
const { container } = render( render(
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={false} />, <TileAvatar id="@a:example.org" name="Alice" size={96} loading={false} />,
); );
expect(container.querySelector(".loading")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Loading")).toBe(null);
}); });
}); });

View File

@@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.
import { type FC } from "react"; import { type FC } from "react";
import { InlineSpinner } from "@vector-im/compound-web"; import { InlineSpinner } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import styles from "./TileAvatar.module.css"; import styles from "./TileAvatar.module.css";
import { Avatar, type Props as AvatarProps } from "../Avatar"; import { Avatar, type Props as AvatarProps } from "../Avatar";
@@ -17,11 +18,12 @@ interface Props extends AvatarProps {
} }
export const TileAvatar: FC<Props> = ({ size, loading, ...props }) => { export const TileAvatar: FC<Props> = ({ size, loading, ...props }) => {
const { t } = useTranslation();
return ( return (
<div> <div>
{loading && ( {loading && (
<div className={styles.loading}> <div className={styles.loading}>
<InlineSpinner size={size / 3} /> <InlineSpinner size={size / 3} aria-label={t("common.loading")} />
</div> </div>
)} )}
<Avatar size={size} {...props} /> <Avatar size={size} {...props} />