2024-09-06 18:31:07 -04:00
|
|
|
/*
|
2024-09-10 17:46:40 -04:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2024-09-06 18:31:07 -04:00
|
|
|
|
2025-02-18 17:59:58 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-10 17:46:40 -04:00
|
|
|
Please see LICENSE in the repository root for full details.
|
2024-09-06 18:31:07 -04:00
|
|
|
*/
|
|
|
|
|
|
2024-09-10 17:35:50 -04:00
|
|
|
import { test, expect, vi } from "vitest";
|
2026-03-16 13:12:49 +01:00
|
|
|
import { act, isInaccessible, render, screen } from "@testing-library/react";
|
2024-09-06 18:31:07 -04:00
|
|
|
import { axe } from "vitest-axe";
|
2024-09-10 17:35:50 -04:00
|
|
|
import userEvent from "@testing-library/user-event";
|
2026-03-04 05:07:41 -06:00
|
|
|
import { TooltipProvider } from "@vector-im/compound-web";
|
2026-03-16 13:12:49 +01:00
|
|
|
import { BehaviorSubject } from "rxjs";
|
2024-09-06 18:31:07 -04:00
|
|
|
|
|
|
|
|
import { SpotlightTile } from "./SpotlightTile";
|
2024-12-06 12:28:37 +01:00
|
|
|
import {
|
2025-06-12 19:16:37 -04:00
|
|
|
mockLocalParticipant,
|
|
|
|
|
mockMediaDevices,
|
2024-12-06 12:28:37 +01:00
|
|
|
mockRtcMembership,
|
2026-02-25 14:02:59 +01:00
|
|
|
mockLocalMedia,
|
|
|
|
|
mockRemoteMedia,
|
2025-12-10 17:16:38 -05:00
|
|
|
mockRemoteParticipant,
|
2026-03-04 05:07:41 -06:00
|
|
|
mockRemoteScreenShare,
|
2024-12-06 12:28:37 +01:00
|
|
|
} from "../utils/test";
|
2024-11-06 04:36:48 -05:00
|
|
|
import { SpotlightTileViewModel } from "../state/TileViewModel";
|
2025-06-18 18:33:35 -04:00
|
|
|
import { constant } from "../state/Behavior";
|
2026-03-16 13:12:49 +01:00
|
|
|
import {
|
|
|
|
|
createRingingMedia,
|
|
|
|
|
type RingingMediaViewModel,
|
|
|
|
|
} from "../state/media/RingingMediaViewModel";
|
|
|
|
|
import { type MuteStates } from "../state/MuteStates";
|
2024-09-06 18:31:07 -04:00
|
|
|
|
|
|
|
|
global.IntersectionObserver = class MockIntersectionObserver {
|
|
|
|
|
public observe(): void {}
|
|
|
|
|
public unobserve(): void {}
|
|
|
|
|
} as unknown as typeof IntersectionObserver;
|
|
|
|
|
|
|
|
|
|
test("SpotlightTile is accessible", async () => {
|
2026-02-25 14:02:59 +01:00
|
|
|
const vm1 = mockRemoteMedia(
|
2024-12-06 12:28:37 +01:00
|
|
|
mockRtcMembership("@alice:example.org", "AAAA"),
|
2024-09-06 18:31:07 -04:00
|
|
|
{
|
|
|
|
|
rawDisplayName: "Alice",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://adfsg",
|
|
|
|
|
},
|
2025-12-10 17:16:38 -05:00
|
|
|
mockRemoteParticipant({}),
|
2025-10-17 12:34:06 -04:00
|
|
|
);
|
2024-09-10 17:35:50 -04:00
|
|
|
|
2026-02-25 14:02:59 +01:00
|
|
|
const vm2 = mockLocalMedia(
|
2025-10-17 12:34:06 -04:00
|
|
|
mockRtcMembership("@bob:example.org", "BBBB"),
|
|
|
|
|
{
|
|
|
|
|
rawDisplayName: "Bob",
|
|
|
|
|
getMxcAvatarUrl: () => "mxc://dlskf",
|
2024-09-06 18:31:07 -04:00
|
|
|
},
|
2025-10-17 12:34:06 -04:00
|
|
|
mockLocalParticipant({}),
|
|
|
|
|
mockMediaDevices({}),
|
2024-09-06 18:31:07 -04:00
|
|
|
);
|
2025-10-17 12:34:06 -04:00
|
|
|
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const toggleExpanded = vi.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<SpotlightTile
|
|
|
|
|
vm={new SpotlightTileViewModel(constant([vm1, vm2]), constant(false))}
|
|
|
|
|
targetWidth={300}
|
|
|
|
|
targetHeight={200}
|
|
|
|
|
expanded={false}
|
|
|
|
|
onToggleExpanded={toggleExpanded}
|
|
|
|
|
showIndicators
|
|
|
|
|
focusable={true}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(await axe(container)).toHaveNoViolations();
|
|
|
|
|
// Alice should be in the spotlight, with her name and avatar on the
|
|
|
|
|
// first page
|
|
|
|
|
screen.getByText("Alice");
|
|
|
|
|
const aliceAvatar = screen.getByRole("img");
|
|
|
|
|
expect(screen.queryByRole("button", { name: "common.back" })).toBe(null);
|
|
|
|
|
// Bob should be out of the spotlight, and therefore invisible
|
|
|
|
|
expect(isInaccessible(screen.getByText("Bob"))).toBe(true);
|
|
|
|
|
// Now navigate to Bob
|
|
|
|
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
|
|
|
|
screen.getByText("Bob");
|
|
|
|
|
expect(screen.getByRole("img")).not.toBe(aliceAvatar);
|
|
|
|
|
expect(isInaccessible(screen.getByText("Alice"))).toBe(true);
|
|
|
|
|
// Can toggle whether the tile is expanded
|
|
|
|
|
await user.click(screen.getByRole("button", { name: "Expand" }));
|
|
|
|
|
expect(toggleExpanded).toHaveBeenCalled();
|
2024-09-06 18:31:07 -04:00
|
|
|
});
|
2026-03-04 05:07:41 -06:00
|
|
|
|
|
|
|
|
test("Screen share volume UI is shown when screen share has audio", async () => {
|
|
|
|
|
const vm = mockRemoteScreenShare(
|
|
|
|
|
mockRtcMembership("@alice:example.org", "AAAA"),
|
|
|
|
|
{},
|
|
|
|
|
mockRemoteParticipant({}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
vi.spyOn(vm, "audioEnabled$", "get").mockReturnValue(constant(true));
|
|
|
|
|
|
|
|
|
|
const toggleExpanded = vi.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<TooltipProvider>
|
|
|
|
|
<SpotlightTile
|
|
|
|
|
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
|
|
|
|
targetWidth={300}
|
|
|
|
|
targetHeight={200}
|
|
|
|
|
expanded={false}
|
|
|
|
|
onToggleExpanded={toggleExpanded}
|
|
|
|
|
showIndicators
|
|
|
|
|
focusable
|
|
|
|
|
/>
|
|
|
|
|
</TooltipProvider>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(await axe(container)).toHaveNoViolations();
|
|
|
|
|
|
|
|
|
|
// Volume menu button should exist
|
|
|
|
|
expect(screen.queryByRole("button", { name: /volume/i })).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("Screen share volume UI is hidden when screen share has no audio", async () => {
|
|
|
|
|
const vm = mockRemoteScreenShare(
|
|
|
|
|
mockRtcMembership("@alice:example.org", "AAAA"),
|
|
|
|
|
{},
|
|
|
|
|
mockRemoteParticipant({}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
vi.spyOn(vm, "audioEnabled$", "get").mockReturnValue(constant(false));
|
|
|
|
|
|
|
|
|
|
const toggleExpanded = vi.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<SpotlightTile
|
|
|
|
|
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
|
|
|
|
targetWidth={300}
|
|
|
|
|
targetHeight={200}
|
|
|
|
|
expanded={false}
|
|
|
|
|
onToggleExpanded={toggleExpanded}
|
|
|
|
|
showIndicators
|
|
|
|
|
focusable
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(await axe(container)).toHaveNoViolations();
|
|
|
|
|
|
|
|
|
|
// Volume menu button should not exist
|
|
|
|
|
expect(
|
|
|
|
|
screen.queryByRole("button", { name: /volume/i }),
|
|
|
|
|
).not.toBeInTheDocument();
|
|
|
|
|
});
|
2026-03-16 13:12:49 +01:00
|
|
|
|
|
|
|
|
test("SpotlightTile displays ringing media", async () => {
|
|
|
|
|
const pickupState$ = new BehaviorSubject<
|
|
|
|
|
RingingMediaViewModel["pickupState$"]["value"]
|
|
|
|
|
>("ringing");
|
|
|
|
|
const vm = createRingingMedia({
|
|
|
|
|
pickupState$,
|
|
|
|
|
muteStates: {
|
|
|
|
|
video: { enabled$: constant(false) },
|
|
|
|
|
} as unknown as MuteStates,
|
|
|
|
|
id: "test",
|
|
|
|
|
userId: "@alice:example.org",
|
|
|
|
|
displayName$: constant("Alice"),
|
|
|
|
|
mxcAvatarUrl$: constant(undefined),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const toggleExpanded = vi.fn();
|
|
|
|
|
const { container } = render(
|
|
|
|
|
<SpotlightTile
|
|
|
|
|
vm={new SpotlightTileViewModel(constant([vm]), constant(false))}
|
|
|
|
|
targetWidth={300}
|
|
|
|
|
targetHeight={200}
|
|
|
|
|
expanded={false}
|
|
|
|
|
onToggleExpanded={toggleExpanded}
|
|
|
|
|
showIndicators
|
|
|
|
|
focusable={true}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(await axe(container)).toHaveNoViolations();
|
|
|
|
|
// Alice should be in the spotlight with the right status
|
|
|
|
|
screen.getByText("Alice");
|
|
|
|
|
screen.getByText("Calling…");
|
|
|
|
|
|
|
|
|
|
// Now we time out ringing to Alice
|
|
|
|
|
act(() => pickupState$.next("timeout"));
|
|
|
|
|
screen.getByText("Call ended");
|
|
|
|
|
});
|