mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-08 20:09:19 +00:00
Add tests to make sure we always have one settings button.
This commit is contained in:
@@ -22,7 +22,7 @@ import {
|
|||||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
import inCallViewStyles from "../components/InCallFooter.module.css";
|
import inCallFooterStyles from "../components/InCallFooter.module.css";
|
||||||
import { platform } from "../Platform";
|
import { platform } from "../Platform";
|
||||||
|
|
||||||
interface MicButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface MicButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
@@ -177,9 +177,9 @@ export const SettingsButton: FC<SettingsButtonProps> = ({
|
|||||||
<Tooltip label={t("common.settings")}>
|
<Tooltip label={t("common.settings")}>
|
||||||
<CpdButton
|
<CpdButton
|
||||||
className={classNames(className, {
|
className={classNames(className, {
|
||||||
[inCallViewStyles.settingsOnlyShowWide]:
|
[inCallFooterStyles.settingsOnlyShowWide]:
|
||||||
showForScreenWidth === "wide",
|
showForScreenWidth === "wide",
|
||||||
[inCallViewStyles.settingsOnlyShowNarrow]:
|
[inCallFooterStyles.settingsOnlyShowNarrow]:
|
||||||
showForScreenWidth === "narrow",
|
showForScreenWidth === "narrow",
|
||||||
})}
|
})}
|
||||||
iconOnly
|
iconOnly
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface InCallFooterProps {
|
|||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
hideSettingsButton: boolean;
|
hideSettingsButton: boolean;
|
||||||
hideLogo: boolean;
|
hideLogo: boolean;
|
||||||
|
/** Pip controls buttonSize and hides: settings button, layout switcher and logo */
|
||||||
asPip: boolean;
|
asPip: boolean;
|
||||||
gridMode: GridMode;
|
gridMode: GridMode;
|
||||||
setGridMode: (mode: GridMode) => void;
|
setGridMode: (mode: GridMode) => void;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { render, type RenderResult } from "@testing-library/react";
|
import { render, type RenderResult } from "@testing-library/react";
|
||||||
import { type LocalParticipant } from "livekit-client";
|
import { type LocalParticipant } from "livekit-client";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter, MemoryRouter } from "react-router-dom";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
@@ -34,13 +34,17 @@ import {
|
|||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||||
|
import { type CallViewModelOptions } from "../state/CallViewModel/CallViewModel";
|
||||||
import { alice, local } from "../utils/test-fixtures";
|
import { alice, local } from "../utils/test-fixtures";
|
||||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||||
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer";
|
||||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||||
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
import { type MediaDevices as ECMediaDevices } from "../state/MediaDevices";
|
||||||
|
import { constant } from "../state/Behavior";
|
||||||
|
import { AppBar } from "../AppBar";
|
||||||
import { initializeWidget } from "../widget";
|
import { initializeWidget } from "../widget";
|
||||||
|
import inCallFooterStyles from "../components/InCallFooter.module.css";
|
||||||
|
|
||||||
initializeWidget();
|
initializeWidget();
|
||||||
vi.hoisted(
|
vi.hoisted(
|
||||||
@@ -96,6 +100,11 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
interface CreateInCallViewArgs {
|
interface CreateInCallViewArgs {
|
||||||
mediaDevices?: ECMediaDevices;
|
mediaDevices?: ECMediaDevices;
|
||||||
|
callViewModelOptions?: Partial<CallViewModelOptions>;
|
||||||
|
/** If set, uses a MemoryRouter with this as the initial entry instead of BrowserRouter */
|
||||||
|
initialRoute?: string;
|
||||||
|
/** If true, wraps the rendered tree in an AppBar provider */
|
||||||
|
withAppBar?: boolean;
|
||||||
}
|
}
|
||||||
function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
|
function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
|
||||||
rtcSession: MockRTCSession;
|
rtcSession: MockRTCSession;
|
||||||
@@ -112,47 +121,60 @@ function createInCallView(args: CreateInCallViewArgs = {}): RenderResult & {
|
|||||||
const { vm, rtcSession } = getBasicCallViewModelEnvironment(
|
const { vm, rtcSession } = getBasicCallViewModelEnvironment(
|
||||||
[local, alice],
|
[local, alice],
|
||||||
undefined,
|
undefined,
|
||||||
{},
|
args.callViewModelOptions ?? {},
|
||||||
args.mediaDevices,
|
args.mediaDevices,
|
||||||
);
|
);
|
||||||
|
|
||||||
rtcSession.joined = true;
|
rtcSession.joined = true;
|
||||||
const room = rtcSession.room;
|
const room = rtcSession.room;
|
||||||
const client = room.client;
|
const client = room.client;
|
||||||
|
|
||||||
|
const Router = args.initialRoute
|
||||||
|
? ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<MemoryRouter initialEntries={[args.initialRoute!]}>
|
||||||
|
{children}
|
||||||
|
</MemoryRouter>
|
||||||
|
)
|
||||||
|
: BrowserRouter;
|
||||||
|
|
||||||
|
const inCallView = (
|
||||||
|
<InCallView
|
||||||
|
client={client}
|
||||||
|
rtcSession={rtcSession.asMockedSession()}
|
||||||
|
muteStates={muteState}
|
||||||
|
vm={vm}
|
||||||
|
matrixInfo={{
|
||||||
|
userId: "",
|
||||||
|
displayName: "",
|
||||||
|
avatarUrl: "",
|
||||||
|
roomId: "",
|
||||||
|
roomName: "",
|
||||||
|
roomAlias: null,
|
||||||
|
roomAvatar: null,
|
||||||
|
e2eeSystem: {
|
||||||
|
kind: E2eeType.NONE,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
matrixRoom={room}
|
||||||
|
onShareClick={null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = args.withAppBar ? <AppBar>{inCallView}</AppBar> : inCallView;
|
||||||
|
|
||||||
const renderResult = render(
|
const renderResult = render(
|
||||||
<BrowserRouter>
|
<Router>
|
||||||
<MediaDevicesContext value={args.mediaDevices ?? mockMediaDevices({})}>
|
<MediaDevicesContext value={args.mediaDevices ?? mockMediaDevices({})}>
|
||||||
<ReactionsSenderProvider
|
<ReactionsSenderProvider
|
||||||
vm={vm}
|
vm={vm}
|
||||||
rtcSession={rtcSession.asMockedSession()}
|
rtcSession={rtcSession.asMockedSession()}
|
||||||
>
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<RoomContext value={livekitRoom}>
|
<RoomContext value={livekitRoom}>{content}</RoomContext>
|
||||||
<InCallView
|
|
||||||
client={client}
|
|
||||||
rtcSession={rtcSession.asMockedSession()}
|
|
||||||
muteStates={muteState}
|
|
||||||
vm={vm}
|
|
||||||
matrixInfo={{
|
|
||||||
userId: "",
|
|
||||||
displayName: "",
|
|
||||||
avatarUrl: "",
|
|
||||||
roomId: "",
|
|
||||||
roomName: "",
|
|
||||||
roomAlias: null,
|
|
||||||
roomAvatar: null,
|
|
||||||
e2eeSystem: {
|
|
||||||
kind: E2eeType.NONE,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
matrixRoom={room}
|
|
||||||
onShareClick={null}
|
|
||||||
/>
|
|
||||||
</RoomContext>
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ReactionsSenderProvider>
|
</ReactionsSenderProvider>
|
||||||
</MediaDevicesContext>
|
</MediaDevicesContext>
|
||||||
</BrowserRouter>,
|
</Router>,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...renderResult,
|
...renderResult,
|
||||||
@@ -167,6 +189,53 @@ describe("InCallView", () => {
|
|||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("settings button with AppBar header", () => {
|
||||||
|
it("is accessible when showHeader is false", () => {
|
||||||
|
// windowSize with height <= 600 results in "flat" windowMode,
|
||||||
|
// which means showHeader$ emits false.
|
||||||
|
const { getAllByRole } = createInCallView({
|
||||||
|
initialRoute: "/?header=app_bar",
|
||||||
|
withAppBar: true,
|
||||||
|
callViewModelOptions: {
|
||||||
|
// Set windowMode$ to "flat" (height <= 600)
|
||||||
|
windowSize$: constant({ width: 1000, height: 500 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// When showHeader is false, hideSettingsButton is false,
|
||||||
|
// so the settings button is visible in the footer.
|
||||||
|
const settingsBtns = getAllByRole("button", { name: "Settings" });
|
||||||
|
expect(settingsBtns.length).toBe(2);
|
||||||
|
const [btnA, btnB] = settingsBtns;
|
||||||
|
// here we check for two settings buttons because there are two buttons in the bottom bar. One for the
|
||||||
|
// the narrow layout and another one for the wide layout.
|
||||||
|
// Their visibility uses @media css queries, which cannot be tested in JSDOM,
|
||||||
|
// but we can at least check that both buttons are rendered and have the correct classes.
|
||||||
|
expect(btnA).toBeInTheDocument();
|
||||||
|
expect(btnA).toHaveClass(inCallFooterStyles.settingsOnlyShowWide);
|
||||||
|
expect(btnB).toBeInTheDocument();
|
||||||
|
expect(btnB).toHaveClass(inCallFooterStyles.settingsOnlyShowNarrow);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is accessible when showHeader is true", () => {
|
||||||
|
// windowSize with height > 600 and width > 600 results in "normal" windowMode,
|
||||||
|
// which means showHeader$ emits true.
|
||||||
|
const { getAllByRole } = createInCallView({
|
||||||
|
initialRoute: "/?header=app_bar",
|
||||||
|
withAppBar: true,
|
||||||
|
callViewModelOptions: {
|
||||||
|
// Set windowMode$ to "normal" (height >= 600)
|
||||||
|
windowSize$: constant({ width: 1000, height: 800 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// When showHeader is true and headerStyle is AppBar,
|
||||||
|
// hideSettingsButton is true in the footer, but the settings
|
||||||
|
// button is rendered in the AppBar via useAppBarSecondaryButton.
|
||||||
|
const settingsBtns = getAllByRole("button", { name: "Settings" });
|
||||||
|
expect(settingsBtns.length).toBe(1);
|
||||||
|
|
||||||
|
expect(settingsBtns[0]).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
describe("audioOutputSwitcher", () => {
|
describe("audioOutputSwitcher", () => {
|
||||||
it("is visible and can be clicked", async () => {
|
it("is visible and can be clicked", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|||||||
@@ -360,7 +360,12 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
let header: ReactNode = null;
|
let header: ReactNode = null;
|
||||||
if (showHeader) {
|
if (showHeader) {
|
||||||
switch (headerStyle) {
|
switch (headerStyle) {
|
||||||
case "none":
|
case HeaderStyle.AppBar: {
|
||||||
|
// dont build a header here. The AppBar will take care of it.
|
||||||
|
header = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case HeaderStyle.None:
|
||||||
// Cosmetic header to fill out space while still affecting the bounds of
|
// Cosmetic header to fill out space while still affecting the bounds of
|
||||||
// the grid
|
// the grid
|
||||||
header = (
|
header = (
|
||||||
@@ -370,7 +375,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "standard":
|
case HeaderStyle.Standard:
|
||||||
header = (
|
header = (
|
||||||
<Header
|
<Header
|
||||||
className={styles.header}
|
className={styles.header}
|
||||||
@@ -565,7 +570,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
// Hide the logo for both embedded solutions. mobile: HeaderStyle.AppBar and desktop: HeaderStyle.None.
|
// Hide the logo for both embedded solutions. mobile: HeaderStyle.AppBar and desktop: HeaderStyle.None.
|
||||||
hideLogo={headerStyle !== HeaderStyle.Standard}
|
hideLogo={headerStyle !== HeaderStyle.Standard}
|
||||||
hideSettingsButton={headerStyle === HeaderStyle.AppBar}
|
// Only hide the settings button if we have an AppBar header and we are showing the header
|
||||||
|
hideSettingsButton={headerStyle === HeaderStyle.AppBar && showHeader}
|
||||||
asPip={layout.type === "pip"}
|
asPip={layout.type === "pip"}
|
||||||
gridMode={gridMode}
|
gridMode={gridMode}
|
||||||
setGridMode={setGridMode}
|
setGridMode={setGridMode}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_8_"
|
aria-labelledby="_r_8_"
|
||||||
class="_button_13vu4_8 settingForBottomLeftCorner _has-icon_13vu4_60 _icon-only_13vu4_53"
|
class="_button_13vu4_8 settingsOnlyShowWide _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||||
data-kind="tertiary"
|
data-kind="tertiary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -298,7 +298,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-labelledby="_r_d_"
|
aria-labelledby="_r_d_"
|
||||||
class="_button_13vu4_8 settingForButtonsBar _has-icon_13vu4_60 _icon-only_13vu4_53"
|
class="_button_13vu4_8 settingsOnlyShowNarrow _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||||
data-kind="secondary"
|
data-kind="secondary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user