diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx index 839c917f14..16379483bc 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/DomainsCell.tsx @@ -1,4 +1,4 @@ -import { Chip, Grid } from "@mui/material"; +import { Chip, Grid2 } from "@mui/material"; import { type ReactElement } from "react"; import { type SemanticDomain, type Sense } from "api/models"; @@ -19,12 +19,14 @@ export function gatherDomains(senses: Sense[]): SemanticDomain[] { export default function DomainsCell(props: CellProps): ReactElement { return ( - + {gatherDomains(props.word.senses).map((dom) => ( - - - + ))} - + ); } diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx index 674e9a62ed..f969cd30b4 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx @@ -13,10 +13,11 @@ import { Dialog, DialogContent, DialogTitle, - Grid, + Grid2, IconButton, MenuItem, Select, + Stack, type SelectChangeEvent, } from "@mui/material"; import { grey, yellow } from "@mui/material/colors"; @@ -42,7 +43,6 @@ import { } from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/utilities"; import { useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; -import { themeColors } from "types/theme"; import { type FileWithSpeakerId, newPronunciation, @@ -83,6 +83,16 @@ export enum EditDialogId { TextFieldVernacular = "edit-dialog-vernacular-textfield", } +export enum EditDialogTextId { + CardFlag = "reviewEntries.columns.flag", + CardNote = "reviewEntries.columns.note", + CardPronunciations = "reviewEntries.columns.pronunciations", + CardSenses = "reviewEntries.columns.senses", + CardVernacular = "reviewEntries.columns.vernacular", + DialogCancel = "reviewEntries.discardChanges", + TitlePrefix = "reviewEntries.columns.edit", +} + enum EditField { Flag, Note, @@ -333,155 +343,150 @@ export default function EditDialog(props: EditDialogProps): ReactElement { handleCancel={() => setCancelDialog(false)} handleConfirm={cancelAndClose} open={cancelDialog} - text="reviewEntries.discardChanges" + text={EditDialogTextId.DialogCancel} /> - - - {t("reviewEntries.columns.edit")} - {" : "} - {props.word.vernacular} - - - - + + {`${t(EditDialogTextId.TitlePrefix)} : ${props.word.vernacular}`} + + + + + - + - - + + + - + {/* Vernacular */} - - - - - - setNewWord((prev) => ({ - ...prev, - vernacular: e.target.value, - })) - } - value={newWord.vernacular} - vernacular - /> - - - - - {/* Senses */} - - - 1 && ( - setShowSenses((prev) => !prev)} - > - {showSenses ? ( - - ) : ( - - )} - - ) + + + + + setNewWord((prev) => ({ + ...prev, + vernacular: e.target.value, + })) } - title={t("reviewEntries.columns.senses")} - /> - - - + + + + {/* Senses */} + + 1 && ( + setShowSenses((prev) => !prev)} + > + {showSenses ? ( + + ) : ( + + )} + + ) + } + title={t(EditDialogTextId.CardSenses)} + /> + + {/* Pronunciations */} - - - - - - } - audio={newAudio} - deleteAudio={delNewAudio} - replaceAudio={repNewAudio} - uploadAudio={uplNewAudio} - /> - - - + + + + + } + audio={newAudio} + deleteAudio={delNewAudio} + replaceAudio={repNewAudio} + uploadAudio={uplNewAudio} + /> + + {/* Note */} - - - + + + updateNoteText(e.target.value)} + value={newWord.note.text} /> - - updateNoteText(e.target.value)} - value={newWord.note.text} - /> - - - + + {/* Flag */} - - - - - - {newWord.flag.active ? ( - - ) : ( - - )} - - updateFlag(e.target.value)} - value={newWord.flag.active ? newWord.flag.text : ""} - /> - - - - + + + + + {newWord.flag.active ? ( + + ) : ( + + )} + + updateFlag(e.target.value)} + value={newWord.flag.active ? newWord.flag.text : ""} + /> + + + > diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx index d00ee57c3b..a87997353b 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx @@ -7,8 +7,9 @@ import { Dialog, DialogContent, DialogTitle, - Grid, + Grid2, IconButton, + Stack, Typography, } from "@mui/material"; import { grey, yellow } from "@mui/material/colors"; @@ -56,6 +57,16 @@ export enum EditSenseDialogId { TextFieldGlossPrefix = "edit-sense-gloss-textfield-", } +export enum EditSenseDialogTextId { + CardDefinitions = "reviewEntries.columns.definitions", + CardGlosses = "reviewEntries.columns.glosses", + CardPartOfSpeech = "reviewEntries.columns.partOfSpeech", + CardPartOfSpeechUnspecified = "grammaticalCategory.group.Unspecified", + CardSemanticDomains = "reviewEntries.columns.domains", + DialogCancel = "reviewEntries.discardChanges", + Title = "reviewEntries.editSense", +} + export enum EditSenseField { Definitions, Glosses, @@ -180,110 +191,104 @@ export default function EditSenseDialog( handleCancel={() => setCancelDialog(false)} handleConfirm={cancelAndClose} open={cancelDialog} - text="reviewEntries.discardChanges" + text={EditSenseDialogTextId.DialogCancel} /> - - {t("reviewEntries.editSense")} - + + {t(EditSenseDialogTextId.Title)} + + - t.palette.success.main }} /> + + - t.palette.error.main }} /> + - - + + + - + {/* Definitions */} {definitionsEnabled && ( - - - - - - - - - )} - - {/* Glosses */} - - - + + - - + )} + + {/* Glosses */} + + + + + + {/* Part of Speech */} {grammaticalInfoEnabled && ( - - - - - {newSense.grammaticalInfo.catGroup === - GramCatGroup.Unspecified ? ( - - {t("grammaticalCategory.group.Unspecified")} - - ) : ( - - )} - - - - )} - - {/* Semantic Domains */} - - - + + - + {newSense.grammaticalInfo.catGroup === + GramCatGroup.Unspecified ? ( + + {t(EditSenseDialogTextId.CardPartOfSpeechUnspecified)} + + ) : ( + + )} - - + )} + + {/* Semantic Domains */} + + + + + + + > @@ -337,6 +342,7 @@ function DefinitionTextField(props: DefinitionTextFieldProps): ReactElement { error={props.error} fullWidth id={props.textFieldId} + inputProps={{ "data-testid": props.textFieldId }} label={props.definition.language} lang={props.definition.language} margin="dense" @@ -399,6 +405,7 @@ function GlossTextField(props: GlossTextFieldProps): ReactElement { error={props.error} fullWidth id={props.textFieldId} + inputProps={{ "data-testid": props.textFieldId }} label={props.gloss.language} lang={props.gloss.language} margin="dense" @@ -450,33 +457,34 @@ function DomainList(props: DomainListProps): ReactElement { return ( <> - + {props.domains.length > 0 ? ( props.domains.map((domain, index) => ( - - deleteDomain(domain.id)} - /> - + deleteDomain(domain.id)} + /> )) ) : ( - + - + )} { e.currentTarget.blur(); // else dialog reopens when domain selected with Enter setAddingDom(true); }} - size="large" > - + + setAddingDom(false)} diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx index a8183b0284..1c11bc7446 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent.tsx @@ -6,9 +6,9 @@ import { Edit, RestoreFromTrash, } from "@mui/icons-material"; -import { CardContent, Divider, Grid, Icon } from "@mui/material"; +import { Box, CardContent, Divider, Grid2, Icon, Stack } from "@mui/material"; import { grey, yellow } from "@mui/material/colors"; -import { type ReactElement, useEffect, useState } from "react"; +import { Fragment, type ReactElement, useEffect, useState } from "react"; import { type Sense, Status } from "api/models"; import { IconButtonWithTooltip } from "components/Buttons"; @@ -18,7 +18,7 @@ import EditSenseDialog from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCe import { isSenseChanged } from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/utilities"; import { newSense } from "types/word"; -enum EditSensesId { +export enum EditSensesId { ButtonSenseAdd = "add-sense-button", ButtonSenseBumpDownPrefix = "bump-up-sense-button-", ButtonSenseBumpUpPrefix = "bump-down-sense-button-", @@ -55,35 +55,39 @@ export default function EditSensesCardContent( return ( {props.showSenses ? ( - <> + {props.newSenses.map((s, i) => ( - props.moveSense(i, i + 1) - : undefined - } - bumpSenseUp={i ? () => props.moveSense(i, i - 1) : undefined} - edited={changes[i]} - key={s.guid} - sense={s} - toggleSenseDeleted={() => props.toggleSenseDeleted(i)} - updateSense={props.updateOrAddSense} - /> + + props.moveSense(i, i + 1) + : undefined + } + bumpSenseUp={i ? () => props.moveSense(i, i - 1) : undefined} + edited={changes[i]} + sense={s} + toggleSenseDeleted={() => props.toggleSenseDeleted(i)} + updateSense={props.updateOrAddSense} + /> + + ))} + } onClick={() => setAddSense(true)} size="small" /> + setAddSense(false)} isOpen={addSense} save={props.updateOrAddSense} sense={newSense()} /> - > + ) : ( change) ? yellow[100] : undefined} @@ -111,88 +115,74 @@ export function EditSense(props: EditSenseProps): ReactElement { const [editing, setEditing] = useState(false); return ( - <> - - {props.bumpSenseDown || props.bumpSenseUp ? ( - - - - : } - onClick={props.bumpSenseUp} - size="small" - /> - - - : } - onClick={props.bumpSenseDown} - size="small" - /> - - - - ) : null} - - - {deleted ? ( - - } - onClick={props.toggleSenseDeleted} - size="small" - /> - - ) : ( - <> - - } - onClick={ - sense.accessibility === Status.Protected - ? undefined - : props.toggleSenseDeleted - } - size="small" - textId={ - sense.accessibility === Status.Protected - ? "reviewEntries.deleteDisabled" - : undefined - } - /> - - - } - onClick={() => setEditing(true)} - size="small" - /> - - > - )} - - - - + {props.bumpSenseDown || props.bumpSenseUp ? ( + + : } + onClick={props.bumpSenseUp} + size="small" + /> + + : } + onClick={props.bumpSenseDown} + size="small" /> - - setEditing(false)} - isOpen={editing} - save={props.updateSense} + + ) : null} + + + {deleted ? ( + } + onClick={props.toggleSenseDeleted} + size="small" + /> + ) : ( + <> + } + onClick={ + sense.accessibility === Status.Protected + ? undefined + : props.toggleSenseDeleted + } + size="small" + textId={ + sense.accessibility === Status.Protected + ? "reviewEntries.deleteDisabled" + : undefined + } + /> + + } + onClick={() => setEditing(true)} + size="small" + /> + > + )} + + + + - - - > + + + setEditing(false)} + isOpen={editing} + save={props.updateSense} + sense={sense} + /> + ); } diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditDialog.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditDialog.test.tsx index 667382fb87..e48a76d15b 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditDialog.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditDialog.test.tsx @@ -1,69 +1,44 @@ -import { Delete, RestoreFromTrash } from "@mui/icons-material"; -import { type ChangeEvent } from "react"; +import { act, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; -import { type ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; -import { GramCatGroup, Status, type Word } from "api/models"; +import { type Word } from "api/models"; import { type CurrentProjectState } from "components/Project/ProjectReduxTypes"; -import SummarySenseCard from "components/WordCard/SummarySenseCard"; import EditDialog, { EditDialogId, } from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog"; -import EditSensesCardContent, { - EditSense, -} from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent"; import { newProject } from "types/project"; -import { newSemanticDomain } from "types/semanticDomain"; -import { newDefinition, newSense, newWord } from "types/word"; +import { newSense, newWord } from "types/word"; import { defaultWritingSystem } from "types/writingSystem"; -// Container uses Portal, not supported in react-test-renderer -jest.mock("@mui/material/Dialog", () => - jest.requireActual("@mui/material/Container") -); -// Textfield with multiline not supported in react-test-renderer -jest.mock("@mui/material/TextField", () => "div"); - jest.mock("backend", () => ({ - deleteAudio: (...args: any[]) => mockDeleteAudio(...args), + deleteAudio: () => jest.fn(), updateWord: (word: Word) => mockUpdateWord(word), })); jest.mock("components/Pronunciations/AudioRecorder"); jest.mock( - "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog" + "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent", + () => ({ + __esModule: true, + default: ({ showSenses }: { showSenses: boolean }) => ( + + ), + }) ); -jest.mock("i18n", () => ({})); +jest.mock("i18n", () => ({})); // else `thrown: "Error: AggregateError` const mockClose = jest.fn(); const mockConfirm = jest.fn(); -const mockDeleteAudio = jest.fn(); const mockUpdateWord = jest.fn(); -const mockTextFieldEvent = ( - value: string -): ChangeEvent => - ({ target: { value } }) as any; +const mockSenseStackId = "stack-o-senses"; +const mockSenseSummaryId = "summary-o-senses"; + +/** Returns minimalist multi-sense word. */ const mockWord = (): Word => ({ ...newWord("vernacular"), - senses: [ - { - ...newSense("gloss 1"), - definitions: [newDefinition("def A", "aa"), newDefinition("def B", "bb")], - }, - { - ...newSense("gloss 2"), - semanticDomains: [newSemanticDomain("2.2", "two-point-two")], - }, - { ...newSense("gloss 3"), accessibility: Status.Protected }, - { - ...newSense("gloss 4"), - grammaticalInfo: { - catGroup: GramCatGroup.Verb, - grammaticalCategory: "vt", - }, - }, - ], + senses: [newSense("gloss1"), newSense("gloss2")], }); const currentProjectState: Partial = { @@ -77,11 +52,9 @@ const currentProjectState: Partial = { }; const mockStore = configureMockStore()({ currentProjectState }); -let renderer: ReactTestRenderer; - const renderEditDialog = async (): Promise => await act(async () => { - renderer = create( + render( @@ -100,12 +73,7 @@ describe("EditDialog", () => { describe("cancel and save buttons", () => { test("cancel button closes if no changes", async () => { // Click the cancel button - const cancelButton = renderer.root.findByProps({ - id: EditDialogId.ButtonCancel, - }); - await act(async () => { - cancelButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditDialogId.ButtonCancel)); // Ensure a close without saving expect(mockClose).toHaveBeenCalledTimes(1); @@ -115,27 +83,14 @@ describe("EditDialog", () => { test("cancel button opens dialog if changes", async () => { // Make a change - const noteTextField = renderer.root.findByProps({ - id: EditDialogId.TextFieldNote, - }); - const newFlagText = "New note!"; - await act(async () => { - noteTextField.props.onChange(mockTextFieldEvent(newFlagText)); - }); + const noteField = screen.getByTestId(EditDialogId.TextFieldNote); + await userEvent.type(noteField, "New note!"); // Click the cancel button and cancel the cancel - const cancelButton = renderer.root.findByProps({ - id: EditDialogId.ButtonCancel, - }); - await act(async () => { - cancelButton.props.onClick(); - }); - const cancelButNoButton = renderer.root.findByProps({ - id: EditDialogId.ButtonCancelDialogCancel, - }); - await act(async () => { - cancelButNoButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditDialogId.ButtonCancel)); + await userEvent.click( + screen.getByTestId(EditDialogId.ButtonCancelDialogCancel) + ); // Ensure nothing happened expect(mockClose).not.toHaveBeenCalled(); @@ -143,15 +98,10 @@ describe("EditDialog", () => { expect(mockUpdateWord).not.toHaveBeenCalled(); // Click the cancel button and confirm the cancel - await act(async () => { - cancelButton.props.onClick(); - }); - const cancelAndYesButton = renderer.root.findByProps({ - id: EditDialogId.ButtonCancelDialogConfirm, - }); - await act(async () => { - cancelAndYesButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditDialogId.ButtonCancel)); + await userEvent.click( + screen.getByTestId(EditDialogId.ButtonCancelDialogConfirm) + ); // Ensure a close without saving expect(mockClose).toHaveBeenCalledTimes(1); @@ -161,12 +111,7 @@ describe("EditDialog", () => { test("save button closes if no changes", async () => { // Click the save button - const saveButton = renderer.root.findByProps({ - id: EditDialogId.ButtonSave, - }); - await act(async () => { - saveButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditDialogId.ButtonSave)); // Ensure a close without saving expect(mockClose).toHaveBeenCalledTimes(1); @@ -176,21 +121,12 @@ describe("EditDialog", () => { test("save button saves changes and closes", async () => { // Make a change - const flagTextField = renderer.root.findByProps({ - id: EditDialogId.TextFieldFlag, - }); + const flagField = screen.getByTestId(EditDialogId.TextFieldFlag); const newFlagText = "New flag!"; - await act(async () => { - flagTextField.props.onChange(mockTextFieldEvent(newFlagText)); - }); + await userEvent.type(flagField, newFlagText); // Click the save button - const saveButton = renderer.root.findByProps({ - id: EditDialogId.ButtonSave, - }); - await act(async () => { - saveButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditDialogId.ButtonSave)); // Ensure save and close occurred, with dispatch up update goal expect(mockClose).toHaveBeenCalledTimes(1); @@ -201,57 +137,20 @@ describe("EditDialog", () => { }); }); - describe("senses", () => { - test("sense view toggle", async () => { - // Not summary view by default - expect(renderer.root.findAllByType(SummarySenseCard)).toHaveLength(0); - - // Click to turn on summary view - const button = renderer.root.findByProps({ - id: EditDialogId.ButtonSensesViewToggle, - }); - await act(async () => { - button.props.onClick(); - }); - expect(renderer.root.findAllByType(SummarySenseCard)).toHaveLength(1); - - // Click again to turn off summary view - await act(async () => { - button.props.onClick(); - }); - expect(renderer.root.findAllByType(SummarySenseCard)).toHaveLength(0); - }); - - test("add a sense", async () => { - expect(renderer.root.findAllByType(EditSense)).toHaveLength(4); - const senses = renderer.root.findByType(EditSensesCardContent); - await act(async () => { - senses.props.updateOrAddSense(newSense("new gloss")); - }); - expect(renderer.root.findAllByType(EditSense)).toHaveLength(5); - }); - - test("delete/restore a sense", async () => { - expect(renderer.root.findAllByType(EditSense)).toHaveLength(4); - expect(renderer.root.findAllByType(Delete)).toHaveLength(4); - expect(renderer.root.findAllByType(RestoreFromTrash)).toHaveLength(0); - const senses = renderer.root.findByType(EditSensesCardContent); - - // Delete the first sense - await act(async () => { - senses.props.toggleSenseDeleted(0); - }); - expect(renderer.root.findAllByType(EditSense)).toHaveLength(4); - expect(renderer.root.findAllByType(Delete)).toHaveLength(3); - expect(renderer.root.findAllByType(RestoreFromTrash)).toHaveLength(1); - - // Restore the first sense - await act(async () => { - senses.props.toggleSenseDeleted(0); - }); - expect(renderer.root.findAllByType(EditSense)).toHaveLength(4); - expect(renderer.root.findAllByType(Delete)).toHaveLength(4); - expect(renderer.root.findAllByType(RestoreFromTrash)).toHaveLength(0); - }); + test("sense-view toggle button", async () => { + // Not summary view by default + expect(screen.queryByTestId(mockSenseStackId)).toBeTruthy(); + expect(screen.queryByTestId(mockSenseSummaryId)).toBeNull(); + + // Click to turn on summary view + const button = screen.getByTestId(EditDialogId.ButtonSensesViewToggle); + await userEvent.click(button); + expect(screen.queryByTestId(mockSenseStackId)).toBeNull(); + expect(screen.queryByTestId(mockSenseSummaryId)).toBeTruthy(); + + // Click again to turn off summary view + await userEvent.click(button); + expect(screen.queryByTestId(mockSenseStackId)).toBeTruthy(); + expect(screen.queryByTestId(mockSenseSummaryId)).toBeNull(); }); }); diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSenseDialog.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSenseDialog.test.tsx index 7f6a7b1cc3..06e42db000 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSenseDialog.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSenseDialog.test.tsx @@ -1,36 +1,19 @@ -import { type ChangeEvent } from "react"; +import { act, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; -import { type ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import { Project, type Sense } from "api/models"; import EditSenseDialog, { EditSenseDialogId, + EditSenseDialogTextId, } from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog"; import { type StoreState, defaultState } from "rootRedux/types"; import { newSense } from "types/word"; -// Dialog uses Portal, not supported in react-test-renderer -jest.mock("@mui/material/Dialog", () => - jest.requireActual("@mui/material/Container") -); -// Textfield with multiline not supported in react-test-renderer -jest.mock("@mui/material/TextField", () => "div"); - -jest.mock("components/TreeView", () => "div"); -jest.mock("rootRedux/hooks", () => ({ - ...jest.requireActual("rootRedux/hooks"), - useAppDispatch: () => jest.fn(), -})); - const mockClose = jest.fn(); const mockSave = jest.fn(); -const mockTextFieldEvent = ( - value: string -): ChangeEvent => - ({ target: { value } }) as any; - const mockState = ( definitionsEnabled = false, grammaticalInfoEnabled = false @@ -46,8 +29,6 @@ const mockState = ( }; }; -let renderer: ReactTestRenderer; - const renderEditSenseDialog = async ( definitionsEnabled = false, grammaticalInfoEnabled = false @@ -56,7 +37,7 @@ const renderEditSenseDialog = async ( mockState(definitionsEnabled, grammaticalInfoEnabled) ); await act(async () => { - renderer = create( + render( { test("cancel button closes if no changes", async () => { // Click the cancel button - const cancelButton = renderer.root.findByProps({ - id: EditSenseDialogId.ButtonCancel, - }); - await act(async () => { - cancelButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditSenseDialogId.ButtonCancel)); // Ensure a close without saving expect(mockClose).toHaveBeenCalledTimes(1); @@ -95,42 +71,24 @@ describe("EditSenseDialog", () => { test("cancel button opens dialog if changes", async () => { // Make a change - const glossTextField = renderer.root.findByProps({ - id: `${EditSenseDialogId.TextFieldGlossPrefix}0`, - }); - const newGlossText = "New gloss!"; - await act(async () => { - glossTextField.props.onChange(mockTextFieldEvent(newGlossText)); - }); + const testId = `${EditSenseDialogId.TextFieldGlossPrefix}0`; + await userEvent.type(screen.getByTestId(testId), "glossier"); // Click the cancel button and cancel the cancel - const cancelButton = renderer.root.findByProps({ - id: EditSenseDialogId.ButtonCancel, - }); - await act(async () => { - cancelButton.props.onClick(); - }); - const cancelButNoButton = renderer.root.findByProps({ - id: EditSenseDialogId.ButtonCancelDialogCancel, - }); - await act(async () => { - cancelButNoButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditSenseDialogId.ButtonCancel)); + await userEvent.click( + screen.getByTestId(EditSenseDialogId.ButtonCancelDialogCancel) + ); // Ensure nothing happened expect(mockClose).not.toHaveBeenCalled(); expect(mockSave).not.toHaveBeenCalled(); // Click the cancel button and confirm the cancel - await act(async () => { - cancelButton.props.onClick(); - }); - const cancelAndYesButton = renderer.root.findByProps({ - id: EditSenseDialogId.ButtonCancelDialogConfirm, - }); - await act(async () => { - cancelAndYesButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditSenseDialogId.ButtonCancel)); + await userEvent.click( + screen.getByTestId(EditSenseDialogId.ButtonCancelDialogConfirm) + ); // Ensure a close without saving expect(mockClose).toHaveBeenCalledTimes(1); @@ -139,12 +97,7 @@ describe("EditSenseDialog", () => { test("save button closes if no changes", async () => { // Click the save button - const saveButton = renderer.root.findByProps({ - id: EditSenseDialogId.ButtonSave, - }); - await act(async () => { - saveButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditSenseDialogId.ButtonSave)); // Ensure a close without saving expect(mockClose).toHaveBeenCalledTimes(1); @@ -153,21 +106,14 @@ describe("EditSenseDialog", () => { test("save button saves changes and closes", async () => { // Make a change - const glossTextField = renderer.root.findByProps({ - id: `${EditSenseDialogId.TextFieldGlossPrefix}0`, - }); + const testId = `${EditSenseDialogId.TextFieldGlossPrefix}0`; + const glossField = screen.getByTestId(testId); + await userEvent.clear(glossField); const newGlossText = "New gloss!"; - await act(async () => { - glossTextField.props.onChange(mockTextFieldEvent(newGlossText)); - }); + await userEvent.type(glossField, newGlossText); // Click the save button - const saveButton = renderer.root.findByProps({ - id: EditSenseDialogId.ButtonSave, - }); - await act(async () => { - saveButton.props.onClick(); - }); + await userEvent.click(screen.getByTestId(EditSenseDialogId.ButtonSave)); // Ensure save and close occurred expect(mockClose).toHaveBeenCalledTimes(1); @@ -178,35 +124,19 @@ describe("EditSenseDialog", () => { }); describe("definitionsEnabled & grammaticalInfoEnabled", () => { - const definitionsTitle = "reviewEntries.columns.definitions"; - const partOfSpeechTitle = "reviewEntries.columns.partOfSpeech"; + const definitionsTitle = EditSenseDialogTextId.CardDefinitions; + const partOfSpeechTitle = EditSenseDialogTextId.CardPartOfSpeech; test("show definitions when definitionsEnabled is true", async () => { await renderEditSenseDialog(true, false); - expect( - renderer.root.findAllByProps({ - title: definitionsTitle, - }) - ).toHaveLength(1); - expect( - renderer.root.findAllByProps({ - title: partOfSpeechTitle, - }) - ).toHaveLength(0); + expect(screen.queryByText(definitionsTitle)).toBeTruthy(); + expect(screen.queryByText(partOfSpeechTitle)).toBeNull(); }); test("show part of speech when grammaticalInfoEnabled is true", async () => { await renderEditSenseDialog(false, true); - expect( - renderer.root.findAllByProps({ - title: definitionsTitle, - }) - ).toHaveLength(0); - expect( - renderer.root.findAllByProps({ - title: partOfSpeechTitle, - }) - ).toHaveLength(1); + expect(screen.queryByText(definitionsTitle)).toBeNull(); + expect(screen.queryByText(partOfSpeechTitle)).toBeTruthy(); }); }); }); diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSensesCardContent.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSensesCardContent.test.tsx new file mode 100644 index 0000000000..957dad9435 --- /dev/null +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSensesCardContent.test.tsx @@ -0,0 +1,111 @@ +import "@testing-library/jest-dom"; +import { act, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { GramCatGroup, Sense, Status } from "api/models"; +import EditSensesCardContent, { + EditSensesId, +} from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSensesCardContent"; +import { newSemanticDomain } from "types/semanticDomain"; +import { newDefinition, newSense } from "types/word"; + +jest.mock("components/WordCard/SenseCard"); +jest.mock( + "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog" +); + +const mockMoveSense = jest.fn(); +const mockToggleSenseDeleted = jest.fn(); +const mockUpdateOrAddSense = jest.fn(); + +const mockSenseGuids = ["guid1", "guid2", "guid3", "guid4"]; +const mockSenses = (): Sense[] => [ + { + ...newSense("gloss 1"), + definitions: [newDefinition("def A", "aa"), newDefinition("def B", "bb")], + guid: mockSenseGuids[0], + }, + { + ...newSense("gloss 2"), + guid: mockSenseGuids[1], + semanticDomains: [newSemanticDomain("2.2", "two-point-two")], + }, + { + ...newSense("gloss 3"), + accessibility: Status.Protected, + guid: mockSenseGuids[2], + }, + { + ...newSense("gloss 4"), + grammaticalInfo: { + catGroup: GramCatGroup.Verb, + grammaticalCategory: "vt", + }, + guid: mockSenseGuids[3], + }, +]; + +const renderEditSensesCardContent = async (showSenses = true): Promise => + await act(async () => { + render( + mockMoveSense(from, to)} + newSenses={mockSenses()} + oldSenses={mockSenses()} + showSenses={showSenses} + toggleSenseDeleted={mockToggleSenseDeleted} + updateOrAddSense={mockUpdateOrAddSense} + /> + ); + }); + +beforeEach(async () => { + jest.clearAllMocks(); +}); + +describe("EditSensesCardContent", () => { + it("renders sense summary", async () => { + await renderEditSensesCardContent(false); + expect(screen.queryByRole("list")).toBeNull(); + expect(screen.queryAllByRole("listitem")).toHaveLength(0); + }); + + it("renders senses", async () => { + await renderEditSensesCardContent(true); + expect(screen.queryByRole("list")).toBeTruthy(); + expect(screen.queryAllByRole("listitem")).toHaveLength(4); + }); + + describe("up/down buttons", () => { + beforeEach(async () => { + await renderEditSensesCardContent(); + }); + + const sensesCount = mockSenses().length; + + for (let i = 0; i < sensesCount; i++) { + test(`move ${i} up`, async () => { + const testId = EditSensesId.ButtonSenseBumpUpPrefix + mockSenseGuids[i]; + if (i) { + await userEvent.click(screen.getByTestId(testId)); + expect(mockMoveSense).toHaveBeenCalledTimes(1); + expect(mockMoveSense).toHaveBeenCalledWith(i, i - 1); + } else { + expect(screen.getByTestId(testId)).toBeDisabled(); + } + }); + + test(`move ${i} down`, async () => { + const testId = + EditSensesId.ButtonSenseBumpDownPrefix + mockSenseGuids[i]; + if (i < sensesCount - 1) { + await userEvent.click(screen.getByTestId(testId)); + expect(mockMoveSense).toHaveBeenCalledTimes(1); + expect(mockMoveSense).toHaveBeenCalledWith(i, i + 1); + } else { + expect(screen.getByTestId(testId)).toBeDisabled(); + } + }); + } + }); +}); diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx index df0840d02a..604e1720a9 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/PartOfSpeechCell.tsx @@ -1,4 +1,4 @@ -import { Grid } from "@mui/material"; +import { Grid2 } from "@mui/material"; import { type ReactElement } from "react"; import { type GrammaticalInfo, type Sense } from "api/models"; @@ -18,12 +18,13 @@ function gatherGramInfo(senses: Sense[]): GrammaticalInfo[] { export default function PartOfSpeechCell(props: CellProps): ReactElement { return ( - + {gatherGramInfo(props.word.senses).map((gi) => ( - - - + ))} - + ); } diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx index f8fa7b2917..1fbf5cdf8a 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/index.tsx @@ -82,6 +82,20 @@ export enum ColumnId { Vernacular = "vernacular", } +export const ColumnHeaderTextId: Record = { + [ColumnId.Definitions]: "reviewEntries.columns.definitions", + [ColumnId.Domains]: "reviewEntries.columns.domains", + [ColumnId.Delete]: "reviewEntries.columns.delete", + [ColumnId.Edit]: "reviewEntries.columns.edit", + [ColumnId.Flag]: "reviewEntries.columns.flag", + [ColumnId.Glosses]: "reviewEntries.columns.glosses", + [ColumnId.Note]: "reviewEntries.columns.note", + [ColumnId.PartOfSpeech]: "reviewEntries.columns.partOfSpeech", + [ColumnId.Pronunciations]: "reviewEntries.columns.pronunciations", + [ColumnId.Senses]: "reviewEntries.columns.sensesCount", + [ColumnId.Vernacular]: "reviewEntries.columns.vernacular", +}; + // Constants for pagination state. const rowsPerPage = [10, 100]; const initPaginationState: MRT_PaginationState = { @@ -217,7 +231,7 @@ export default function ReviewEntriesTable(props: { ), enableHiding: false, Header: "", - header: t("reviewEntries.columns.edit"), + header: t(ColumnHeaderTextId[ColumnId.Edit]), id: ColumnId.Edit, size: IconColumnSize, visibleInShowHideMenu: false, @@ -229,7 +243,7 @@ export default function ReviewEntriesTable(props: { enableColumnOrdering: false, enableHiding: false, filterFn: ff.filterFnString, - header: t("reviewEntries.columns.vernacular"), + header: t(ColumnHeaderTextId[ColumnId.Vernacular]), id: ColumnId.Vernacular, size: BaselineColumnSize - 40, sortingFn: sf.sortingFnVernacular(vernLang), @@ -240,7 +254,7 @@ export default function ReviewEntriesTable(props: { enableFilterMatchHighlighting: false, filterFn: "equals", Header: #, - header: t("reviewEntries.columns.sensesCount"), + header: t(ColumnHeaderTextId[ColumnId.Senses]), id: ColumnId.Senses, muiTableHeadCellProps: { sx: { @@ -257,7 +271,7 @@ export default function ReviewEntriesTable(props: { columnHelper.accessor((w) => w.senses.flatMap((s) => s.definitions), { Cell: ({ row }: CellProps) => , filterFn: ff.filterFnDefinitions, - header: t("reviewEntries.columns.definitions"), + header: t(ColumnHeaderTextId[ColumnId.Definitions]), id: ColumnId.Definitions, size: BaselineColumnSize + 20, sortingFn: sf.sortingFnDefinitions, @@ -268,7 +282,7 @@ export default function ReviewEntriesTable(props: { columnHelper.accessor((w) => w.senses.flatMap((s) => s.glosses), { Cell: ({ row }: CellProps) => , filterFn: ff.filterFnGlosses, - header: t("reviewEntries.columns.glosses"), + header: t(ColumnHeaderTextId[ColumnId.Glosses]), id: ColumnId.Glosses, sortingFn: sf.sortingFnGlosses, }), @@ -285,7 +299,7 @@ export default function ReviewEntriesTable(props: { value: g, })), filterVariant: "select", - header: t("reviewEntries.columns.partOfSpeech"), + header: t(ColumnHeaderTextId[ColumnId.PartOfSpeech]), id: ColumnId.PartOfSpeech, sortingFn: sf.sortingFnPartOfSpeech, visibleInShowHideMenu: grammaticalInfoEnabled, @@ -295,7 +309,7 @@ export default function ReviewEntriesTable(props: { columnHelper.accessor((w) => w.senses.flatMap((s) => s.semanticDomains), { Cell: ({ row }: CellProps) => , filterFn: ff.filterFnDomains, - header: t("reviewEntries.columns.domains"), + header: t(ColumnHeaderTextId[ColumnId.Domains]), id: ColumnId.Domains, sortingFn: sf.sortingFnDomains, }), @@ -308,17 +322,11 @@ export default function ReviewEntriesTable(props: { filterFn: ff.filterFnPronunciations(speakers), Header: ( <> - t.palette.error.main }} - /> - t.palette.success.main }} - /> + + > ), - header: t("reviewEntries.columns.pronunciations"), + header: t(ColumnHeaderTextId[ColumnId.Pronunciations]), id: ColumnId.Pronunciations, muiTableHeadCellProps: { sx: { @@ -337,7 +345,7 @@ export default function ReviewEntriesTable(props: { columnHelper.accessor((w) => w.note.text || undefined, { Cell: ({ row }: CellProps) => , filterFn: ff.filterFnString, - header: t("reviewEntries.columns.note"), + header: t(ColumnHeaderTextId[ColumnId.Note]), id: ColumnId.Note, size: BaselineColumnSize - 40, sortingFn: sf.sortingFnNote, @@ -347,13 +355,8 @@ export default function ReviewEntriesTable(props: { columnHelper.accessor("flag", { Cell: ({ row }: CellProps) => , filterFn: ff.filterFnFlag, - Header: ( - t.palette.error.main }} - /> - ), - header: t("reviewEntries.columns.flag"), + Header: , + header: t(ColumnHeaderTextId[ColumnId.Flag]), id: ColumnId.Flag, muiTableHeadCellProps: { sx: { @@ -376,7 +379,7 @@ export default function ReviewEntriesTable(props: { ), enableHiding: false, Header: "", - header: t("reviewEntries.columns.delete"), + header: t(ColumnHeaderTextId[ColumnId.Delete]), id: ColumnId.Delete, size: IconColumnSize, visibleInShowHideMenu: false, diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/tests/WordsMock.ts b/src/goals/ReviewEntries/ReviewEntriesTable/tests/WordsMock.ts index ea88df4254..d2afc8f74e 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/tests/WordsMock.ts +++ b/src/goals/ReviewEntries/ReviewEntriesTable/tests/WordsMock.ts @@ -51,6 +51,8 @@ senses[3][1].semanticDomains.push(newSemanticDomain("1")); senses[0][0].semanticDomains.push(newSemanticDomain("3")); // (leave senses[1] without semantic domains) +export const verns = ["Alfa", "Delta", "Bravo", "Charlie"]; + // Defined words for vernaculars sorted order [0, 2, 3, 1] // and for pronunciations sorted order [3, 2, 1, 0] // and for flags sorted order [1, 0, 3, 2] @@ -59,7 +61,7 @@ senses[0][0].semanticDomains.push(newSemanticDomain("3")); export function mockWords(): Word[] { return [ { - ...newWord("Alfa"), + ...newWord(verns[0]), audio: [newPronunciation(), newPronunciation(), newPronunciation()], flag: newFlag("India"), id: "0", @@ -67,7 +69,7 @@ export function mockWords(): Word[] { senses: senses[0], }, { - ...newWord("Delta"), + ...newWord(verns[1]), audio: [newPronunciation(), newPronunciation()], flag: { active: true, text: "" }, // (active flag with empty text is sorted to first) id: "1", @@ -75,7 +77,7 @@ export function mockWords(): Word[] { senses: senses[1], }, { - ...newWord("Bravo"), + ...newWord(verns[2]), audio: [newPronunciation()], // (flag with `active = false` is sorted to last) id: "2", @@ -83,7 +85,7 @@ export function mockWords(): Word[] { senses: senses[2], }, { - ...newWord("Charlie"), + ...newWord(verns[3]), flag: newFlag("Juliett"), id: "3", note: newNote("Lima"), diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx index 7c1c6239d3..e0095a9df9 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/tests/index.test.tsx @@ -1,25 +1,20 @@ -import { TableSortLabel } from "@mui/material"; -import { MRT_TableBodyRow, MRT_TableHeadCell } from "material-react-table"; +import { act, render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; -import { type ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import { defaultState } from "components/Project/ProjectReduxTypes"; import ReviewEntriesTable, { + ColumnHeaderTextId, ColumnId, } from "goals/ReviewEntries/ReviewEntriesTable"; -import VernacularCell from "goals/ReviewEntries/ReviewEntriesTable/Cells/VernacularCell"; import { mockWords, sortOrder, + verns, } from "goals/ReviewEntries/ReviewEntriesTable/tests/WordsMock"; import { type StoreState } from "rootRedux/types"; -// With `columnFilterDisplayMode: "popover",`, it is necessary to mock out `Grow`. -// To access filter `TextField`s, replace both `Grow`, `Modal` with `div`. -// However, using a `TextField`'s `.props.onChange()` doesn't activate a filter. -jest.mock("@mui/material/Grow", () => "div"); - // Intercept i18n to set the resolvedLanguage for localization testing. jest.mock("react-i18next", () => ({ ...jest.requireActual("react-i18next"), @@ -34,15 +29,13 @@ const setMockUseTranslation = (resolvedLanguage: string): void => { }; jest.mock("backend", () => ({ - getAllSpeakers: (projectId: string) => mockGetAllSpeakers(projectId), + getAllSpeakers: () => Promise.resolve([]), getFrontierWords: (...args: any[]) => mockGetFrontierWords(...args), getWord: (wordId: string) => mockGetWord(wordId), })); jest.mock("components/Pronunciations/PronunciationsBackend"); jest.mock("i18n", () => ({})); -const mockClickEvent = { stopPropagation: jest.fn() }; -const mockGetAllSpeakers = jest.fn(); const mockGetFrontierWords = jest.fn(); const mockGetWord = jest.fn(); const mockState = ( @@ -59,8 +52,6 @@ const mockState = ( }, }); -let renderer: ReactTestRenderer; - const renderReviewEntriesTable = async ( definitionsEnabled = false, grammaticalInfoEnabled = false, @@ -71,7 +62,7 @@ const renderReviewEntriesTable = async ( mockState(definitionsEnabled, grammaticalInfoEnabled) ); await act(async () => { - renderer = create( + render( @@ -81,7 +72,6 @@ const renderReviewEntriesTable = async ( function setMockFunctions(): void { jest.clearAllMocks(); - mockGetAllSpeakers.mockResolvedValue([]); mockGetFrontierWords.mockResolvedValue(mockWords()); } @@ -93,7 +83,8 @@ describe("ReviewEntriesTable", () => { test("initial render fetches frontier and loads data", async () => { await renderReviewEntriesTable(); expect(mockGetFrontierWords).toHaveBeenCalled(); - expect(renderer.root.findAllByType(MRT_TableBodyRow)).toHaveLength(4); + const rowCount = mockWords().length + 1; // +1 for header row + expect(screen.getAllByRole("row")).toHaveLength(rowCount); }); describe("table sort", () => { @@ -103,16 +94,18 @@ describe("ReviewEntriesTable", () => { /** Checks if the WordsMock.tsx words have been sorted by the given column. */ const checkRowOrder = (col: number, dir: "asc" | "desc"): void => { - const rowIds = renderer.root - .findAllByType(VernacularCell) - .map((cell) => cell.props.word.id); + // Use distinct mock verns to determine sorted order. + const sorted = screen + .getAllByRole("row") + .slice(1) + .map((r) => verns.findIndex((v) => within(r).queryByText(v))); + + // Verify the order is right. const order = [...sortOrder[col]]; if (dir === "desc") { order.reverse(); } - order.forEach((id, index) => { - expect(rowIds[index]).toEqual(`${id}`); - }); + expect(sorted).toEqual(order); }; /** The accessor columns in default order. */ @@ -130,43 +123,32 @@ describe("ReviewEntriesTable", () => { cols.forEach((col, i) => { test(`sorting by ${col} column`, async () => { - const button = renderer.root.findAllByType(TableSortLabel)[i]; - expect(button.props.direction).toBeUndefined(); - await act(async () => { - button.props.onClick(mockClickEvent); - }); - expect(button.props.direction).toEqual("asc"); + // The icon changes when clicked, so use the surrounding span. + const button = screen.getAllByTestId("SyncAltIcon")[i].closest("span"); + + await userEvent.click(button!); checkRowOrder(i, "asc"); - await act(async () => { - button.props.onClick(mockClickEvent); - }); - expect(button.props.direction).toEqual("desc"); + + await userEvent.click(button!); checkRowOrder(i, "desc"); - await act(async () => { - button.props.onClick(mockClickEvent); - }); - expect(button.props.direction).toBeUndefined(); }); }); }); describe("definitionsEnabled & grammaticalInfoEnabled", () => { + const defTextId = ColumnHeaderTextId[ColumnId.Definitions]; + const posTextId = ColumnHeaderTextId[ColumnId.PartOfSpeech]; + test("show definitions when definitionsEnabled is true", async () => { await renderReviewEntriesTable(true, false); - const colIds = renderer.root - .findAllByType(MRT_TableHeadCell) - .map((col) => col.props.header.id); - expect(colIds).toContain(ColumnId.Definitions); - expect(colIds).not.toContain(ColumnId.PartOfSpeech); + expect(screen.queryByText(defTextId)).toBeTruthy(); + expect(screen.queryByText(posTextId)).toBeNull(); }); test("show part of speech when grammaticalInfoEnabled is true", async () => { await renderReviewEntriesTable(false, true); - const colIds = renderer.root - .findAllByType(MRT_TableHeadCell) - .map((col) => col.props.header.id); - expect(colIds).not.toContain(ColumnId.Definitions); - expect(colIds).toContain(ColumnId.PartOfSpeech); + expect(screen.queryByText(defTextId)).toBeNull(); + expect(screen.queryByText(posTextId)).toBeTruthy(); }); }); @@ -183,15 +165,13 @@ describe("ReviewEntriesTable", () => { test("defaults to en", async () => { await renderReviewEntriesTable(); - // Throws error if no component found with specified `title` prop - renderer.root.findByProps({ title: localizedText["en"] }); + screen.getByLabelText(localizedText["en"]); // throws if none }); Object.entries(localizedText).forEach(([lang, text]) => { test(lang, async () => { await renderReviewEntriesTable(false, false, lang); - // Throws error if no component found with specified `title` prop - renderer.root.findByProps({ title: text }); + screen.getByLabelText(text); // throws if none }); }); });