Skip to content

Commit 2d0319d

Browse files
authored
Migrate ProjectUsers components to testing-library (#3935)
1 parent ec9c0f5 commit 2d0319d

File tree

9 files changed

+117
-129
lines changed

9 files changed

+117
-129
lines changed

src/components/Buttons/DeleteButtonWithDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Delete } from "@mui/icons-material";
22
import { ReactElement, useState } from "react";
33

4-
import { IconButtonWithTooltip } from "components/Buttons";
4+
import IconButtonWithTooltip from "components/Buttons/IconButtonWithTooltip";
55
import { CancelConfirmDialog } from "components/Dialogs";
66

77
interface DeleteButtonWithDialogProps {

src/components/Buttons/FlagButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Flag as FlagFilled, FlagOutlined } from "@mui/icons-material";
22
import { Fragment, type ReactElement, useEffect, useState } from "react";
33

44
import { type Flag } from "api/models";
5-
import { IconButtonWithTooltip } from "components/Buttons";
5+
import IconButtonWithTooltip from "components/Buttons/IconButtonWithTooltip";
66
import { DeleteEditTextDialog } from "components/Dialogs";
77

88
interface FlagButtonProps {

src/components/Dialogs/DeleteEditTextDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ export default function DeleteEditTextDialog(
120120
<NormalizedTextField
121121
variant="standard"
122122
autoFocus
123-
data-testid={props.textFieldId}
124123
value={text}
125124
onChange={(event) => setText(event.target.value)}
126125
onKeyPress={confirmIfEnter}
127126
InputProps={{ endAdornment }}
127+
inputProps={{ "data-testid": props.textFieldId }}
128128
id={props.textFieldId}
129129
/>
130130
</DialogContent>

src/components/Dialogs/EditTextDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ export default function EditTextDialog(
9898
<NormalizedTextField
9999
variant="standard"
100100
autoFocus
101-
data-testid={props.textFieldId}
102101
value={text}
103102
onChange={(event) => setText(event.target.value)}
104103
onKeyPress={confirmIfEnter}
105104
InputProps={{ endAdornment }}
105+
inputProps={{ "data-testid": props.textFieldId }}
106106
id={props.textFieldId}
107107
/>
108108
</DialogContent>

src/components/Dialogs/SubmitTextDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ export default function SubmitTextDialog(
8787
<NormalizedTextField
8888
variant="standard"
8989
autoFocus
90-
data-testid={props.textFieldId}
9190
value={text}
9291
onChange={(event) => setText(event.target.value)}
9392
onKeyPress={confirmIfEnter}
9493
InputProps={{ endAdornment }}
94+
inputProps={{ "data-testid": props.textFieldId }}
9595
id={props.textFieldId}
9696
/>
9797
</DialogContent>

src/components/ProjectUsers/EmailInvite.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import { getProjectId } from "backend/localStorage";
1010
import { LoadingDoneButton } from "components/Buttons";
1111
import { NormalizedTextField } from "utilities/fontComponents";
1212

13+
export enum EmailInviteTextId {
14+
ButtonSubmit = "buttons.invite",
15+
TextFieldEmail = "projectSettings.invite.emailLabel",
16+
TextFieldMessage = "projectSettings.invite.emailMessage",
17+
ToastUserExists = "projectSettings.invite.userExists",
18+
TypographyTitle = "projectSettings.invite.inviteByEmailLabel",
19+
}
20+
1321
interface InviteProps {
1422
addToProject: (userId: string) => void;
1523
close: () => void;
@@ -35,7 +43,7 @@ export default function EmailInvite(props: InviteProps): ReactElement {
3543
);
3644
} else {
3745
props.addToProject(await backend.getUserIdByEmailOrUsername(email));
38-
toast.info(t("projectSettings.invite.userExists"));
46+
toast.info(t(EmailInviteTextId.ToastUserExists));
3947
}
4048
setIsDone(true);
4149
setIsLoading(false);
@@ -51,15 +59,15 @@ export default function EmailInvite(props: InviteProps): ReactElement {
5159
<Stack alignContent="center" spacing={2}>
5260
{/* Title */}
5361
<Typography variant="h5" align="center">
54-
{t("projectSettings.invite.inviteByEmailLabel")}
62+
{t(EmailInviteTextId.TypographyTitle)}
5563
</Typography>
5664

5765
{/* Email address input */}
5866
<NormalizedTextField
5967
autoFocus
6068
fullWidth
6169
id="project-user-invite-email"
62-
label={t("projectSettings.invite.emailLabel")}
70+
label={t(EmailInviteTextId.TextFieldEmail)}
6371
onChange={(e) => setEmail(e.target.value)}
6472
required
6573
slotProps={{ htmlInput: { maxLength: 320 } }}
@@ -69,7 +77,7 @@ export default function EmailInvite(props: InviteProps): ReactElement {
6977
<NormalizedTextField
7078
fullWidth
7179
id="project-user-invite-message"
72-
label={t("projectSettings.invite.emailMessage")}
80+
label={t(EmailInviteTextId.TextFieldMessage)}
7381
onChange={(e) => setMessage(e.target.value)}
7482
slotProps={{ htmlInput: { maxLength: 10000 } }}
7583
/>
@@ -86,7 +94,7 @@ export default function EmailInvite(props: InviteProps): ReactElement {
8694
variant: "contained",
8795
}}
8896
>
89-
{t("buttons.invite")}
97+
{t(EmailInviteTextId.ButtonSubmit)}
9098
</LoadingDoneButton>
9199
</Grid2>
92100
</Stack>

src/components/ProjectUsers/tests/EmailInvite.test.tsx

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import renderer from "react-test-renderer";
1+
import "@testing-library/jest-dom";
2+
import { act, render, screen } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
24

3-
import { LoadingDoneButton } from "components/Buttons";
4-
import EmailInvite from "components/ProjectUsers/EmailInvite";
5+
import MockBypassLoadableButton from "components/Buttons/LoadingDoneButton";
6+
import EmailInvite, {
7+
EmailInviteTextId,
8+
} from "components/ProjectUsers/EmailInvite";
59

610
jest.mock("backend", () => ({
711
emailInviteToProject: () => mockEmailInviteToProject(),
@@ -12,51 +16,64 @@ jest.mock("backend", () => ({
1216
jest.mock("backend/localStorage", () => ({
1317
getProjectId: () => "mockId",
1418
}));
19+
jest.mock("components/Buttons", () => ({
20+
LoadingDoneButton: MockBypassLoadableButton,
21+
}));
1522

1623
const mockAddToProject = jest.fn();
1724
const mockClose = jest.fn();
1825
const mockEmailInviteToProject = jest.fn();
1926
const mockIsEmailOrUsernameAvailable = jest.fn();
2027

21-
let testRenderer: renderer.ReactTestRenderer;
22-
2328
describe("EmailInvite", () => {
2429
beforeEach(() => {
2530
jest.resetAllMocks();
26-
renderer.act(() => {
27-
testRenderer = renderer.create(
28-
<EmailInvite addToProject={mockAddToProject} close={mockClose} />
29-
);
31+
act(() => {
32+
render(<EmailInvite addToProject={mockAddToProject} close={mockClose} />);
3033
});
3134
});
3235

36+
const typeEmail = async (email: string): Promise<void> =>
37+
await userEvent.type(
38+
screen.getByText(EmailInviteTextId.TextFieldEmail),
39+
email
40+
);
41+
42+
it("has disabled button with invalid email", async () => {
43+
const button = screen.getByText(EmailInviteTextId.ButtonSubmit);
44+
expect(button).toBeDisabled();
45+
46+
await typeEmail("not-valid@4");
47+
expect(button).toBeDisabled();
48+
});
49+
50+
it("has enabled button with valid email", async () => {
51+
const button = screen.getByText(EmailInviteTextId.ButtonSubmit);
52+
expect(button).toBeDisabled();
53+
54+
await typeEmail("[email protected]");
55+
expect(button).toBeEnabled();
56+
});
57+
3358
it("closes after submit", async () => {
34-
await renderer.act(async () => {
35-
testRenderer.root
36-
.findByType(LoadingDoneButton)
37-
.props.buttonProps.onClick();
38-
});
59+
await typeEmail("[email protected]");
60+
expect(mockClose).not.toHaveBeenCalled();
61+
await userEvent.click(screen.getByText(EmailInviteTextId.ButtonSubmit));
3962
expect(mockClose).toHaveBeenCalledTimes(1);
4063
});
4164

4265
it("adds user if already exists", async () => {
4366
mockIsEmailOrUsernameAvailable.mockResolvedValueOnce(false);
44-
await renderer.act(async () => {
45-
testRenderer.root
46-
.findByType(LoadingDoneButton)
47-
.props.buttonProps.onClick();
48-
});
67+
await typeEmail("[email protected]");
68+
await userEvent.click(screen.getByText(EmailInviteTextId.ButtonSubmit));
4969
expect(mockAddToProject).toHaveBeenCalledTimes(1);
5070
expect(mockEmailInviteToProject).not.toHaveBeenCalled();
5171
});
5272

5373
it("invite user if doesn't exists", async () => {
5474
mockIsEmailOrUsernameAvailable.mockResolvedValueOnce(true);
55-
await renderer.act(async () => {
56-
testRenderer.root
57-
.findByType(LoadingDoneButton)
58-
.props.buttonProps.onClick();
59-
});
75+
await typeEmail("[email protected]");
76+
await userEvent.click(screen.getByText(EmailInviteTextId.ButtonSubmit));
6077
expect(mockAddToProject).not.toHaveBeenCalled();
6178
expect(mockEmailInviteToProject).toHaveBeenCalledTimes(1);
6279
});

src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx

Lines changed: 43 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import renderer from "react-test-renderer";
1+
import "@testing-library/jest-dom";
2+
import { act, render, screen } from "@testing-library/react";
3+
import userEvent from "@testing-library/user-event";
24

35
import ProjectSpeakersList, {
4-
AddSpeakerListItem,
56
ProjectSpeakersId,
6-
SpeakerListItem,
77
} from "components/ProjectUsers/ProjectSpeakersList";
88
import { randomSpeaker } from "types/project";
99

10-
// Dialog uses portals, which are not supported in react-test-renderer.
11-
jest.mock("@mui/material/Dialog", () =>
12-
jest.requireActual("@mui/material/Container")
13-
);
14-
1510
jest.mock("backend", () => ({
1611
createSpeaker: (name: string, projectId?: string) =>
1712
mockCreateSpeaker(name, projectId),
@@ -21,8 +16,7 @@ jest.mock("backend", () => ({
2116
updateSpeakerName: (speakerId: string, name: string, projectId?: string) =>
2217
mockUpdateSpeakerName(speakerId, name, projectId),
2318
}));
24-
// Mock "i18n", else `Error: connect ECONNREFUSED ::1:80`
25-
jest.mock("i18n", () => ({}));
19+
jest.mock("i18n", () => ({})); // else `thrown: "Error: AggregateError`
2620

2721
const mockCreateSpeaker = jest.fn();
2822
const mockDeleteSpeaker = jest.fn();
@@ -32,13 +26,9 @@ const mockUpdateSpeakerName = jest.fn();
3226
const mockProjId = "mock-project-id";
3327
const mockSpeakers = [randomSpeaker(), randomSpeaker(), randomSpeaker()];
3428

35-
let testRenderer: renderer.ReactTestRenderer;
36-
37-
const renderProjectSpeakersList = async (
38-
projId = mockProjId
39-
): Promise<void> => {
40-
await renderer.act(async () => {
41-
testRenderer = renderer.create(<ProjectSpeakersList projectId={projId} />);
29+
const renderProjectSpeakersList = async (): Promise<void> => {
30+
await act(async () => {
31+
render(<ProjectSpeakersList projectId={mockProjId} />);
4232
});
4333
};
4434

@@ -50,93 +40,66 @@ beforeEach(() => {
5040
});
5141

5242
describe("ProjectSpeakersList", () => {
53-
it("shows right number of speakers and an item to add a speaker", async () => {
43+
it("shows list item for each speakers, +1 for add-a-speaker", async () => {
5444
await renderProjectSpeakersList();
55-
expect(testRenderer.root.findAllByType(SpeakerListItem)).toHaveLength(
56-
mockSpeakers.length
45+
expect(screen.queryAllByRole("listitem")).toHaveLength(
46+
mockSpeakers.length + 1
5747
);
58-
expect(testRenderer.root.findByType(AddSpeakerListItem)).toBeTruthy();
5948
});
6049

6150
it("updates speaker name if changed", async () => {
6251
await renderProjectSpeakersList();
52+
const speaker = mockSpeakers[0];
6353

6454
// Click the button to edit speaker
65-
const editButton = testRenderer.root.findByProps({
66-
id: `${ProjectSpeakersId.ButtonEditPrefix}${mockSpeakers[0].id}`,
67-
});
68-
await renderer.act(() => {
69-
editButton.props.onClick();
70-
});
71-
72-
// Submit the current name with extra whitespace
73-
const mockEvent = {
74-
preventDefault: jest.fn(),
75-
target: { value: `\t\t${mockSpeakers[0].name} ` },
76-
};
77-
await renderer.act(() => {
78-
testRenderer.root
79-
.findByProps({ id: ProjectSpeakersId.TextFieldEdit })
80-
.props.onChange(mockEvent);
81-
});
82-
await renderer.act(() => {
83-
testRenderer.root
84-
.findByProps({ id: ProjectSpeakersId.ButtonEditConfirm })
85-
.props.onClick();
86-
});
55+
const editButton = screen.getByTestId(
56+
`${ProjectSpeakersId.ButtonEditPrefix}${speaker.id}`
57+
);
58+
await userEvent.click(editButton);
59+
60+
// Add whitespace to the current name
61+
await userEvent.type(
62+
screen.getByTestId(ProjectSpeakersId.TextFieldEdit),
63+
" "
64+
);
65+
await userEvent.click(
66+
screen.getByTestId(ProjectSpeakersId.ButtonEditConfirm)
67+
);
8768

8869
// Ensure no name update was submitted
8970
expect(mockUpdateSpeakerName).not.toHaveBeenCalled();
9071

9172
// Click the button to edit speaker
92-
await renderer.act(() => {
93-
editButton.props.onClick();
94-
});
95-
96-
// Submit a new name
97-
const name = "Mr. Different";
98-
mockEvent.target.value = name;
99-
await renderer.act(() => {
100-
testRenderer.root
101-
.findByProps({ id: ProjectSpeakersId.TextFieldEdit })
102-
.props.onChange(mockEvent);
103-
});
104-
await renderer.act(() => {
105-
testRenderer.root
106-
.findByProps({ id: ProjectSpeakersId.ButtonEditConfirm })
107-
.props.onClick();
108-
});
73+
await userEvent.click(editButton);
74+
75+
// Add non-whitespace
76+
await userEvent.type(
77+
screen.getByTestId(ProjectSpeakersId.TextFieldEdit),
78+
"!"
79+
);
80+
await userEvent.click(
81+
screen.getByTestId(ProjectSpeakersId.ButtonEditConfirm)
82+
);
10983

11084
// Ensure the name update was submitted
111-
expect(mockUpdateSpeakerName.mock.calls[0][1]).toEqual(name);
85+
expect(mockUpdateSpeakerName.mock.calls[0][1]).toEqual(`${speaker.name}!`);
11286
});
11387

11488
it("trims whitespace when adding a speaker", async () => {
11589
await renderProjectSpeakersList();
11690

11791
// Click the button to add a speaker
118-
await renderer.act(() => {
119-
testRenderer.root
120-
.findByProps({ id: ProjectSpeakersId.ButtonAdd })
121-
.props.onClick();
122-
});
92+
await userEvent.click(screen.getByTestId(ProjectSpeakersId.ButtonAdd));
12393

12494
// Submit the name of the speaker with extra whitespace
12595
const name = "Ms. Nym";
126-
const mockEvent = {
127-
preventDefault: jest.fn(),
128-
target: { value: ` ${name}\t ` },
129-
};
130-
await renderer.act(() => {
131-
testRenderer.root
132-
.findByProps({ id: ProjectSpeakersId.TextFieldAdd })
133-
.props.onChange(mockEvent);
134-
});
135-
await renderer.act(() => {
136-
testRenderer.root
137-
.findByProps({ id: ProjectSpeakersId.ButtonAddConfirm })
138-
.props.onClick();
139-
});
96+
await userEvent.type(
97+
screen.getByTestId(ProjectSpeakersId.TextFieldAdd),
98+
` ${name}\t `
99+
);
100+
await userEvent.click(
101+
screen.getByTestId(ProjectSpeakersId.ButtonAddConfirm)
102+
);
140103

141104
// Ensure new speaker was submitted with trimmed name
142105
expect(mockCreateSpeaker.mock.calls[0][0]).toEqual(name);

0 commit comments

Comments
 (0)