|
| 1 | +/* |
| 2 | +Copyright 2026 New Vector Ltd. |
| 3 | +
|
| 4 | +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial |
| 5 | +Please see LICENSE in the repository root for full details. |
| 6 | +*/ |
| 7 | + |
| 8 | +import { type JSX, type ReactNode } from "react"; |
| 9 | +import { describe, expect, test, vi } from "vitest"; |
| 10 | +import { render, screen } from "@testing-library/react"; |
| 11 | +import { TooltipProvider } from "@vector-im/compound-web"; |
| 12 | + |
| 13 | +import { CallParticipantRow } from "./CallParticipantRow"; |
| 14 | +import { type CallParticipant } from "./useCallParticipants"; |
| 15 | + |
| 16 | +// Mock the Avatar component to avoid MXC URL resolution side effects |
| 17 | +vi.mock("../Avatar", () => ({ |
| 18 | + Avatar: ({ |
| 19 | + id, |
| 20 | + name, |
| 21 | + size, |
| 22 | + }: { |
| 23 | + id: string; |
| 24 | + name: string; |
| 25 | + size: number; |
| 26 | + }): JSX.Element => ( |
| 27 | + <div data-testid={`avatar-${id}`} data-name={name} data-size={size} /> |
| 28 | + ), |
| 29 | + Size: { SM: "sm", MD: "md", LG: "lg", XL: "xl" }, |
| 30 | +})); |
| 31 | + |
| 32 | +function renderWithProviders(children: ReactNode): ReturnType<typeof render> { |
| 33 | + return render(<TooltipProvider>{children}</TooltipProvider>); |
| 34 | +} |
| 35 | + |
| 36 | +function makeParticipants(count: number): CallParticipant[] { |
| 37 | + return Array.from({ length: count }, (_, i) => ({ |
| 38 | + userId: `@user${i}:example.org`, |
| 39 | + displayName: `User ${i}`, |
| 40 | + avatarUrl: null, |
| 41 | + })); |
| 42 | +} |
| 43 | + |
| 44 | +describe("CallParticipantRow", () => { |
| 45 | + test("renders nothing when no participants", () => { |
| 46 | + const { container } = renderWithProviders( |
| 47 | + <CallParticipantRow participants={[]} />, |
| 48 | + ); |
| 49 | + expect(container.textContent).toBe(""); |
| 50 | + }); |
| 51 | + |
| 52 | + test("renders single participant", () => { |
| 53 | + const participants = makeParticipants(1); |
| 54 | + renderWithProviders(<CallParticipantRow participants={participants} />); |
| 55 | + |
| 56 | + expect(screen.getByText("User 0")).toBeInTheDocument(); |
| 57 | + expect(screen.getByTestId("avatar-@user0:example.org")).toBeInTheDocument(); |
| 58 | + }); |
| 59 | + |
| 60 | + test("renders multiple participants up to limit", () => { |
| 61 | + const participants = makeParticipants(5); |
| 62 | + renderWithProviders(<CallParticipantRow participants={participants} />); |
| 63 | + |
| 64 | + for (let i = 0; i < 5; i++) { |
| 65 | + expect(screen.getByText(`User ${i}`)).toBeInTheDocument(); |
| 66 | + } |
| 67 | + // No overflow |
| 68 | + expect(screen.queryByText(/\+\d+/)).not.toBeInTheDocument(); |
| 69 | + }); |
| 70 | + |
| 71 | + test("renders overflow when exceeding default limit", () => { |
| 72 | + const participants = makeParticipants(10); |
| 73 | + renderWithProviders(<CallParticipantRow participants={participants} />); |
| 74 | + |
| 75 | + // First 8 are visible |
| 76 | + for (let i = 0; i < 8; i++) { |
| 77 | + expect(screen.getByText(`User ${i}`)).toBeInTheDocument(); |
| 78 | + } |
| 79 | + // Users 8 and 9 are in overflow (only visible in tooltip, not rendered in DOM) |
| 80 | + expect(screen.queryByText("User 8")).not.toBeInTheDocument(); |
| 81 | + expect(screen.queryByText("User 9")).not.toBeInTheDocument(); |
| 82 | + // Overflow count shown |
| 83 | + expect(screen.getByText("+2")).toBeInTheDocument(); |
| 84 | + }); |
| 85 | + |
| 86 | + test("renders overflow with custom display limit", () => { |
| 87 | + const participants = makeParticipants(5); |
| 88 | + renderWithProviders( |
| 89 | + <CallParticipantRow participants={participants} displayLimit={3} />, |
| 90 | + ); |
| 91 | + |
| 92 | + // First 3 are visible |
| 93 | + for (let i = 0; i < 3; i++) { |
| 94 | + expect(screen.getByText(`User ${i}`)).toBeInTheDocument(); |
| 95 | + } |
| 96 | + // 4th and 5th in overflow |
| 97 | + expect(screen.queryByText("User 3")).not.toBeInTheDocument(); |
| 98 | + expect(screen.queryByText("User 4")).not.toBeInTheDocument(); |
| 99 | + expect(screen.getByText("+2")).toBeInTheDocument(); |
| 100 | + }); |
| 101 | + |
| 102 | + test("overflow element shows ellipsis", () => { |
| 103 | + const participants = makeParticipants(10); |
| 104 | + renderWithProviders(<CallParticipantRow participants={participants} />); |
| 105 | + |
| 106 | + expect(screen.getByText("…")).toBeInTheDocument(); |
| 107 | + }); |
| 108 | + |
| 109 | + test("has accessible list role", () => { |
| 110 | + const participants = makeParticipants(3); |
| 111 | + renderWithProviders(<CallParticipantRow participants={participants} />); |
| 112 | + |
| 113 | + expect(screen.getByRole("list")).toBeInTheDocument(); |
| 114 | + expect(screen.getAllByRole("listitem")).toHaveLength(3); |
| 115 | + }); |
| 116 | + |
| 117 | + test("overflow adds an additional listitem", () => { |
| 118 | + const participants = makeParticipants(10); |
| 119 | + renderWithProviders(<CallParticipantRow participants={participants} />); |
| 120 | + |
| 121 | + // 8 visible + 1 overflow = 9 listitems |
| 122 | + expect(screen.getAllByRole("listitem")).toHaveLength(9); |
| 123 | + }); |
| 124 | + |
| 125 | + test("assigns correct screen reader label", () => { |
| 126 | + const participants: CallParticipant[] = [ |
| 127 | + { userId: "@alice:example.org", displayName: "Alice", avatarUrl: null }, |
| 128 | + { userId: "@bob:example.org", displayName: "Bob", avatarUrl: null }, |
| 129 | + ]; |
| 130 | + renderWithProviders(<CallParticipantRow participants={participants} />); |
| 131 | + |
| 132 | + const list = screen.getByRole("list"); |
| 133 | + expect(list.getAttribute("aria-label")).toContain("Alice"); |
| 134 | + expect(list.getAttribute("aria-label")).toContain("Bob"); |
| 135 | + }); |
| 136 | +}); |
0 commit comments