Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test a few React components #2614

Merged
merged 11 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"vite": "^5.0.0",
"vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^4.0.0",
"vitest": "^2.0.0"
"vitest": "^2.0.0",
"vitest-axe": "^1.0.0-pre.3"
}
}
39 changes: 39 additions & 0 deletions src/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright 2024 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { expect, test } from "vitest";
import { render, screen } from "@testing-library/react";
import { axe } from "vitest-axe";
import { TooltipProvider } from "@vector-im/compound-web";

import { RoomHeaderInfo } from "./Header";

test("RoomHeaderInfo is accessible", async () => {
const { container } = render(
<TooltipProvider>
<RoomHeaderInfo
id="!a:example.org"
name="Mission Control"
avatarUrl=""
encrypted
participantCount={11}
/>
</TooltipProvider>,
);
expect(await axe(container)).toHaveNoViolations();
// Check that the room name acts as a heading
screen.getByRole("heading", { name: "Mission Control" });
});
3 changes: 2 additions & 1 deletion src/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
styles.drawer,
{ [styles.tabbed]: tabbed },
)}
aria-describedby={undefined}

Check warning on line 101 in src/Modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/Modal.tsx#L101

Added line #L101 was not covered by tests
robintown marked this conversation as resolved.
Show resolved Hide resolved
{...rest}
>
<div className={styles.content}>
Expand All @@ -120,7 +121,7 @@
<DialogOverlay
className={classNames(overlayStyles.bg, overlayStyles.animate)}
/>
<DialogContent asChild {...rest}>
<DialogContent asChild aria-describedby={undefined} {...rest}>
<Glass
className={classNames(
className,
Expand Down
14 changes: 3 additions & 11 deletions src/Toast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,12 @@ limitations under the License.
*/

import { describe, expect, test, vi } from "vitest";
import { render, configure } from "@testing-library/react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { Toast } from "../src/Toast";
import { withFakeTimers } from "./utils/test";

configure({
defaultHidden: true,
});

// Test Explanation:
// This test the toast. We need to use { document: window.document } because the toast listens
// for user input on `window`.
describe("Toast", () => {
test("renders", () => {
const { queryByRole } = render(
Expand All @@ -45,7 +38,7 @@ describe("Toast", () => {
});

test("dismisses when Esc is pressed", async () => {
const user = userEvent.setup({ document: window.document });
const user = userEvent.setup();
const onDismiss = vi.fn();
render(
<Toast open={true} onDismiss={onDismiss}>
Expand All @@ -59,15 +52,14 @@ describe("Toast", () => {
test("dismisses when background is clicked", async () => {
const user = userEvent.setup();
const onDismiss = vi.fn();
const { getByRole, unmount } = render(
const { getByRole } = render(
<Toast open={true} onDismiss={onDismiss}>
Hello world!
</Toast>,
);
const background = getByRole("dialog").previousSibling! as Element;
await user.click(background);
expect(onDismiss).toHaveBeenCalled();
unmount();
});

test("dismisses itself after the specified timeout", () => {
Expand Down
1 change: 0 additions & 1 deletion src/room/EncryptionLock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export const EncryptionLock: FC<Props> = ({ encrypted }) => {
height={16}
className={styles.lock}
data-encrypted={encrypted}
aria-hidden
/>
</Tooltip>
);
Expand Down
44 changes: 44 additions & 0 deletions src/room/InviteModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
Copyright 2024 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { render, screen } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { Room } from "matrix-js-sdk/src/matrix";
import { axe } from "vitest-axe";
import { BrowserRouter } from "react-router-dom";
import userEvent from "@testing-library/user-event";

import { InviteModal } from "./InviteModal";

// Used by copy-to-clipboard
window.prompt = (): null => null;

test("InviteModal is accessible", async () => {
const user = userEvent.setup();
const room = {
roomId: "!a:example.org",
name: "Mission Control",
} as unknown as Room;
const onDismiss = vi.fn();
const { container } = render(
<InviteModal room={room} open={true} onDismiss={onDismiss} />,
{ wrapper: BrowserRouter },
);

expect(await axe(container)).toHaveNoViolations();
await user.click(screen.getByRole("button", { name: "action.copy_link" }));
expect(onDismiss).toBeCalled();
});
61 changes: 13 additions & 48 deletions src/state/MediaViewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,52 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { RoomMember } from "matrix-js-sdk/src/matrix";
import { expect, test, vi } from "vitest";
import { LocalParticipant, RemoteParticipant } from "livekit-client";

import {
LocalUserMediaViewModel,
RemoteUserMediaViewModel,
} from "./MediaViewModel";
import { withTestScheduler } from "../utils/test";
withLocalMedia,
withRemoteMedia,
withTestScheduler,
} from "../utils/test";

function withLocal(continuation: (vm: LocalUserMediaViewModel) => void): void {
const member = {} as unknown as RoomMember;
const vm = new LocalUserMediaViewModel(
"a",
member,
{} as unknown as LocalParticipant,
true,
);
try {
continuation(vm);
} finally {
vm.destroy();
}
}

function withRemote(
participant: Partial<RemoteParticipant>,
continuation: (vm: RemoteUserMediaViewModel) => void,
): void {
const member = {} as unknown as RoomMember;
const vm = new RemoteUserMediaViewModel(
"a",
member,
{ setVolume() {}, ...participant } as RemoteParticipant,
true,
);
try {
continuation(vm);
} finally {
vm.destroy();
}
}

test("set a participant's volume", () => {
test("set a participant's volume", async () => {
const setVolumeSpy = vi.fn();
withRemote({ setVolume: setVolumeSpy }, (vm) =>
await withRemoteMedia({}, { setVolume: setVolumeSpy }, async (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", {
a() {
Expand All @@ -72,9 +37,9 @@ test("set a participant's volume", () => {
);
});

test("mute and unmute a participant", () => {
test("mute and unmute a participant", async () => {
const setVolumeSpy = vi.fn();
withRemote({ setVolume: setVolumeSpy }, (vm) =>
await withRemoteMedia({}, { setVolume: setVolumeSpy }, async (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-abc|", {
a() {
Expand All @@ -99,8 +64,8 @@ test("mute and unmute a participant", () => {
);
});

test("toggle fit/contain for a participant's video", () => {
withRemote({}, (vm) =>
test("toggle fit/contain for a participant's video", async () => {
await withRemoteMedia({}, {}, async (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-ab|", {
a: () => vm.toggleFitContain(),
Expand All @@ -115,15 +80,15 @@ test("toggle fit/contain for a participant's video", () => {
);
});

test("local media remembers whether it should always be shown", () => {
withLocal((vm) =>
test("local media remembers whether it should always be shown", async () => {
await withLocalMedia(async (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm.setAlwaysShow(false) });
expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false });
}),
);
// Next local media should start out *not* always shown
withLocal((vm) =>
await withLocalMedia(async (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm.setAlwaysShow(true) });
expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true });
Expand Down
52 changes: 52 additions & 0 deletions src/tile/GridTile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2024 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { RemoteTrackPublication } from "livekit-client";
import { test, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { axe } from "vitest-axe";

import { GridTile } from "./GridTile";
import { withRemoteMedia } from "../utils/test";

test("GridTile is accessible", async () => {
await withRemoteMedia(
{
rawDisplayName: "Alice",
getMxcAvatarUrl: () => "mxc://adfsg",
},
{
setVolume() {},
getTrackPublication: () =>
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
},
async (vm) => {
const { container } = render(
<GridTile
vm={vm}
onOpenProfile={() => {}}
targetWidth={300}
targetHeight={200}
showVideo
showSpeakingIndicators
/>,
);
expect(await axe(container)).toHaveNoViolations();
// Name should be visible
screen.getByText("Alice");
},
);
});
1 change: 0 additions & 1 deletion src/tile/MediaView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
width={20}
height={20}
className={styles.errorIcon}
aria-hidden
/>
</Tooltip>
)}
Expand Down
57 changes: 57 additions & 0 deletions src/tile/SpotlightTile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Copyright 2024 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { RemoteTrackPublication } from "livekit-client";
import { test, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { axe } from "vitest-axe";

import { SpotlightTile } from "./SpotlightTile";
import { withRemoteMedia } from "../utils/test";

global.IntersectionObserver = class MockIntersectionObserver {
public observe(): void {}
public unobserve(): void {}
} as unknown as typeof IntersectionObserver;

test("SpotlightTile is accessible", async () => {
await withRemoteMedia(
{
rawDisplayName: "Alice",
getMxcAvatarUrl: () => "mxc://adfsg",
},
{
getTrackPublication: () =>
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
},
async (vm) => {
const { container } = render(
<SpotlightTile
vms={[vm]}
targetWidth={300}
targetHeight={200}
maximised={false}
expanded={false}
onToggleExpanded={() => {}}
showIndicators
/>,
);
expect(await axe(container)).toHaveNoViolations();
// Name should be visible
screen.getByText("Alice");
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a lot more expectations here:

  • avatar url
  • expanded
  • indicators
    ...

Copy link
Member Author

@robintown robintown Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I've added some more expectations now. I wasn't able to test the specific avatar URL, as that requires a full Matrix client setup, and the indicators are (intentionally, IIRC?) excluded from the accessibility tree. But, there were still some useful things to test, and I actually caught a couple more accessibility issues this way!

);
});
Loading