Request full device names when device switchers are open
This commit is contained in:
@@ -6,12 +6,25 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fn, userEvent, within, expect } from "storybook/test";
|
import { fn, userEvent, within, expect } from "storybook/test";
|
||||||
|
import { type JSX } from "react";
|
||||||
|
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
||||||
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
|
import { MediaDevices } from "../state/MediaDevices";
|
||||||
|
import { globalScope } from "../state/ObservableScope";
|
||||||
|
|
||||||
|
const mediaDevices = new MediaDevices(globalScope);
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
component: MediaMuteAndSwitchButton,
|
component: MediaMuteAndSwitchButton,
|
||||||
|
decorators: [
|
||||||
|
(Story): JSX.Element => (
|
||||||
|
<MediaDevicesContext value={mediaDevices}>
|
||||||
|
<Story />
|
||||||
|
</MediaDevicesContext>
|
||||||
|
),
|
||||||
|
],
|
||||||
} satisfies Meta<typeof MediaMuteAndSwitchButton>;
|
} satisfies Meta<typeof MediaMuteAndSwitchButton>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|||||||
@@ -8,14 +8,35 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { act, render, screen, type RenderResult } from "@testing-library/react";
|
import { act, render, screen, type RenderResult } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { type JSX, useState } from "react";
|
import { type JSX, useState, type ReactNode } from "react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
||||||
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
|
import { type MediaDevices } from "../state/MediaDevices";
|
||||||
|
|
||||||
|
interface RenderOptions {
|
||||||
|
requestDeviceNames: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComponent(
|
||||||
|
component: ReactNode,
|
||||||
|
{ requestDeviceNames = (): void => {} }: Partial<RenderOptions> = {},
|
||||||
|
): RenderResult {
|
||||||
|
return render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<MediaDevicesContext
|
||||||
|
value={{ requestDeviceNames } as unknown as MediaDevices}
|
||||||
|
>
|
||||||
|
{component}
|
||||||
|
</MediaDevicesContext>
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe("MediaMuteAndSwitchButton", () => {
|
describe("MediaMuteAndSwitchButton", () => {
|
||||||
test("renders", () => {
|
test("renders", () => {
|
||||||
const { container } = render(
|
const { container } = renderComponent(
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<MediaMuteAndSwitchButton title={"Switcher"} iconsAndLabels={"audio"} />
|
<MediaMuteAndSwitchButton title={"Switcher"} iconsAndLabels={"audio"} />
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
@@ -28,14 +49,12 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
type: "video" | "audio",
|
type: "video" | "audio",
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
): RenderResult => {
|
): RenderResult => {
|
||||||
return render(
|
return renderComponent(
|
||||||
<TooltipProvider>
|
<MediaMuteAndSwitchButton
|
||||||
<MediaMuteAndSwitchButton
|
title={"Switcher"}
|
||||||
title={"Switcher"}
|
iconsAndLabels={type}
|
||||||
iconsAndLabels={type}
|
enabled={enabled}
|
||||||
enabled={enabled}
|
/>,
|
||||||
/>
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const renderAudioEndabled = renderLabels("audio", true);
|
const renderAudioEndabled = renderLabels("audio", true);
|
||||||
@@ -60,15 +79,13 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
test("calls mute on mute press", async () => {
|
test("calls mute on mute press", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onMute = vi.fn();
|
const onMute = vi.fn();
|
||||||
const { getByRole } = render(
|
const { getByRole } = renderComponent(
|
||||||
<TooltipProvider>
|
<MediaMuteAndSwitchButton
|
||||||
<MediaMuteAndSwitchButton
|
title={"Switcher"}
|
||||||
title={"Switcher"}
|
onMuteClick={onMute}
|
||||||
onMuteClick={onMute}
|
iconsAndLabels="audio"
|
||||||
iconsAndLabels="audio"
|
enabled={true}
|
||||||
enabled={true}
|
/>,
|
||||||
/>
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(getByRole("switch", { name: "Mute microphone" }));
|
await user.click(getByRole("switch", { name: "Mute microphone" }));
|
||||||
@@ -76,10 +93,27 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
expect(onMute).toHaveBeenCalled();
|
expect(onMute).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("requests device names when opened", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const requestDeviceNames = vi.fn();
|
||||||
|
renderComponent(
|
||||||
|
<MediaMuteAndSwitchButton
|
||||||
|
title="Switcher"
|
||||||
|
iconsAndLabels="audio"
|
||||||
|
enabled
|
||||||
|
/>,
|
||||||
|
{ requestDeviceNames },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(requestDeviceNames).not.toHaveBeenCalled();
|
||||||
|
await user.click(screen.getByRole("button", { name: "Microphone" }));
|
||||||
|
expect(requestDeviceNames).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test("shows numbered devices correctly", async () => {
|
test("shows numbered devices correctly", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(
|
renderComponent(
|
||||||
<TooltipProvider>
|
<>
|
||||||
<MediaMuteAndSwitchButton
|
<MediaMuteAndSwitchButton
|
||||||
title="Switcher"
|
title="Switcher"
|
||||||
iconsAndLabels="audio"
|
iconsAndLabels="audio"
|
||||||
@@ -100,7 +134,7 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
]}
|
]}
|
||||||
selectedOption="cam1"
|
selectedOption="cam1"
|
||||||
/>
|
/>
|
||||||
</TooltipProvider>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Microphone" }));
|
await user.click(screen.getByRole("button", { name: "Microphone" }));
|
||||||
@@ -115,20 +149,18 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
test("calls select callback on menu click", async () => {
|
test("calls select callback on menu click", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const { getByRole } = render(
|
const { getByRole } = renderComponent(
|
||||||
<TooltipProvider>
|
<MediaMuteAndSwitchButton
|
||||||
<MediaMuteAndSwitchButton
|
title="Switcher"
|
||||||
title="Switcher"
|
iconsAndLabels="audio"
|
||||||
iconsAndLabels="audio"
|
enabled={true}
|
||||||
enabled={true}
|
options={[
|
||||||
options={[
|
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
||||||
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
||||||
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
]}
|
||||||
]}
|
selectedOption="mic1"
|
||||||
selectedOption="mic1"
|
onSelect={onSelect}
|
||||||
onSelect={onSelect}
|
/>,
|
||||||
/>
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(getByRole("button", { name: "Microphone" }));
|
await user.click(getByRole("button", { name: "Microphone" }));
|
||||||
@@ -139,20 +171,18 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
test("does not call select callback on already selected menu click", async () => {
|
test("does not call select callback on already selected menu click", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const { getByRole } = render(
|
const { getByRole } = renderComponent(
|
||||||
<TooltipProvider>
|
<MediaMuteAndSwitchButton
|
||||||
<MediaMuteAndSwitchButton
|
title="Switcher"
|
||||||
title="Switcher"
|
iconsAndLabels="audio"
|
||||||
iconsAndLabels="audio"
|
enabled={true}
|
||||||
enabled={true}
|
options={[
|
||||||
options={[
|
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
||||||
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
||||||
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
]}
|
||||||
]}
|
selectedOption="mic1"
|
||||||
selectedOption="mic1"
|
onSelect={onSelect}
|
||||||
onSelect={onSelect}
|
/>,
|
||||||
/>
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(getByRole("button", { name: "Microphone" }));
|
await user.click(getByRole("button", { name: "Microphone" }));
|
||||||
@@ -169,29 +199,27 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
function Wrapper(): JSX.Element {
|
function Wrapper(): JSX.Element {
|
||||||
const [selectedOption, setSelectedOption] = useState("mic1");
|
const [selectedOption, setSelectedOption] = useState("mic1");
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<MediaMuteAndSwitchButton
|
||||||
<MediaMuteAndSwitchButton
|
title="Switcher"
|
||||||
title="Switcher"
|
iconsAndLabels="audio"
|
||||||
iconsAndLabels="audio"
|
enabled={true}
|
||||||
enabled={true}
|
options={[
|
||||||
options={[
|
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
||||||
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
||||||
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
]}
|
||||||
]}
|
selectedOption={selectedOption}
|
||||||
selectedOption={selectedOption}
|
onSelect={(id) => {
|
||||||
onSelect={(id) => {
|
onSelectPressed();
|
||||||
onSelectPressed();
|
void promise.then(() => {
|
||||||
void promise.then(() => {
|
setSelectedOption(id);
|
||||||
setSelectedOption(id);
|
onOptionUpdated();
|
||||||
onOptionUpdated();
|
});
|
||||||
});
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getByRole } = render(<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("menuitem", { name: "Microphone 2" }));
|
||||||
@@ -224,16 +252,14 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
const onVideoBlurToggle = vi.fn();
|
const onVideoBlurToggle = vi.fn();
|
||||||
const { getByRole } = render(
|
const { getByRole } = renderComponent(
|
||||||
<TooltipProvider>
|
<MediaMuteAndSwitchButton
|
||||||
<MediaMuteAndSwitchButton
|
title="Switcher"
|
||||||
title="Switcher"
|
iconsAndLabels="video"
|
||||||
iconsAndLabels="video"
|
enabled={true}
|
||||||
enabled={true}
|
videoBlurToggleClick={onVideoBlurToggle}
|
||||||
videoBlurToggleClick={onVideoBlurToggle}
|
onSelect={onSelect}
|
||||||
onSelect={onSelect}
|
/>,
|
||||||
/>
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(getByRole("button", { name: "Camera" }));
|
await user.click(getByRole("button", { name: "Camera" }));
|
||||||
@@ -251,19 +277,17 @@ describe("MediaMuteAndSwitchButton", () => {
|
|||||||
|
|
||||||
test("renders check icon to mark the selected menu item", async () => {
|
test("renders check icon to mark the selected menu item", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const { getByRole } = render(
|
const { getByRole } = renderComponent(
|
||||||
<TooltipProvider>
|
<MediaMuteAndSwitchButton
|
||||||
<MediaMuteAndSwitchButton
|
title="Switcher"
|
||||||
title="Switcher"
|
iconsAndLabels="audio"
|
||||||
iconsAndLabels="audio"
|
enabled={true}
|
||||||
enabled={true}
|
options={[
|
||||||
options={[
|
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
||||||
{ label: { type: "name", name: "Microphone 1" }, id: "mic1" },
|
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
||||||
{ label: { type: "name", name: "Microphone 2" }, id: "mic2" },
|
]}
|
||||||
]}
|
selectedOption="mic2"
|
||||||
selectedOption="mic2"
|
/>,
|
||||||
/>
|
|
||||||
</TooltipProvider>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// open menu
|
// open menu
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ComponentType, useState, type FC } from "react";
|
import { type ComponentType, useState, type FC, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import styles from "./MediaMuteAndSwitchButton.module.css";
|
import styles from "./MediaMuteAndSwitchButton.module.css";
|
||||||
import { MicButton, VideoButton } from "../button";
|
import { MicButton, VideoButton } from "../button";
|
||||||
import { type DeviceLabel } from "../state/MediaDevices";
|
import { type DeviceLabel } from "../state/MediaDevices";
|
||||||
|
import { useMediaDevices } from "../MediaDevicesContext";
|
||||||
|
|
||||||
export interface MenuOptions {
|
export interface MenuOptions {
|
||||||
label: DeviceLabel;
|
label: DeviceLabel;
|
||||||
@@ -69,6 +70,12 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const devices = useMediaDevices();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (menuOpen) devices.requestDeviceNames();
|
||||||
|
}, [menuOpen, devices]);
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
let toggles: { label: string; enabled: boolean; id: string }[] = [];
|
let toggles: { label: string; enabled: boolean; id: string }[] = [];
|
||||||
switch (iconsAndLabels) {
|
switch (iconsAndLabels) {
|
||||||
@@ -128,6 +135,7 @@ export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
|||||||
t("settings.devices.microphone_numbered", { n });
|
t("settings.devices.microphone_numbered", { n });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
|||||||
Reference in New Issue
Block a user