diff --git a/package.json b/package.json index 46a86b45..6d5fca42 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@sentry/react": "^8.0.0", "@sentry/vite-plugin": "^2.0.0", "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", diff --git a/src/Avatar.tsx b/src/Avatar.tsx index a0ae1483..29ab5236 100644 --- a/src/Avatar.tsx +++ b/src/Avatar.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { useMemo, FC } from "react"; +import { useMemo, FC, CSSProperties } from "react"; import { Avatar as CompoundAvatar } from "@vector-im/compound-web"; import { getAvatarUrl } from "./utils/matrix"; @@ -33,6 +33,7 @@ interface Props { className?: string; src?: string; size?: Size | number; + style?: CSSProperties; } export const Avatar: FC = ({ @@ -41,6 +42,8 @@ export const Avatar: FC = ({ name, src, size = Size.MD, + style, + ...props }) => { const { client } = useClient(); @@ -64,6 +67,8 @@ export const Avatar: FC = ({ name={name} size={`${sizePx}px`} src={resolvedSrc} + style={style} + {...props} /> ); }; diff --git a/src/tile/MediaView.module.css b/src/tile/MediaView.module.css index 3ed6c83d..4594c284 100644 --- a/src/tile/MediaView.module.css +++ b/src/tile/MediaView.module.css @@ -34,10 +34,6 @@ Please see LICENSE in the repository root for full details. object-fit: contain; } -.media.videoMuted video { - display: none; -} - .bg { background-color: var(--cpd-color-bg-subtle-secondary); inline-size: 100%; @@ -47,7 +43,6 @@ Please see LICENSE in the repository root for full details. } .avatar { - display: none; position: absolute; top: 50%; left: 50%; @@ -55,10 +50,6 @@ Please see LICENSE in the repository root for full details. pointer-events: none; } -.media.videoMuted .avatar { - display: initial; -} - /* CSS makes us put a condition here, even though all we want to do is unconditionally select the container so we can use cqmin units */ @container mediaView (width > 0) { diff --git a/src/tile/MediaView.test.tsx b/src/tile/MediaView.test.tsx new file mode 100644 index 00000000..238ffdd1 --- /dev/null +++ b/src/tile/MediaView.test.tsx @@ -0,0 +1,117 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { describe, expect, test } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { axe } from "vitest-axe"; +import { TooltipProvider } from "@vector-im/compound-web"; +import { + TrackReference, + TrackReferencePlaceholder, +} from "@livekit/components-core"; +import { Track, TrackPublication } from "livekit-client"; +import { type ComponentProps } from "react"; + +import { MediaView } from "./MediaView"; +import { EncryptionStatus } from "../state/MediaViewModel"; +import { mockLocalParticipant } from "../utils/test"; + +describe("MediaView", () => { + const participant = mockLocalParticipant({}); + const trackReferencePlaceholder: TrackReferencePlaceholder = { + participant, + source: Track.Source.Camera, + }; + const trackReference: TrackReference = { + ...trackReferencePlaceholder, + publication: new TrackPublication(Track.Kind.Video, "id", "name"), + }; + + const baseProps: ComponentProps = { + displayName: "some name", + videoEnabled: true, + videoFit: "contain", + targetWidth: 300, + targetHeight: 200, + encryptionStatus: EncryptionStatus.Connecting, + mirror: false, + unencryptedWarning: false, + video: trackReference, + member: undefined, + }; + + test("is accessible", async () => { + const { container } = render(); + expect(await axe(container)).toHaveNoViolations(); + }); + + describe("placeholder track", () => { + test("neither video nor avatar are shown", () => { + render(); + expect(screen.queryByTestId("video")).toBeNull(); + expect(screen.queryAllByRole("img", { name: "some name" }).length).toBe( + 0, + ); + }); + }); + + describe("name tag", () => { + test("is shown with name", () => { + render(); + expect(screen.getByTestId("name_tag")).toHaveTextContent("Bob"); + }); + }); + + describe("unencryptedWarning", () => { + test("is shown and accessible", async () => { + const { container } = render( + + + , + ); + expect(await axe(container)).toHaveNoViolations(); + expect( + screen.getByRole("img", { name: "common.unencrypted" }), + ).toBeTruthy(); + }); + + test("is not shown", () => { + render( + + + , + ); + expect( + screen.queryAllByRole("img", { name: "common.unencrypted" }).length, + ).toBe(0); + }); + }); + + describe("videoEnabled", () => { + test("just video is visible", () => { + render( + + + , + ); + expect(screen.getByTestId("video")).toBeVisible(); + expect(screen.queryAllByRole("img", { name: "some name" }).length).toBe( + 0, + ); + }); + + test("just avatar is visible", () => { + render( + + + , + ); + expect(screen.getByRole("img", { name: "some name" })).toBeVisible(); + expect(screen.getByTestId("video")).not.toBeVisible(); + }); + }); +}); diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index 9707e707..4e69bf41 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -76,7 +76,6 @@ export const MediaView = forwardRef( ( size={avatarSize} src={member?.getMxcAvatarUrl()} className={styles.avatar} + style={{ display: videoEnabled ? "none" : "initial" }} /> {video.publication !== undefined && ( ( // There's no reason for this to be focusable tabIndex={-1} disablePictureInPicture + style={{ display: videoEnabled ? "block" : "none" }} + data-testid="video" /> )} @@ -133,7 +135,13 @@ export const MediaView = forwardRef( )*/}
{nameTagLeadingIcon} - + {displayName} {unencryptedWarning && ( @@ -146,6 +154,8 @@ export const MediaView = forwardRef( width={20} height={20} className={styles.errorIcon} + role="img" + aria-label={t("common.unencrypted")} /> )} diff --git a/src/vitest.setup.ts b/src/vitest.setup.ts index 421ec663..38b8704e 100644 --- a/src/vitest.setup.ts +++ b/src/vitest.setup.ts @@ -15,6 +15,7 @@ import { afterEach } from "vitest"; import { cleanup } from "@testing-library/react"; import "vitest-axe/extend-expect"; import { logger } from "matrix-js-sdk/src/logger"; +import "@testing-library/jest-dom/vitest"; import EN_GB from "../locales/en-GB/app.json"; import { Config } from "./config/Config"; diff --git a/yarn.lock b/yarn.lock index 96ee1ae4..06f07543 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,11 @@ resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71" integrity sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q== +"@adobe/css-tools@^4.4.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.1.tgz#2447a230bfe072c1659e6815129c03cf170710e3" + integrity sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ== + "@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -2914,6 +2919,19 @@ lz-string "^1.5.0" pretty-format "^27.0.2" +"@testing-library/jest-dom@^6.6.3": + version "6.6.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" + integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" + redent "^3.0.0" + "@testing-library/react-hooks@^8.0.1": version "8.0.1" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" @@ -3527,7 +3545,7 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" -aria-query@^5.3.2: +aria-query@^5.0.0, aria-query@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== @@ -3928,6 +3946,14 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" @@ -4206,6 +4232,11 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssdb@^8.2.1: version "8.2.1" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.1.tgz#62a5d9a41e2c86f1d7c35981098fc5ce47c5766c" @@ -4400,6 +4431,11 @@ dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -6118,6 +6154,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loglevel@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" @@ -7269,6 +7310,14 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reflect.getprototypeof@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859"