diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx index 8580c3b240..c66b3541bc 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/index.tsx @@ -1,5 +1,5 @@ import { Close } from "@mui/icons-material"; -import { Grid, IconButton, Typography } from "@mui/material"; +import { Grid2, IconButton, Typography } from "@mui/material"; import { ReactElement } from "react"; import CharacterInfo from "goals/CharacterInventory/CharInv/CharacterDetail/CharacterInfo"; @@ -18,7 +18,7 @@ export default function CharacterDetail( props: CharacterDetailProps ): ReactElement { return ( - - + {props.character} {""} {/* There is a zero-width joiner here in case of non-printing characters. */} - - + + - - + + props.close()} size="large"> {" "} - - + + - - + + - - + + - - + + ); } diff --git a/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx b/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx index d342dd16a2..1854b75dfc 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx @@ -1,17 +1,37 @@ import { KeyboardArrowDown } from "@mui/icons-material"; -import { Button, Collapse, Grid } from "@mui/material"; +import { + Button, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Grid2, +} from "@mui/material"; import { ReactElement, ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; +import { LoadingButton } from "components/Buttons"; import { + exit, setRejectedCharacters, setValidCharacters, + uploadInventory, } from "goals/CharacterInventory/Redux/CharacterInventoryActions"; import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; import theme from "types/theme"; import { TextFieldWithFont } from "utilities/fontComponents"; +export enum CharInvCancelSaveIds { + ButtonCancel = "char-inv-cancel-button", + ButtonSave = "char-inv-save-button", + DialogCancel = "char-inv-cancel-dialog", + DialogCancelButtonNo = "char-inv-cancel-dialog-no-button", + DialogCancelButtonYes = "char-inv-cancel-dialog-yes-button", +} + /** * Allows for viewing and entering accepted and rejected characters in a * character set @@ -23,56 +43,91 @@ export default function CharacterEntry(): ReactElement { (state: StoreState) => state.characterInventoryState ); - const [checked, setChecked] = useState(false); + const [advancedOpen, setAdvancedOpen] = useState(false); + const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const [saveInProgress, setSaveInProgress] = useState(false); const { t } = useTranslation(); + const save = async (): Promise => { + setSaveInProgress(true); + await dispatch(uploadInventory()); + }; + return ( - - - + + {/* Cancel yes/no dialog */} + setCancelDialogOpen(false)} + open={cancelDialogOpen} /> - - - {/* Input for accepted characters */} - - dispatch(setValidCharacters(chars))} - /> - - - {/* Input for rejected characters */} - - dispatch(setRejectedCharacters(chars))} + + + {/* Advanced toggle-button */} + + + + + {/* Input for accepted characters */} + dispatch(setValidCharacters(chars))} + /> + + {/* Input for rejected characters */} + dispatch(setRejectedCharacters(chars))} + /> + + ); } @@ -89,16 +144,68 @@ function CharactersInput(props: CharactersInputProps): ReactElement { autoComplete="off" fullWidth id={props.id} - inputProps={{ spellCheck: false, style: { letterSpacing: 5 } }} + inputProps={{ + "data-testid": props.id, + spellCheck: false, + style: { letterSpacing: 5 }, + }} label={props.label} name="characters" onChange={(e) => props.setCharacters(e.target.value.replace(/\s/g, "").split("")) } - style={{ maxWidth: 512, marginTop: theme.spacing(1) }} + style={{ marginTop: theme.spacing(2) }} value={props.characters.join("")} variant="outlined" vernacular /> ); } + +interface CancelDialogProps { + open: boolean; + onClose: () => void; +} + +/** "Are you sure?" dialog for the cancel button */ +function CancelDialog(props: CancelDialogProps): ReactElement { + const { t } = useTranslation(); + + return ( + props.onClose()} + open={props.open} + > + {t("charInventory.dialog.title")} + + + + {t("charInventory.dialog.content")} + + + + + + + + + + ); +} diff --git a/src/goals/CharacterInventory/CharInv/CharacterList/CharacterStatusText.tsx b/src/goals/CharacterInventory/CharInv/CharacterList/CharacterStatusText.tsx index a57a44c8db..90d8d1990b 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterList/CharacterStatusText.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterList/CharacterStatusText.tsx @@ -3,7 +3,6 @@ import { type ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { CharacterStatus } from "goals/CharacterInventory/CharacterInventoryTypes"; -import { themeColors } from "types/theme"; interface CharacterStatusTextProps { status: CharacterStatus; @@ -17,24 +16,22 @@ export default function CharacterStatusText( return ( {t(`buttons.${props.status}`)} ); } -function CharacterStatusStyle(status: CharacterStatus): { color: string } { +function CharStatusColor(status: CharacterStatus): string { switch (status) { case CharacterStatus.Accepted: - return { color: themeColors.success }; + return "success.main"; case CharacterStatus.Rejected: - return { color: themeColors.error }; + return "error.main"; case CharacterStatus.Undecided: - return { color: themeColors.primary }; + return "primary.main"; } } diff --git a/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx b/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx index 5608e58b5c..921c1989f0 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterList/index.tsx @@ -1,5 +1,11 @@ import { ArrowDownward, ArrowUpward } from "@mui/icons-material"; -import { FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material"; +import { + FormControl, + Grid2, + InputLabel, + MenuItem, + Select, +} from "@mui/material"; import { ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -42,53 +48,54 @@ export default function CharacterList(): ReactElement { return ( <> - - - - {t("charInventory.sortBy")} - - - - - { - /* The grid of character tiles */ - orderedChars.map((character) => ( - selectCharacter(character.character)} - fontHeight={fontHeight} - cardWidth={cardWidth} - /> - )) - } + + + {t("charInventory.sortBy")} + + + + + + { + /* The grid of character tiles */ + orderedChars.map((character) => ( + selectCharacter(character.character)} + fontHeight={fontHeight} + cardWidth={cardWidth} + /> + )) + } + ); } diff --git a/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx b/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx index 3820c844fe..db9bdbdd35 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterSetHeader.tsx @@ -1,5 +1,5 @@ import { Help } from "@mui/icons-material"; -import { Grid, Tooltip, Typography } from "@mui/material"; +import { Tooltip, Typography } from "@mui/material"; import { ReactElement } from "react"; import { useTranslation } from "react-i18next"; @@ -9,20 +9,18 @@ export default function CharacterSetHeader(): ReactElement { const { t } = useTranslation(); return ( - - + {t("charInventory.characterSet.title")}{" "} + - {t("charInventory.characterSet.title")}{" "} - - - - - + + + ); } diff --git a/src/goals/CharacterInventory/CharInv/index.tsx b/src/goals/CharacterInventory/CharInv/index.tsx index f7d8a06a99..ddb85da517 100644 --- a/src/goals/CharacterInventory/CharInv/index.tsx +++ b/src/goals/CharacterInventory/CharInv/index.tsx @@ -1,39 +1,17 @@ -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid, -} from "@mui/material"; -import { ReactElement, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Grid2, Stack } from "@mui/material"; +import { ReactElement, useEffect } from "react"; -import { LoadingButton } from "components/Buttons"; import CharacterDetail from "goals/CharacterInventory/CharInv/CharacterDetail"; import CharacterEntry from "goals/CharacterInventory/CharInv/CharacterEntry"; import CharacterList from "goals/CharacterInventory/CharInv/CharacterList"; import CharacterSetHeader from "goals/CharacterInventory/CharInv/CharacterSetHeader"; import { - exit, loadCharInvData, resetCharInv, setSelectedCharacter, - uploadInventory, } from "goals/CharacterInventory/Redux/CharacterInventoryActions"; import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; -import theme from "types/theme"; - -const idPrefix = "character-inventory"; -export const buttonIdCancel = `${idPrefix}-cancel-button`; -export const buttonIdSave = `${idPrefix}-save-button`; -export const dialogButtonIdNo = `${idPrefix}-cancel-dialog-no-button`; -export const dialogButtonIdYes = `${idPrefix}-cancel-dialog-yes-button`; -export const dialogIdCancel = `${idPrefix}-cancel-dialog`; -const dialogTextIdCancel = `${idPrefix}-cancel-dialog-text`; -const dialogTitleIdCancel = `${idPrefix}-cancel-dialog-title`; /** * Allows users to define a character inventory for a project @@ -45,11 +23,6 @@ export default function CharacterInventory(): ReactElement { (state: StoreState) => state.characterInventoryState.selectedCharacter ); - const [cancelDialogOpen, setCancelDialogOpen] = useState(false); - const [saveInProgress, setSaveInProgress] = useState(false); - - const { t } = useTranslation(); - useEffect(() => { dispatch(loadCharInvData()); @@ -57,99 +30,24 @@ export default function CharacterInventory(): ReactElement { () => dispatch(resetCharInv()); }, [dispatch]); - const save = async (): Promise => { - setSaveInProgress(true); - await dispatch(uploadInventory()); - }; - return ( - <> - - - - - - - - - - - {!!selectedCharacter && ( - dispatch(setSelectedCharacter(""))} - /> - )} - - - - {/* Submission buttons */} - - save(), - style: { margin: theme.spacing(1) }, - }} - > - {t("buttons.save")} - - - - - - - {/* "Are you sure?" dialog for the cancel button */} - setCancelDialogOpen(false)} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - - {t("charInventory.dialog.title")} - - - - {t("charInventory.dialog.content")} - - - - - - - - + + + + + + + + + + {!!selectedCharacter && ( + + dispatch(setSelectedCharacter(""))} + /> + + )} + ); } diff --git a/src/goals/CharacterInventory/CharInv/tests/index.test.tsx b/src/goals/CharacterInventory/CharInv/tests/index.test.tsx index e59662d4b2..64205e6301 100644 --- a/src/goals/CharacterInventory/CharInv/tests/index.test.tsx +++ b/src/goals/CharacterInventory/CharInv/tests/index.test.tsx @@ -1,28 +1,20 @@ +import { + act, + render, + screen, + waitForElementToBeRemoved, +} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; -import { ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; -import CharInv, { - buttonIdCancel, - buttonIdSave, - dialogButtonIdNo, - dialogButtonIdYes, - dialogIdCancel, -} from "goals/CharacterInventory/CharInv"; -import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; +import CharInv from "goals/CharacterInventory/CharInv"; +import { CharInvCancelSaveIds } from "goals/CharacterInventory/CharInv/CharacterEntry"; +import { defaultState } from "rootRedux/types"; -// Replace Dialog with something that doesn't create portals, -// because react-test-renderer does not support portals. -jest.mock("@mui/material/Dialog", () => - jest.requireActual("@mui/material/Container") -); - -jest.mock("goals/CharacterInventory/CharInv/CharacterDetail", () => "div"); jest.mock("goals/CharacterInventory/Redux/CharacterInventoryActions", () => ({ exit: () => mockExit(), loadCharInvData: () => mockLoadCharInvData(), - reset: () => jest.fn(), - setSelectedCharacter: () => mockSetSelectedCharacter(), uploadInventory: () => mockUploadInventory(), })); jest.mock("rootRedux/hooks", () => { @@ -34,17 +26,12 @@ jest.mock("rootRedux/hooks", () => { const mockExit = jest.fn(); const mockLoadCharInvData = jest.fn(); -const mockSetSelectedCharacter = jest.fn(); const mockUploadInventory = jest.fn(); -let charMaster: ReactTestRenderer; - -const mockStore = configureMockStore()({ characterInventoryState }); - async function renderCharInvCreation(): Promise { await act(async () => { - charMaster = create( - + render( + ); @@ -63,31 +50,34 @@ describe("CharInv", () => { it("saves inventory on save", async () => { expect(mockUploadInventory).toHaveBeenCalledTimes(0); - const saveButton = charMaster.root.findByProps({ id: buttonIdSave }); - await act(async () => saveButton.props.onClick()); + await userEvent.click(screen.getByTestId(CharInvCancelSaveIds.ButtonSave)); expect(mockUploadInventory).toHaveBeenCalledTimes(1); }); it("opens a dialogue on cancel, closes on no", async () => { - const cancelDialog = charMaster.root.findByProps({ id: dialogIdCancel }); - expect(cancelDialog.props.open).toBeFalsy(); - - const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); - await act(async () => cancelButton.props.onClick()); - expect(cancelDialog.props.open).toBeTruthy(); + expect(screen.queryByRole("dialog")).toBeNull(); + await userEvent.click( + screen.getByTestId(CharInvCancelSaveIds.ButtonCancel) + ); + expect(screen.queryByRole("dialog")).toBeTruthy(); - const noButton = charMaster.root.findByProps({ id: dialogButtonIdNo }); - await act(async () => noButton.props.onClick()); - expect(cancelDialog.props.open).toBeFalsy(); + await userEvent.click( + screen.getByTestId(CharInvCancelSaveIds.DialogCancelButtonNo) + ); + // Wait for dialog removal, else it's only hidden. + await waitForElementToBeRemoved(() => screen.queryByRole("dialog")); + expect(screen.queryByRole("dialog")).toBeNull(); }); it("exits on cancel-yes", async () => { - const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); - await act(async () => cancelButton.props.onClick()); + await userEvent.click( + screen.getByTestId(CharInvCancelSaveIds.ButtonCancel) + ); expect(mockExit).toHaveBeenCalledTimes(0); - const yesButton = charMaster.root.findByProps({ id: dialogButtonIdYes }); - await act(async () => yesButton.props.onClick()); + await userEvent.click( + screen.getByTestId(CharInvCancelSaveIds.DialogCancelButtonYes) + ); expect(mockExit).toHaveBeenCalledTimes(1); }); }); diff --git a/src/goals/CharacterInventory/CharInvCompleted.tsx b/src/goals/CharacterInventory/CharInvCompleted.tsx index 5dd6ae5ed3..68f3b1a3f5 100644 --- a/src/goals/CharacterInventory/CharInvCompleted.tsx +++ b/src/goals/CharacterInventory/CharInvCompleted.tsx @@ -23,11 +23,14 @@ import { import { useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; -export enum CharInvCompletedId { - TypographyNoCharChanges = "no-char-changes-typography", - TypographyNoWordChanges = "no-word-changes-typography", - TypographyWordChanges = "word-changes-typography", - TypographyWordsUndo = "words-undo-typography", +export enum CharInvCompletedTextId { + CharChangesMore = "charInventory.changes.more", + CharChangesNone = "charInventory.changes.noCharChanges", + Title = "charInventory.title", + WordChanges = "charInventory.changes.wordChanges", + WordChangesNone = "charInventory.changes.noWordChangesFindReplace", + WordChangesUndo = "charInventory.undo.undoWords", + WordChangesWithString = "charInventory.changes.wordChangesWithStrings", } /** Component to display the full details of changes made during one session of the @@ -46,16 +49,16 @@ export default function CharInvCompleted(): ReactElement { {/* Title */} - {t("charInventory.title")} + {t(CharInvCompletedTextId.Title)} {/* Inventory changes */} -
+
{changes.charChanges.length ? ( changes.charChanges.map((c) => ) ) : ( - - {t("charInventory.changes.noCharChanges")} + + {t(CharInvCompletedTextId.CharChangesNone)} )}
@@ -71,9 +74,7 @@ export default function CharInvCompleted(): ReactElement { ) : ( <> - - {t("charInventory.changes.noWordChangesFindReplace")} - + {t(CharInvCompletedTextId.WordChangesNone)} )} @@ -97,16 +98,14 @@ function WordChangesTypography(props: { const wordCount = changes.flatMap((wc) => Object.keys(wc.words)).length; const description = changes.length === 1 - ? t("charInventory.changes.wordChangesWithStrings", { + ? t(CharInvCompletedTextId.WordChangesWithString, { val1: changes[0].find, val2: changes[0].replace, }) - : t("charInventory.changes.wordChanges"); + : t(CharInvCompletedTextId.WordChanges); return ( - - {`${description} ${wordCount}`} - + {`${description} ${wordCount}`} ); } @@ -121,11 +120,9 @@ export function CharInvChangesGoalList(changes: CharInvChanges): ReactElement { const noChanges = !(charChanges?.length || wordChanges?.length); return noChanges ? ( - - {t("charInventory.changes.noCharChanges")} - + {t(CharInvCompletedTextId.CharChangesNone)} ) : ( - + @@ -134,7 +131,7 @@ export function CharInvChangesGoalList(changes: CharInvChanges): ReactElement { /** Components summarizing changes to status of inventory characters * (or null if no status changes). */ -export function CharChangesRows(props: { +function CharChangesRows(props: { changeLimit: number; charChanges?: CharacterChange[]; }): ReactElement | null { @@ -150,9 +147,9 @@ export function CharChangesRows(props: { {charChanges.slice(0, changeLimit - 1).map((c) => ( ))} - + {`+${charChanges.length - (changeLimit - 1)} `} - {t("charInventory.changes.more")} + {t(CharInvCompletedTextId.CharChangesMore)} ) : ( @@ -167,7 +164,7 @@ export function CharChangesRows(props: { /** One-line display of the inventory status change of a character. */ export function CharChange(props: { change: CharacterChange }): ReactElement { return ( -
+
{`${props.change[0]}: `} @@ -189,8 +186,8 @@ function WordChanges(props: { const wordLimit = 5; const undoWordsTypography = inFrontier.length ? ( - - {t("charInventory.undo.undoWords", { + + {t(CharInvCompletedTextId.WordChangesUndo, { val1: inFrontier.length, val2: entries.length, })} @@ -212,8 +209,8 @@ function WordChanges(props: { return (
- - {t("charInventory.changes.wordChangesWithStrings", { + + {t(CharInvCompletedTextId.WordChangesWithString, { val1: props.wordChanges.find, val2: props.wordChanges.replace, })} @@ -225,7 +222,7 @@ function WordChanges(props: { {entries.length > wordLimit ? ( {`+${entries.length - wordLimit} ${t( - "charInventory.changes.more" + CharInvCompletedTextId.CharChangesMore )}`} ) : null} diff --git a/src/goals/CharacterInventory/tests/CharInvCompleted.test.tsx b/src/goals/CharacterInventory/tests/CharInvCompleted.test.tsx index ef63cdfba5..c1c289eeaa 100644 --- a/src/goals/CharacterInventory/tests/CharInvCompleted.test.tsx +++ b/src/goals/CharacterInventory/tests/CharInvCompleted.test.tsx @@ -1,17 +1,10 @@ +import { act, render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; -import { - type ReactTestInstance, - type ReactTestRenderer, - act, - create, -} from "react-test-renderer"; import configureMockStore from "redux-mock-store"; -import WordCard from "components/WordCard"; import CharInvCompleted, { - CharChange, CharInvChangesGoalList, - CharInvCompletedId, + CharInvCompletedTextId, } from "goals/CharacterInventory/CharInvCompleted"; import { type CharInvChanges, @@ -29,8 +22,7 @@ jest.mock("backend", () => ({ getWord: () => Promise.resolve(mockWord()), updateWord: () => jest.fn(), })); -// Mock "i18n", else `thrown: "Error: Error: connect ECONNREFUSED ::1:80 [...]` -jest.mock("i18n", () => ({})); +jest.mock("i18n", () => ({})); // else `thrown: "Error: AggregateError` const mockCharChanges: CharacterChange[] = [ ["a", CharacterStatus.Accepted, CharacterStatus.Rejected], @@ -40,15 +32,11 @@ const mockCharChanges: CharacterChange[] = [ ["e", CharacterStatus.Undecided, CharacterStatus.Accepted], ["f", CharacterStatus.Undecided, CharacterStatus.Rejected], ]; -const mockWordKeys = ["oldA", "oldB", "oldC"]; +const mockWordKeys = ["oldA", "oldB"]; const mockWordChanges: FindAndReplaceChange = { find: "Q", replace: "q", - words: { - [mockWordKeys[0]]: "newA", - [mockWordKeys[1]]: "newB", - [mockWordKeys[2]]: "newC", - }, + words: { [mockWordKeys[0]]: "newA", [mockWordKeys[1]]: "newB" }, }; const mockState = (changes?: CharInvChanges): Partial => ({ goalsState: { @@ -57,115 +45,111 @@ const mockState = (changes?: CharInvChanges): Partial => ({ }, }); -let renderer: ReactTestRenderer; -let root: ReactTestInstance; - beforeEach(() => { jest.resetAllMocks(); }); describe("CharInvCompleted", () => { const renderCharInvCompleted = async ( - changes?: CharInvChanges + changes?: Partial ): Promise => { + const charInvChanges = { ...defaultCharInvChanges, ...changes }; await act(async () => { - renderer = create( - + render( + ); }); - root = renderer.root; }; - it("renders all char inv changes", async () => { - await renderCharInvCompleted({ - ...defaultCharInvChanges, - charChanges: mockCharChanges, - }); - expect(root.findAllByType(CharChange)).toHaveLength(mockCharChanges.length); - expect(root.findAllByType(WordCard)).toHaveLength(0); - - expect(() => - root.findByProps({ id: CharInvCompletedId.TypographyNoCharChanges }) - ).toThrow(); - root.findByProps({ id: CharInvCompletedId.TypographyNoWordChanges }); - expect(() => - root.findByProps({ id: CharInvCompletedId.TypographyWordChanges }) - ).toThrow(); - }); + it("renders char changes", async () => { + await renderCharInvCompleted({ charChanges: mockCharChanges }); - it("renders all words changed", async () => { - await renderCharInvCompleted({ - ...defaultCharInvChanges, - wordChanges: [mockWordChanges], - }); - expect(root.findAllByType(CharChange)).toHaveLength(0); - expect(renderer.root.findAllByType(WordCard)).toHaveLength( - mockWordKeys.length + // One listitem per char-change. + expect(screen.getAllByRole("listitem")).toHaveLength( + mockCharChanges.length ); + expect( + screen.queryByText(CharInvCompletedTextId.CharChangesNone) + ).toBeNull(); - root.findByProps({ id: CharInvCompletedId.TypographyNoCharChanges }); - expect(() => - root.findByProps({ id: CharInvCompletedId.TypographyNoWordChanges }) - ).toThrow(); - root.findByProps({ id: CharInvCompletedId.TypographyWordChanges }); + // No word-changes. + expect( + screen.queryAllByText(CharInvCompletedTextId.WordChangesWithString) + ).toHaveLength(0); + expect( + screen.queryByText(CharInvCompletedTextId.WordChangesNone) + ).toBeTruthy(); + }); + + it("renders word changes", async () => { + await renderCharInvCompleted({ wordChanges: [mockWordChanges] }); + + // One listitem for the no-char-change text. + expect(screen.getAllByRole("listitem")).toHaveLength(1); + expect( + screen.queryByText(CharInvCompletedTextId.CharChangesNone) + ).toBeTruthy(); + + // One word-changes. + expect( + screen.queryAllByText(CharInvCompletedTextId.WordChangesWithString) + ).toHaveLength(1); + expect( + screen.queryByText(CharInvCompletedTextId.WordChangesNone) + ).toBeNull(); }); }); describe("CharInvChangesGoalList", () => { + const changeLimit = 3; + const renderCharInvChangesGoalList = async ( - changes?: CharInvChanges + changes?: Partial ): Promise => { await act(async () => { - renderer = create( - CharInvChangesGoalList(changes ?? defaultCharInvChanges) - ); + render(CharInvChangesGoalList({ ...defaultCharInvChanges, ...changes })); }); - root = renderer.root; }; - it("renders up to 3 char changes", async () => { - const changes = (count: number): CharInvChanges => ({ - ...defaultCharInvChanges, - charChanges: mockCharChanges.slice(0, count), - }); - await renderCharInvChangesGoalList(changes(0)); - expect(root.findAllByType(CharChange)).toHaveLength(0); - await renderCharInvChangesGoalList(changes(1)); - expect(root.findAllByType(CharChange)).toHaveLength(1); - await renderCharInvChangesGoalList(changes(3)); - expect(root.findAllByType(CharChange)).toHaveLength(3); + describe(`shows up to ${changeLimit} char changes`, () => { + for (let i = 0; i <= changeLimit; i++) { + test(`shows ${i} char changes`, async () => { + const charChanges = mockCharChanges.slice(0, i); + await renderCharInvChangesGoalList({ charChanges }); + expect(screen.queryAllByRole("listitem")).toHaveLength(i); + const noChanges = screen.queryByText( + CharInvCompletedTextId.CharChangesNone + ); + if (i) { + expect(noChanges).toBeNull(); + } else { + expect(noChanges).toBeTruthy(); + } + }); + } }); - it("doesn't render more than 3 char changes", async () => { - expect(mockCharChanges.length).toBeGreaterThan(3); - await renderCharInvChangesGoalList({ - ...defaultCharInvChanges, - charChanges: mockCharChanges, - }); - // When more than 3 changes, show 2 changes and a "+_ more" line. - expect(root.findAllByType(CharChange)).toHaveLength(2); + it(`shows only ${changeLimit} items when there are more char changes than that`, async () => { + expect(mockCharChanges.length).toBeGreaterThan(changeLimit); + await renderCharInvChangesGoalList({ charChanges: mockCharChanges }); + expect(screen.queryAllByRole("listitem")).toHaveLength(changeLimit); }); - it("doesn't show word changes when there are none", async () => { - await renderCharInvChangesGoalList(defaultCharInvChanges); - expect(() => - root.findByProps({ id: CharInvCompletedId.TypographyNoWordChanges }) - ).toThrow(); - expect(() => - root.findByProps({ id: CharInvCompletedId.TypographyWordChanges }) - ).toThrow(); + it("doesn't show word changes summary item when there are none", async () => { + await renderCharInvChangesGoalList(); + expect(screen.queryAllByRole("listitem")).toHaveLength(0); + expect( + screen.queryByText(CharInvCompletedTextId.CharChangesNone) + ).toBeTruthy(); }); - it("shows word changes when there are some", async () => { - await renderCharInvChangesGoalList({ - ...defaultCharInvChanges, - wordChanges: [mockWordChanges], - }); - expect(() => - root.findByProps({ id: CharInvCompletedId.TypographyNoWordChanges }) - ).toThrow(); - root.findByProps({ id: CharInvCompletedId.TypographyWordChanges }); + it("shows word changes summary item when there are some", async () => { + await renderCharInvChangesGoalList({ wordChanges: [mockWordChanges] }); + expect(screen.queryAllByRole("listitem")).toHaveLength(1); + expect( + screen.queryByText(CharInvCompletedTextId.CharChangesNone) + ).toBeNull(); }); });