Skip to content

Commit ec9c0f5

Browse files
authored
Migrate ProjectSettings components to testing-library (#3936)
1 parent c263518 commit ec9c0f5

File tree

8 files changed

+150
-179
lines changed

8 files changed

+150
-179
lines changed

src/components/ProjectSettings/ProjectAutocomplete.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { useTranslation } from "react-i18next";
66
import { OffOnSetting } from "api/models";
77
import { type ProjectSettingProps } from "components/ProjectSettings/ProjectSettingsTypes";
88

9+
export enum ProjectAutocompleteTextId {
10+
MenuItemOff = "projectSettings.autocomplete.off",
11+
MenuItemOn = "projectSettings.autocomplete.on",
12+
Tooltip = "projectSettings.autocomplete.hint",
13+
}
14+
915
export default function ProjectAutocomplete(
1016
props: ProjectSettingProps
1117
): ReactElement {
@@ -27,15 +33,15 @@ export default function ProjectAutocomplete(
2733
}
2834
>
2935
<MenuItem value={OffOnSetting.Off}>
30-
{t("projectSettings.autocomplete.off")}
36+
{t(ProjectAutocompleteTextId.MenuItemOff)}
3137
</MenuItem>
3238
<MenuItem value={OffOnSetting.On}>
33-
{t("projectSettings.autocomplete.on")}
39+
{t(ProjectAutocompleteTextId.MenuItemOn)}
3440
</MenuItem>
3541
</Select>
3642

3743
<Tooltip
38-
title={t("projectSettings.autocomplete.hint")}
44+
title={t(ProjectAutocompleteTextId.Tooltip)}
3945
placement={document.body.dir === "rtl" ? "left" : "right"}
4046
>
4147
<HelpOutline fontSize="small" />

src/components/ProjectSettings/ProjectDomains.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export enum ProjectDomainsId {
3535
FieldDomainAddDialogName = "custom-domain-add-name",
3636
}
3737

38+
export const getDomainLabel = (domain: SemanticDomainFull): string =>
39+
`${domain.id} : ${domain.name}`;
40+
3841
export const trimDomain = (domain: SemanticDomainFull): SemanticDomainFull => ({
3942
...domain,
4043
description: domain.description.trim(),
@@ -194,15 +197,15 @@ function CustomDomain(props: CustomDomainProps): ReactElement {
194197
<Accordion>
195198
<AccordionSummary>
196199
<Typography sx={{ width: "calc(100% - 40px)" }}>
197-
{props.domain.id}
198-
{" : "}
199-
{props.domain.name}
200+
{getDomainLabel(props.domain)}
200201
</Typography>
201-
<IconButtonWithTooltip
202-
icon={<Delete />}
202+
<IconButton
203+
component="div" // Avoids nesting a button in the AccordionSummary button
203204
onClick={() => setDeleteDialogOpen(true)}
204205
size="small"
205-
/>
206+
>
207+
<Delete />
208+
</IconButton>
206209
<CancelConfirmDialog
207210
handleCancel={() => setDeleteDialogOpen(false)}
208211
handleConfirm={() => deleteDomain()}
@@ -318,6 +321,7 @@ export function AddDomainDialog(props: AddDomainDialogProps): ReactElement {
318321

319322
<div>
320323
<IconButton
324+
data-testid={ProjectDomainsId.ButtonDomainAddDialogConfirm}
321325
id={ProjectDomainsId.ButtonDomainAddDialogConfirm}
322326
onClick={() => submit()}
323327
size="small"
@@ -326,6 +330,7 @@ export function AddDomainDialog(props: AddDomainDialogProps): ReactElement {
326330
</IconButton>
327331

328332
<IconButton
333+
data-testid={ProjectDomainsId.ButtonDomainAddDialogCancel}
329334
id={ProjectDomainsId.ButtonDomainAddDialogCancel}
330335
onClick={() => cancel()}
331336
size="small"
@@ -350,6 +355,7 @@ export function AddDomainDialog(props: AddDomainDialogProps): ReactElement {
350355
/>
351356
) : (
352357
<IconButton
358+
data-testid={ProjectDomainsId.ButtonDomainAddDialogParentAdd}
353359
id={ProjectDomainsId.ButtonDomainAddDialogParentAdd}
354360
onClick={() => setAddingDom(true)}
355361
>
@@ -370,6 +376,9 @@ export function AddDomainDialog(props: AddDomainDialogProps): ReactElement {
370376
<TextFieldWithFont
371377
analysis
372378
id={ProjectDomainsId.FieldDomainAddDialogName}
379+
inputProps={{
380+
"data-testid": ProjectDomainsId.FieldDomainAddDialogName,
381+
}}
373382
lang={props.lang}
374383
onChange={(e) => setName(e.target.value)}
375384
value={name}

src/components/ProjectSettings/ProjectLanguages.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,19 @@ export default function ProjectLanguages(
217217
/>
218218

219219
<IconButton
220+
data-testid={ProjectLanguagesId.ButtonAddAnalysisLangConfirm}
220221
disabled={!isNewLang}
221-
onClick={() => addAnalysisWritingSystem()}
222222
id={ProjectLanguagesId.ButtonAddAnalysisLangConfirm}
223+
onClick={() => addAnalysisWritingSystem()}
223224
size="large"
224225
>
225226
<Done />
226227
</IconButton>
227228

228229
<IconButton
229-
onClick={() => resetState()}
230+
data-testid={ProjectLanguagesId.ButtonAddAnalysisLangClear}
230231
id={ProjectLanguagesId.ButtonAddAnalysisLangClear}
232+
onClick={() => resetState()}
231233
size="large"
232234
>
233235
<Clear />
@@ -257,22 +259,26 @@ export default function ProjectLanguages(
257259
const vernacularLanguageEditor = (): ReactElement => (
258260
<Stack spacing={1}>
259261
<NormalizedTextField
260-
variant="standard"
262+
autoFocus
261263
id={ProjectLanguagesId.FieldEditVernacularName}
262-
value={newVernName}
263-
onChange={(e) => setNewVernName(e.target.value)}
264+
inputProps={{
265+
"data-testid": ProjectLanguagesId.FieldEditVernacularName,
266+
}}
264267
onBlur={() => {
265268
setChangeVernName(false);
266269
setNewVernName(props.project.vernacularWritingSystem.name);
267270
}}
268-
autoFocus
271+
onChange={(e) => setNewVernName(e.target.value)}
272+
value={newVernName}
273+
variant="standard"
269274
/>
270275

271276
<Button
272-
variant="contained"
277+
data-testid={ProjectLanguagesId.ButtonEditVernacularNameSave}
273278
id={ProjectLanguagesId.ButtonEditVernacularNameSave}
274279
onClick={() => updateVernacularName()}
275280
onMouseDown={(e) => e.preventDefault()}
281+
variant="contained"
276282
>
277283
{t("buttons.save")}
278284
</Button>
@@ -374,12 +380,13 @@ export function SemanticDomainLanguage(
374380
)
375381
) : (
376382
<Select
377-
variant="standard"
383+
data-testid={ProjectLanguagesId.SelectSemDomLang}
378384
id={ProjectLanguagesId.SelectSemDomLang}
379-
value={props.project.semDomWritingSystem.bcp47}
380385
onChange={(event: SelectChangeEvent<string>) =>
381386
setSemDomWritingSystem(event.target.value as string)
382387
}
388+
value={props.project.semDomWritingSystem.bcp47}
389+
variant="standard"
383390
/* Use `displayEmpty` and a conditional `renderValue` function to force
384391
* something to appear when the menu is closed and its value is "" */
385392
displayEmpty

src/components/ProjectSettings/tests/ProjectAutocomplete.test.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
import { Select } from "@mui/material";
2-
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";
34

45
import { OffOnSetting } from "api/models";
5-
import ProjectAutocomplete from "components/ProjectSettings/ProjectAutocomplete";
6+
import ProjectAutocomplete, {
7+
ProjectAutocompleteTextId,
8+
} from "components/ProjectSettings/ProjectAutocomplete";
69
import { randomProject } from "types/project";
710

811
const mockSetProject = jest.fn();
912

1013
const mockProject = randomProject();
11-
12-
let testRenderer: renderer.ReactTestRenderer;
14+
mockProject.autocompleteSetting = OffOnSetting.Off;
1315

1416
const renderAutocomplete = async (): Promise<void> => {
15-
await renderer.act(async () => {
16-
testRenderer = renderer.create(
17+
await act(async () => {
18+
render(
1719
<ProjectAutocomplete project={mockProject} setProject={mockSetProject} />
1820
);
1921
});
@@ -22,13 +24,15 @@ const renderAutocomplete = async (): Promise<void> => {
2224
describe("ProjectAutocomplete", () => {
2325
it("updates project autocomplete", async () => {
2426
await renderAutocomplete();
25-
const selectChange = testRenderer.root.findByType(Select).props.onChange;
26-
await renderer.act(async () => selectChange({ target: { value: "Off" } }));
27-
expect(mockSetProject).toHaveBeenCalledWith({
28-
...mockProject,
29-
autocompleteSetting: OffOnSetting.Off,
30-
});
31-
await renderer.act(async () => selectChange({ target: { value: "On" } }));
27+
expect(
28+
screen.queryByText(ProjectAutocompleteTextId.MenuItemOff)
29+
).toBeTruthy();
30+
expect(screen.queryByText(ProjectAutocompleteTextId.MenuItemOn)).toBeNull();
31+
32+
await userEvent.click(screen.getByRole("combobox"));
33+
await userEvent.click(
34+
screen.getByText(ProjectAutocompleteTextId.MenuItemOn)
35+
);
3236
expect(mockSetProject).toHaveBeenCalledWith({
3337
...mockProject,
3438
autocompleteSetting: OffOnSetting.On,

src/components/ProjectSettings/tests/ProjectDomains.test.tsx

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
1-
import { Accordion } from "@mui/material";
2-
import renderer from "react-test-renderer";
1+
import "@testing-library/jest-dom";
2+
import {
3+
act,
4+
render,
5+
screen,
6+
waitForElementToBeRemoved,
7+
} from "@testing-library/react";
8+
import userEvent from "@testing-library/user-event";
39

410
import {
511
type Project,
612
type SemanticDomainFull,
713
type WritingSystem,
814
} from "api/models";
915
import ProjectDomains, {
10-
AddDomainDialog,
16+
getDomainLabel,
1117
ProjectDomainsId,
1218
trimDomain,
1319
} from "components/ProjectSettings/ProjectDomains";
1420
import { newProject } from "types/project";
1521
import { newSemanticDomain } from "types/semanticDomain";
1622
import { newWritingSystem } from "types/writingSystem";
1723

18-
// Dialog uses portals, which are not supported in react-test-renderer.
19-
jest.mock("@mui/material/Dialog", () => "div");
20-
// Textfield with multiline not supported in react-test-renderer.
21-
jest.mock("@mui/material/TextField", () => "div");
22-
2324
jest.mock("components/TreeView", () => "div");
2425
jest.mock("i18n", () => ({ language: "en-US" }));
2526

2627
const mockSetProject = jest.fn();
2728

28-
let projectMaster: renderer.ReactTestRenderer;
29-
3029
function mockProject(
3130
semDomWritingSystem?: WritingSystem,
3231
customDomains?: SemanticDomainFull[]
@@ -40,51 +39,49 @@ function mockProject(
4039

4140
const renderProjLangs = async (project: Project): Promise<void> => {
4241
mockSetProject.mockResolvedValue(undefined);
43-
await renderer.act(async () => {
44-
projectMaster = renderer.create(
45-
<ProjectDomains project={project} setProject={mockSetProject} />
46-
);
42+
await act(async () => {
43+
render(<ProjectDomains project={project} setProject={mockSetProject} />);
4744
});
4845
};
4946

5047
describe("ProjectDomains", () => {
5148
it("has a button for adding a custom semantic domain", async () => {
5249
await renderProjLangs(mockProject());
53-
const addDialog = projectMaster.root.findByType(AddDomainDialog);
54-
expect(addDialog.props.open).toBeFalsy();
50+
expect(screen.queryByRole("dialog")).toBeNull();
5551

5652
// Open the dialog to add a new domain
57-
const addButton = projectMaster.root.findByProps({
58-
id: ProjectDomainsId.ButtonDomainAdd,
59-
});
60-
await renderer.act(async () => {
61-
addButton.props.onClick();
62-
});
63-
expect(addDialog.props.open).toBeTruthy();
53+
await userEvent.click(screen.getByTestId(ProjectDomainsId.ButtonDomainAdd));
54+
expect(screen.queryByRole("dialog")).toBeTruthy();
6455

6556
// Close the dialog
66-
const cancelButton = projectMaster.root.findByProps({
67-
id: ProjectDomainsId.ButtonDomainAddDialogCancel,
68-
});
69-
await renderer.act(async () => {
70-
cancelButton.props.onClick();
71-
});
72-
expect(addDialog.props.open).toBeFalsy();
57+
await userEvent.click(
58+
screen.getByTestId(ProjectDomainsId.ButtonDomainAddDialogCancel)
59+
);
60+
// Wait for dialog removal, else it's only hidden.
61+
await waitForElementToBeRemoved(() => screen.queryByRole("dialog"));
62+
expect(screen.queryByRole("dialog")).toBeNull();
7363
});
7464

7565
it("only renders custom domains for the current semantic domain language", async () => {
7666
const semDomLang = "fr";
77-
const customDoms = [
67+
const inDoms = [
7868
newSemanticDomain("1", "one", semDomLang),
7969
newSemanticDomain("2", "two", semDomLang),
8070
newSemanticDomain("3", "three", semDomLang),
71+
];
72+
const outDoms = [
8173
newSemanticDomain("-4", "not four", "other"),
8274
newSemanticDomain("-5", "not five", "different"),
8375
];
8476
await renderProjLangs(
85-
mockProject(newWritingSystem(semDomLang), customDoms)
77+
mockProject(newWritingSystem(semDomLang), [...inDoms, ...outDoms])
8678
);
87-
expect(projectMaster.root.findAllByType(Accordion)).toHaveLength(3);
79+
inDoms.forEach((dom) => {
80+
expect(screen.queryByText(getDomainLabel(dom))).toBeTruthy();
81+
});
82+
outDoms.forEach((dom) => {
83+
expect(screen.queryByText(getDomainLabel(dom))).toBeNull();
84+
});
8885
});
8986
});
9087

0 commit comments

Comments
 (0)