diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog.tsx index 595f49537e..002e5e39a5 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog.tsx @@ -15,8 +15,10 @@ interface ReplaceDialogProps { open: boolean; dialogFindValue: string; dialogReplaceValue: string; - handleAccept: () => Promise; handleCancel: () => void; + handleConfirm: () => Promise; + idCancel?: string; + idConfirm?: string; } /** @@ -30,7 +32,7 @@ export default function CharacterReplaceDialog( async function submitFindAndReplace(): Promise { setLoading(true); - await props.handleAccept(); + await props.handleConfirm(); setLoading(false); } @@ -56,12 +58,21 @@ export default function CharacterReplaceDialog( - {t("buttons.confirm")} diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/index.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/index.tsx index 3a530af07a..c7b8ebba8e 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/index.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/index.tsx @@ -8,6 +8,13 @@ import { findAndReplace } from "goals/CharacterInventory/CharInv/CharacterDetail import { useAppDispatch } from "types/hooks"; import { TextFieldWithFont } from "utilities/fontComponents"; +const idPrefix = "find-and-replace"; +const fieldIdFind = `${idPrefix}-find-field`; +const fieldIdReplace = `${idPrefix}-replace-field`; +export const buttonIdSubmit = `${idPrefix}-submit-button`; +export const buttonIdCancel = `${idPrefix}-cancel-button`; +export const buttonIdConfirm = `${idPrefix}-confirm-button`; + interface FindAndReplaceProps { initialFindValue: string; } @@ -25,7 +32,7 @@ export default function FindAndReplace( useEffect(() => { setFindValue(props.initialFindValue); setReplaceValue(""); - }, [props.initialFindValue, setFindValue, setReplaceValue]); + }, [props.initialFindValue]); const dispatchFindAndReplace = async (): Promise => { await dispatch(findAndReplace(findValue, replaceValue)).catch(() => @@ -45,6 +52,7 @@ export default function FindAndReplace( {t("charInventory.characterSet.findAndReplace")} setFindValue(e.target.value)} @@ -55,6 +63,7 @@ export default function FindAndReplace( vernacular /> setReplaceValue(e.target.value)} @@ -64,7 +73,11 @@ export default function FindAndReplace( inputProps={{ maxLength: 100 }} vernacular /> - setWarningDialogOpen(false)} - handleAccept={dispatchFindAndReplace} + handleConfirm={dispatchFindAndReplace} + idCancel={buttonIdCancel} + idConfirm={buttonIdConfirm} /> ); diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx new file mode 100644 index 0000000000..a49b42547e --- /dev/null +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx @@ -0,0 +1,129 @@ +import { Provider } from "react-redux"; +import { + ReactTestInstance, + ReactTestRenderer, + act, + create, +} from "react-test-renderer"; +import configureMockStore from "redux-mock-store"; + +import "tests/reactI18nextMock"; + +import CharacterDetail from "goals/CharacterInventory/CharInv/CharacterDetail"; +import { + buttonIdCancel, + buttonIdConfirm, + buttonIdSubmit, +} from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace"; +import CharacterReplaceDialog from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog"; +import { defaultState } from "goals/CharacterInventory/Redux/CharacterInventoryReducer"; +import { StoreState } from "types"; + +// Dialog uses portals, which are not supported in react-test-renderer. +jest.mock("@mui/material", () => { + const materialUiCore = jest.requireActual("@mui/material"); + return { + ...jest.requireActual("@mui/material"), + Dialog: materialUiCore.Container, + }; +}); + +jest.mock( + "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/FindAndReplaceActions", + () => ({ + findAndReplace: () => mockFindAndReplace(), + }) +); +jest.mock("types/hooks", () => { + return { + ...jest.requireActual("types/hooks"), + useAppDispatch: () => (args: any) => Promise.resolve(args), + }; +}); + +const mockClose = jest.fn(); +const mockFindAndReplace = jest.fn(); + +let charMaster: ReactTestRenderer; + +const mockChar = "#"; +// mockPrefix is a single character whose only appearance in the component +// is in an example of a word containing the mockChar. +const mockPrefix = "@"; +const mockWord = mockPrefix + mockChar; +const mockState: Partial = { + characterInventoryState: { ...defaultState, allWords: [mockWord] }, +}; +const mockStore = configureMockStore()(mockState); + +async function renderCharacterDetail(): Promise { + await act(async () => { + charMaster = create( + + + + ); + }); +} + +const hasText = (item: ReactTestInstance, text: string): boolean => { + const found = item.findAll( + (node) => node.children.length === 1 && node.children[0] === text + ); + return found.length !== 0; +}; + +beforeEach(async () => { + jest.resetAllMocks(); + await renderCharacterDetail(); +}); + +describe("CharacterDetail", () => { + it("renders with example word", () => { + expect(hasText(charMaster.root, mockPrefix)).toBeTruthy(); + }); + + describe("FindAndReplace", () => { + it("has working dialog", async () => { + const dialog = charMaster.root.findByType(CharacterReplaceDialog); + const submitButton = charMaster.root.findByProps({ id: buttonIdSubmit }); + const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); + const confButton = charMaster.root.findByProps({ id: buttonIdConfirm }); + + expect(dialog.props.open).toBeFalsy(); + await act(async () => { + submitButton.props.onClick(); + }); + expect(dialog.props.open).toBeTruthy(); + await act(async () => { + cancelButton.props.onClick(); + }); + expect(dialog.props.open).toBeFalsy(); + await act(async () => { + submitButton.props.onClick(); + }); + expect(dialog.props.open).toBeTruthy(); + await act(async () => { + await confButton.props.onClick(); + }); + expect(dialog.props.open).toBeFalsy(); + }); + + it("only submits after confirmation", async () => { + const submitButton = charMaster.root.findByProps({ id: buttonIdSubmit }); + const cancelButton = charMaster.root.findByProps({ id: buttonIdCancel }); + const confButton = charMaster.root.findByProps({ id: buttonIdConfirm }); + + await act(async () => { + submitButton.props.onClick(); + cancelButton.props.onClick(); + submitButton.props.onClick(); + }); + expect(mockFindAndReplace).not.toBeCalled(); + await act(async () => { + await confButton.props.onClick(); + }); + expect(mockFindAndReplace).toBeCalled(); + }); + }); +});