From 3faa52ded549e8d9bbf6f5f82a17e157277a1427 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 18 Jun 2025 13:40:07 -0400 Subject: [PATCH 1/3] [CharInv] Migrate from deprecated Grid --- .../CharInv/CharacterDetail/index.tsx | 30 +-- .../CharInv/CharacterEntry.tsx | 194 ++++++++++++++---- .../CharInv/CharacterList/index.tsx | 103 +++++----- .../CharInv/CharacterSetHeader.tsx | 28 ++- .../CharacterInventory/CharInv/index.tsx | 142 ++----------- .../CharInv/tests/index.test.tsx | 33 +-- 6 files changed, 272 insertions(+), 258 deletions(-) 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..3e828ff944 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx @@ -1,17 +1,39 @@ 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", + DialogCancelText = "char-inv-cancel-dialog-text", + DialogCancelTitle = "char-inv-cancel-dialog-title", +} + /** * Allows for viewing and entering accepted and rejected characters in a * character set @@ -23,56 +45,89 @@ 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))} + /> + + ); } @@ -95,10 +150,59 @@ function CharactersInput(props: CharactersInputProps): ReactElement { 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/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..620726a307 100644 --- a/src/goals/CharacterInventory/CharInv/tests/index.test.tsx +++ b/src/goals/CharacterInventory/CharInv/tests/index.test.tsx @@ -2,13 +2,8 @@ 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 CharInv from "goals/CharacterInventory/CharInv"; +import { CharInvCancelSaveIds } from "goals/CharacterInventory/CharInv/CharacterEntry"; import { defaultState as characterInventoryState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; // Replace Dialog with something that doesn't create portals, @@ -63,30 +58,42 @@ describe("CharInv", () => { it("saves inventory on save", async () => { expect(mockUploadInventory).toHaveBeenCalledTimes(0); - const saveButton = charMaster.root.findByProps({ id: buttonIdSave }); + const saveButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.ButtonSave, + }); await act(async () => saveButton.props.onClick()); expect(mockUploadInventory).toHaveBeenCalledTimes(1); }); it("opens a dialogue on cancel, closes on no", async () => { - const cancelDialog = charMaster.root.findByProps({ id: dialogIdCancel }); + const cancelDialog = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.DialogCancel, + }); expect(cancelDialog.props.open).toBeFalsy(); - const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); + const cancelButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.ButtonCancel, + }); await act(async () => cancelButton.props.onClick()); expect(cancelDialog.props.open).toBeTruthy(); - const noButton = charMaster.root.findByProps({ id: dialogButtonIdNo }); + const noButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.DialogCancelButtonNo, + }); await act(async () => noButton.props.onClick()); expect(cancelDialog.props.open).toBeFalsy(); }); it("exits on cancel-yes", async () => { - const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); + const cancelButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.ButtonCancel, + }); await act(async () => cancelButton.props.onClick()); expect(mockExit).toHaveBeenCalledTimes(0); - const yesButton = charMaster.root.findByProps({ id: dialogButtonIdYes }); + const yesButton = charMaster.root.findByProps({ + id: CharInvCancelSaveIds.DialogCancelButtonYes, + }); await act(async () => yesButton.props.onClick()); expect(mockExit).toHaveBeenCalledTimes(1); }); From c5fe9efba62c6a033db2f532f5131d4290719d2b Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 18 Jun 2025 16:09:12 -0400 Subject: [PATCH 2/3] [CharInv] Move testing to @testing-library --- .../CharInv/CharacterEntry.tsx | 21 ++- .../CharInv/tests/index.test.tsx | 73 +++----- .../CharacterInventory/CharInvCompleted.tsx | 57 +++--- .../tests/CharInvCompleted.test.tsx | 172 ++++++++---------- 4 files changed, 145 insertions(+), 178 deletions(-) diff --git a/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx b/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx index 3e828ff944..1854b75dfc 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterEntry.tsx @@ -30,8 +30,6 @@ export enum CharInvCancelSaveIds { DialogCancel = "char-inv-cancel-dialog", DialogCancelButtonNo = "char-inv-cancel-dialog-no-button", DialogCancelButtonYes = "char-inv-cancel-dialog-yes-button", - DialogCancelText = "char-inv-cancel-dialog-text", - DialogCancelTitle = "char-inv-cancel-dialog-title", } /** @@ -69,6 +67,7 @@ export default function CharacterEntry(): ReactElement { {/* Save button */} save(), }} @@ -80,6 +79,7 @@ export default function CharacterEntry(): ReactElement { {/* Cancel button */}