Skip to content

Commit 938becc

Browse files
authored
Add tests for Modal (element-hq#2731)
* Add Modal tests. * fix type * apply review feedback * lint * remove act
1 parent 9bf40ed commit 938becc

File tree

4 files changed

+158
-4
lines changed

4 files changed

+158
-4
lines changed

src/Modal.test.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
Copyright 2024 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only
5+
Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
import { expect, test } from "vitest";
9+
import { render } from "@testing-library/react";
10+
import { ReactNode, useState } from "react";
11+
import { afterEach } from "node:test";
12+
import userEvent from "@testing-library/user-event";
13+
14+
import { Modal } from "./Modal";
15+
16+
const originalMatchMedia = window.matchMedia;
17+
afterEach(() => {
18+
window.matchMedia = originalMatchMedia;
19+
});
20+
21+
test("that nothing is rendered when the modal is closed", () => {
22+
const { queryByRole } = render(
23+
<Modal title="My modal" open={false}>
24+
<p>This is the content.</p>
25+
</Modal>,
26+
);
27+
expect(queryByRole("dialog")).toBeNull();
28+
});
29+
30+
test("the content is rendered when the modal is open", () => {
31+
const { queryByRole } = render(
32+
<Modal title="My modal" open={true}>
33+
<p>This is the content.</p>
34+
</Modal>,
35+
);
36+
expect(queryByRole("dialog")).toMatchSnapshot();
37+
});
38+
39+
test("the modal can be closed by clicking the close button", async () => {
40+
function ModalFn(): ReactNode {
41+
const [isOpen, setOpen] = useState(true);
42+
return (
43+
<Modal title="My modal" open={isOpen} onDismiss={() => setOpen(false)}>
44+
<p>This is the content.</p>
45+
</Modal>
46+
);
47+
}
48+
const user = userEvent.setup();
49+
const { queryByRole, getByRole } = render(<ModalFn />);
50+
await user.click(getByRole("button", { name: "action.close" }));
51+
expect(queryByRole("dialog")).toBeNull();
52+
});
53+
54+
test("the modal renders as a drawer in mobile viewports", () => {
55+
window.matchMedia = function (query): MediaQueryList {
56+
return {
57+
matches: query.includes("hover: none"),
58+
addEventListener(): MediaQueryList {
59+
return this as MediaQueryList;
60+
},
61+
removeEventListener(): MediaQueryList {
62+
return this as MediaQueryList;
63+
},
64+
} as unknown as MediaQueryList;
65+
};
66+
67+
const { queryByRole } = render(
68+
<Modal title="My modal" open={true}>
69+
<p>This is the content.</p>
70+
</Modal>,
71+
);
72+
expect(queryByRole("dialog")).toMatchSnapshot();
73+
});

src/Modal.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export const Modal: FC<Props> = ({
8989
styles.drawer,
9090
{ [styles.tabbed]: tabbed },
9191
)}
92+
role="dialog"
9293
// Suppress the warning about there being no description; the modal
9394
// has an accessible title
9495
aria-describedby={undefined}
@@ -114,9 +115,14 @@ export const Modal: FC<Props> = ({
114115
<DialogOverlay
115116
className={classNames(overlayStyles.bg, overlayStyles.animate)}
116117
/>
117-
{/* Suppress the warning about there being no description; the modal
118-
has an accessible title */}
119-
<DialogContent asChild aria-describedby={undefined} {...rest}>
118+
<DialogContent
119+
asChild
120+
// Suppress the warning about there being no description; the modal
121+
// has an accessible title
122+
aria-describedby={undefined}
123+
role="dialog"
124+
{...rest}
125+
>
120126
<Glass
121127
className={classNames(
122128
className,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`the content is rendered when the modal is open 1`] = `
4+
<div
5+
aria-labelledby="radix-:r4:"
6+
class="overlay animate modal dialog _glass_1x9g9_17"
7+
data-state="open"
8+
id="radix-:r3:"
9+
role="dialog"
10+
style="pointer-events: auto;"
11+
tabindex="-1"
12+
>
13+
<div
14+
class="content"
15+
>
16+
<div
17+
class="header"
18+
>
19+
<h2
20+
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121"
21+
id="radix-:r4:"
22+
>
23+
My modal
24+
</h2>
25+
</div>
26+
<div
27+
class="body"
28+
>
29+
<p>
30+
This is the content.
31+
</p>
32+
</div>
33+
</div>
34+
</div>
35+
`;
36+
37+
exports[`the modal renders as a drawer in mobile viewports 1`] = `
38+
<div
39+
aria-labelledby="radix-:ra:"
40+
class="overlay modal drawer"
41+
data-state="open"
42+
id="radix-:r9:"
43+
role="dialog"
44+
style="pointer-events: auto;"
45+
tabindex="-1"
46+
vaul-drawer=""
47+
vaul-drawer-direction="bottom"
48+
vaul-drawer-visible="true"
49+
>
50+
<div
51+
class="content"
52+
>
53+
<div
54+
class="header"
55+
>
56+
<div
57+
class="handle"
58+
/>
59+
<h2
60+
id="radix-:ra:"
61+
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
62+
>
63+
My modal
64+
</h2>
65+
</div>
66+
<div
67+
class="body"
68+
>
69+
<p>
70+
This is the content.
71+
</p>
72+
</div>
73+
</div>
74+
</div>
75+
`;

src/useMediaQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useEventTarget } from "./useEvents";
1313
* React hook that tracks whether the given media query matches.
1414
*/
1515
export function useMediaQuery(query: string): boolean {
16-
const mediaQuery = useMemo(() => matchMedia(query), [query]);
16+
const mediaQuery = useMemo(() => window.matchMedia(query), [query]);
1717

1818
const [numChanges, setNumChanges] = useState(0);
1919
useEventTarget(

0 commit comments

Comments
 (0)