From 5e65e16de2d708315ff7c5ee78b07d7eff7c390e Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Sep 2025 15:09:41 -0400 Subject: [PATCH 1/3] Retire InputLabelProps, InputProps --- .../Dialogs/DeleteEditTextDialog.tsx | 3 +- src/components/Dialogs/EditTextDialog.tsx | 3 +- src/components/Dialogs/SubmitTextDialog.tsx | 3 +- .../ProjectUsers/ProjectSpeakersList.tsx | 12 ------- .../tests/ProjectSpeakersList.test.tsx | 32 ++++++------------- src/components/TreeView/TreeSearch.tsx | 5 +-- .../TreeView/tests/TreeSearch.test.tsx | 20 ++++++------ src/utilities/fontComponents.tsx | 17 ++++++---- 8 files changed, 34 insertions(+), 61 deletions(-) diff --git a/src/components/Dialogs/DeleteEditTextDialog.tsx b/src/components/Dialogs/DeleteEditTextDialog.tsx index 2039906397..afb9261679 100644 --- a/src/components/Dialogs/DeleteEditTextDialog.tsx +++ b/src/components/Dialogs/DeleteEditTextDialog.tsx @@ -123,8 +123,7 @@ export default function DeleteEditTextDialog( value={text} onChange={(event) => setText(event.target.value)} onKeyPress={confirmIfEnter} - InputProps={{ endAdornment }} - inputProps={{ "data-testid": props.textFieldId }} + slotProps={{ input: { endAdornment } }} id={props.textFieldId} /> diff --git a/src/components/Dialogs/EditTextDialog.tsx b/src/components/Dialogs/EditTextDialog.tsx index 7bf48c4324..5ca977ed1b 100644 --- a/src/components/Dialogs/EditTextDialog.tsx +++ b/src/components/Dialogs/EditTextDialog.tsx @@ -101,8 +101,7 @@ export default function EditTextDialog( value={text} onChange={(event) => setText(event.target.value)} onKeyPress={confirmIfEnter} - InputProps={{ endAdornment }} - inputProps={{ "data-testid": props.textFieldId }} + slotProps={{ input: { endAdornment } }} id={props.textFieldId} /> diff --git a/src/components/Dialogs/SubmitTextDialog.tsx b/src/components/Dialogs/SubmitTextDialog.tsx index 9cb04562ed..4e2e97ef0d 100644 --- a/src/components/Dialogs/SubmitTextDialog.tsx +++ b/src/components/Dialogs/SubmitTextDialog.tsx @@ -90,8 +90,7 @@ export default function SubmitTextDialog( value={text} onChange={(event) => setText(event.target.value)} onKeyPress={confirmIfEnter} - InputProps={{ endAdornment }} - inputProps={{ "data-testid": props.textFieldId }} + slotProps={{ input: { endAdornment } }} id={props.textFieldId} /> diff --git a/src/components/ProjectUsers/ProjectSpeakersList.tsx b/src/components/ProjectUsers/ProjectSpeakersList.tsx index 6119ebe842..bb75049927 100644 --- a/src/components/ProjectUsers/ProjectSpeakersList.tsx +++ b/src/components/ProjectUsers/ProjectSpeakersList.tsx @@ -19,13 +19,7 @@ import SpeakerConsentListItemIcon from "components/ProjectUsers/SpeakerConsentLi export enum ProjectSpeakersId { ButtonAdd = "speaker-add-button", - ButtonAddCancel = "speaker-add-cancel-button", - ButtonAddConfirm = "speaker-add-confirm-button", - ButtonDeleteCancel = "speaker-delete-cancel-button", - ButtonDeleteConfirm = "speaker-delete-confirm-button", ButtonDeletePrefix = "speaker-delete-button-", - ButtonEditCancel = "speaker-edit-cancel-button", - ButtonEditConfirm = "speaker-edit-confirm-button", ButtonEditPrefix = "speaker-edit-button-", TextFieldAdd = "speaker-add-textfield", TextFieldEdit = "speaker-edit-textfield", @@ -113,8 +107,6 @@ function EditSpeakerNameListItemIcon(props: ProjSpeakerProps): ReactElement { /> {open && ( setOpen(false)} open={open} text={props.speaker.name} @@ -137,8 +129,6 @@ function DeleteSpeakerListItemIcon(props: ProjSpeakerProps): ReactElement { setOpen(false)} open={open} submitText={handleSubmitText} diff --git a/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx b/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx index 0920f25fdf..718a8ff42f 100644 --- a/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx +++ b/src/components/ProjectUsers/tests/ProjectSpeakersList.test.tsx @@ -1,5 +1,5 @@ import "@testing-library/jest-dom"; -import { act, render, screen } from "@testing-library/react"; +import { act, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import ProjectSpeakersList, { @@ -39,6 +39,12 @@ beforeEach(() => { mockUpdateSpeakerName.mockResolvedValue(""); }); +const typeInDialogAndConfirm = async (text: string): Promise => { + const dialog = screen.getByRole("dialog"); + await userEvent.type(within(dialog).getByRole("textbox"), text); + await userEvent.click(within(dialog).getByText("buttons.confirm")); +}; + describe("ProjectSpeakersList", () => { it("shows list item for each speakers, +1 for add-a-speaker", async () => { await renderProjectSpeakersList(); @@ -58,13 +64,7 @@ describe("ProjectSpeakersList", () => { await userEvent.click(editButton); // Add whitespace to the current name - await userEvent.type( - screen.getByTestId(ProjectSpeakersId.TextFieldEdit), - " " - ); - await userEvent.click( - screen.getByTestId(ProjectSpeakersId.ButtonEditConfirm) - ); + await typeInDialogAndConfirm(" "); // Ensure no name update was submitted expect(mockUpdateSpeakerName).not.toHaveBeenCalled(); @@ -73,13 +73,7 @@ describe("ProjectSpeakersList", () => { await userEvent.click(editButton); // Add non-whitespace - await userEvent.type( - screen.getByTestId(ProjectSpeakersId.TextFieldEdit), - "!" - ); - await userEvent.click( - screen.getByTestId(ProjectSpeakersId.ButtonEditConfirm) - ); + await typeInDialogAndConfirm("!"); // Ensure the name update was submitted expect(mockUpdateSpeakerName.mock.calls[0][1]).toEqual(`${speaker.name}!`); @@ -93,13 +87,7 @@ describe("ProjectSpeakersList", () => { // Submit the name of the speaker with extra whitespace const name = "Ms. Nym"; - await userEvent.type( - screen.getByTestId(ProjectSpeakersId.TextFieldAdd), - ` ${name}\t ` - ); - await userEvent.click( - screen.getByTestId(ProjectSpeakersId.ButtonAddConfirm) - ); + await typeInDialogAndConfirm(` ${name}\t `); // Ensure new speaker was submitted with trimmed name expect(mockCreateSpeaker.mock.calls[0][0]).toEqual(name); diff --git a/src/components/TreeView/TreeSearch.tsx b/src/components/TreeView/TreeSearch.tsx index 20b2c23b85..144c4200de 100644 --- a/src/components/TreeView/TreeSearch.tsx +++ b/src/components/TreeView/TreeSearch.tsx @@ -20,8 +20,6 @@ export interface TreeSearchProps { animate: (domain: SemanticDomainTreeNode) => Promise; } -export const testId = "testSearch"; - export default function TreeSearch(props: TreeSearchProps): ReactElement { const { t } = useTranslation(); const { input, handleChange, searchAndSelectDomain, searchError, setInput } = @@ -42,7 +40,6 @@ export default function TreeSearch(props: TreeSearchProps): ReactElement { return ( { jest.clearAllMocks(); }); -function getSearchInput(): HTMLInputElement { - return screen.getByTestId(testId); -} - function setupSpies(domain: SemanticDomainTreeNode | undefined): void { jest.spyOn(backend, "getSemanticDomainTreeNode").mockResolvedValue(domain); jest @@ -110,20 +106,22 @@ describe("TreeSearch", () => { describe("Integration tests, verify component uses hooks to achieve desired UX", () => { it("typing non-matching domain search data does not clear input, or attempt to navigate", async () => { render(); - expect(getSearchInput().value).toEqual(""); + const textField = screen.getByRole("textbox"); + expect(textField).toHaveValue(""); const searchText = "flibbertigibbet"; - await userEvent.type(getSearchInput(), `${searchText}{enter}`); - expect(getSearchInput().value).toEqual(searchText); + await userEvent.type(textField, `${searchText}{enter}`); + expect(textField).toHaveValue(searchText); // verify that no attempt to switch domains happened expect(MOCK_ANIMATE).toHaveBeenCalledTimes(0); }); it("typing valid domain number navigates and clears input", async () => { render(); - expect(getSearchInput().value).toEqual(""); + const textField = screen.getByRole("textbox"); + expect(textField).toHaveValue(""); setupSpies(domMap[mapIds.lastKid]); - await userEvent.type(getSearchInput(), `${mapIds.lastKid}{enter}`); - expect(getSearchInput().value).toEqual(""); + await userEvent.type(textField, `${mapIds.lastKid}{enter}`); + expect(textField).toHaveValue(""); // verify that we would switch to the domain requested expect(MOCK_ANIMATE).toHaveBeenCalledWith(domMap[mapIds.lastKid]); }); diff --git a/src/utilities/fontComponents.tsx b/src/utilities/fontComponents.tsx index 7f235f8384..518c9bae4d 100644 --- a/src/utilities/fontComponents.tsx +++ b/src/utilities/fontComponents.tsx @@ -58,15 +58,20 @@ export function TextFieldWithFont(props: TextFieldWithFontProps): ReactElement { const fontContext = useContext(FontContext); // Use spread to remove the custom props from what is passed into TextField. const { analysis, lang, vernacular, ...textFieldProps } = props; + const input = textFieldProps.slotProps?.input; + const inputProps = typeof input === "function" ? input({}) : input; return ( ); From fe95ff2a32d4bcf743a2da9e9a738507fb618880 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 11 Sep 2025 15:10:28 -0400 Subject: [PATCH 2/3] Retire inputProps --- src/components/Login/Login.tsx | 2 +- src/components/Login/Signup.tsx | 2 +- src/components/Login/tests/Login.test.tsx | 16 ++-- src/components/Login/tests/Signup.test.tsx | 5 +- src/components/PasswordReset/Request.tsx | 52 +++++-------- src/components/PasswordReset/ResetPage.tsx | 38 +++++----- .../PasswordReset/tests/Request.test.tsx | 30 +++----- .../PasswordReset/tests/ResetPage.test.tsx | 49 ++++++++----- .../ProjectSettings/ProjectDomains.tsx | 3 - .../ProjectSettings/ProjectLanguages.tsx | 3 - .../tests/ProjectLanguages.test.tsx | 4 +- .../CharacterDetail/FindAndReplace.tsx | 10 +-- .../CharInv/CharacterEntry.tsx | 73 +++++++------------ .../CharInv/tests/index.test.tsx | 23 +++--- .../Cells/EditCell/EditDialog.tsx | 15 ++-- .../Cells/EditCell/EditSenseDialog.tsx | 12 +-- .../Cells/EditCell/tests/EditDialog.test.tsx | 14 +++- .../EditCell/tests/EditSenseDialog.test.tsx | 15 ++-- 18 files changed, 165 insertions(+), 201 deletions(-) diff --git a/src/components/Login/Login.tsx b/src/components/Login/Login.tsx index b8678cf56a..86aaf89f59 100644 --- a/src/components/Login/Login.tsx +++ b/src/components/Login/Login.tsx @@ -104,9 +104,9 @@ export default function Login(): ReactElement { const defaultTextFieldProps = (id?: string): TextFieldProps => ({ id, - inputProps: { "data-testid": id, maxLength: 100 }, margin: "normal", required: true, + slotProps: { htmlInput: { maxLength: 100 } }, style: { width: "100%" }, variant: "outlined", }); diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx index 37b9480cc4..ef65c0219c 100644 --- a/src/components/Login/Signup.tsx +++ b/src/components/Login/Signup.tsx @@ -177,11 +177,11 @@ export default function Signup(props: SignupProps): ReactElement { const defaultTextFieldProps = (field: SignupField): TextFieldProps => ({ error: fieldError[field], id: signupFieldId[field], - inputProps: { "data-testid": signupFieldId[field], maxLength: 100 }, label: t(signupFieldTextId[field]), margin: "normal", onChange: (e) => updateField(e, field), required: true, + slotProps: { htmlInput: { maxLength: 100 } }, style: { width: "100%" }, value: fieldText[field], variant: "outlined", diff --git a/src/components/Login/tests/Login.test.tsx b/src/components/Login/tests/Login.test.tsx index 654cd65f66..5b7951e211 100644 --- a/src/components/Login/tests/Login.test.tsx +++ b/src/components/Login/tests/Login.test.tsx @@ -39,8 +39,8 @@ const renderLogin = async (): Promise => { }; /** Type the given value into the field with the given ID. */ -const typeInField = async (id: LoginId, value: string): Promise => { - await userEvent.type(screen.getByTestId(id), value); +const typeInField = async (id: LoginTextId, value: string): Promise => { + await userEvent.type(screen.getByLabelText(new RegExp(id)), value); }; /** Click the Login button and confirm whether the field with the given ID has an error. */ @@ -84,22 +84,22 @@ describe("Login", () => { // Don't test with empty username or password, because those prevent submission. it("errors when username is whitespace", async () => { - await typeInField(LoginId.FieldUsername, " "); - await typeInField(LoginId.FieldPassword, "nonempty"); + await typeInField(LoginTextId.LabelUsername, " "); + await typeInField(LoginTextId.LabelPassword, "nonempty"); await loginAndCheckError(LoginId.FieldUsername); }); it("errors when password is whitespace", async () => { - await typeInField(LoginId.FieldUsername, "nonempty"); - await typeInField(LoginId.FieldPassword, " "); + await typeInField(LoginTextId.LabelUsername, "nonempty"); + await typeInField(LoginTextId.LabelPassword, " "); await loginAndCheckError(LoginId.FieldPassword); }); it("submits when username and password are valid", async () => { - await typeInField(LoginId.FieldUsername, "nonempty"); - await typeInField(LoginId.FieldPassword, "nonempty"); + await typeInField(LoginTextId.LabelUsername, "nonempty"); + await typeInField(LoginTextId.LabelPassword, "nonempty"); await loginAndCheckError(); }); diff --git a/src/components/Login/tests/Signup.test.tsx b/src/components/Login/tests/Signup.test.tsx index 7e77e9d3b2..8c4fa15868 100644 --- a/src/components/Login/tests/Signup.test.tsx +++ b/src/components/Login/tests/Signup.test.tsx @@ -8,7 +8,6 @@ import Signup, { SignupField, SignupId, SignupText, - signupFieldId, signupFieldTextId, } from "components/Login/Signup"; import MockCaptcha from "components/Login/tests/MockCaptcha"; @@ -61,8 +60,8 @@ const typeInFields = async (textRecord: Partial): Promise => { if (!text) { continue; } - const id = signupFieldId[field as SignupField]; - await userEvent.type(screen.getByTestId(id), text); + const id = signupFieldTextId[field as SignupField]; + await userEvent.type(screen.getByLabelText(new RegExp(id)), text); } }; diff --git a/src/components/PasswordReset/Request.tsx b/src/components/PasswordReset/Request.tsx index 2e823fd478..778b6995f5 100644 --- a/src/components/PasswordReset/Request.tsx +++ b/src/components/PasswordReset/Request.tsx @@ -17,10 +17,14 @@ import Captcha from "components/Login/Captcha"; import { Path } from "types/path"; import { NormalizedTextField } from "utilities/fontComponents"; -export enum PasswordRequestIds { - ButtonLogin = "password-request-login", - ButtonSubmit = "password-request-submit", - FieldEmailOrUsername = "password-request-text", +export enum ResetRequestTextId { + ButtonSubmit = "passwordReset.submit", + ButtonLogin = "login.backToLogin", + Done = "passwordReset.resetDone", + FieldEmailOrUsername = "passwordReset.emailOrUsername", + FieldEmailOrUsernameError = "passwordReset.resetFail", + Instructions = "passwordReset.resetRequestInstructions", + Title = "passwordReset.resetRequestTitle", } export default function ResetRequest(): ReactElement { @@ -53,7 +57,7 @@ export default function ResetRequest(): ReactElement { - {t("passwordReset.resetRequestTitle")} + {t(ResetRequestTextId.Title)} } /> @@ -61,32 +65,23 @@ export default function ResetRequest(): ReactElement { {isDone ? ( - {t("passwordReset.resetDone")} + {t(ResetRequestTextId.Done)} - ) : (
- - {t("passwordReset.resetRequestInstructions")} - + {t(ResetRequestTextId.Instructions)} setEmailOrUsername(e.target.value)} required value={emailOrUsername} @@ -97,25 +92,18 @@ export default function ResetRequest(): ReactElement { {/* Back-to-login and Submit buttons */} - {t("passwordReset.submit")} + {t(ResetRequestTextId.ButtonSubmit)} diff --git a/src/components/PasswordReset/ResetPage.tsx b/src/components/PasswordReset/ResetPage.tsx index aab3e39799..224170d973 100644 --- a/src/components/PasswordReset/ResetPage.tsx +++ b/src/components/PasswordReset/ResetPage.tsx @@ -18,10 +18,16 @@ import { Path } from "types/path"; import { NormalizedTextField } from "utilities/fontComponents"; import { meetsPasswordRequirements } from "utilities/userUtilities"; -export enum PasswordResetIds { - Password = "PasswordReset.password", - ConfirmPassword = "PasswordReset.confirm-password", - SubmitButton = "PasswordReset.button.submit", +export enum PasswordResetTextId { + ButtonSubmit = "passwordReset.submit", + FieldPassword1 = "login.password", + FieldPassword1Hint = "login.passwordRequirements", + FieldPassword2 = "login.confirmPassword", + FieldPassword2Error = "login.confirmPasswordError", + Invalid = "passwordReset.invalidURL", + ToastSuccess = "passwordReset.resetSuccess", + ToastFail = "passwordReset.resetFail", + Title = "passwordReset.resetTitle", } export default function PasswordReset(): ReactElement { @@ -61,9 +67,9 @@ export default function PasswordReset(): ReactElement { const asyncReset = async (token: string, password: string): Promise => { if (await resetPassword(token, password)) { - toast.success(t("passwordReset.resetSuccess")); + toast.success(t(PasswordResetTextId.ToastSuccess)); } else { - toast.error(t("passwordReset.resetFail")); + toast.error(t(PasswordResetTextId.ToastFail)); } navigate(Path.Login); }; @@ -74,7 +80,7 @@ export default function PasswordReset(): ReactElement { - {t("passwordReset.resetTitle")} + {t(PasswordResetTextId.Title)} } /> @@ -85,11 +91,11 @@ export default function PasswordReset(): ReactElement { error={!passwordFitsRequirements} fullWidth helperText={ - !passwordFitsRequirements && t("login.passwordRequirements") + !passwordFitsRequirements && + t(PasswordResetTextId.FieldPassword1Hint) } id="password-reset-password1" - inputProps={{ "data-testid": PasswordResetIds.Password }} - label={t("login.password")} + label={t(PasswordResetTextId.FieldPassword1)} onChange={(e) => onChangePassword(e.target.value, passwordConfirm) } @@ -103,30 +109,26 @@ export default function PasswordReset(): ReactElement { helperText={ !isPasswordConfirmed && passwordConfirm.length > 0 && - t("login.confirmPasswordError") + t(PasswordResetTextId.FieldPassword2Error) } - id={PasswordResetIds.ConfirmPassword} - inputProps={{ "data-testid": PasswordResetIds.ConfirmPassword }} - label={t("login.confirmPassword")} + label={t(PasswordResetTextId.FieldPassword2)} onChange={(e) => onChangePassword(password, e.target.value)} type="password" value={passwordConfirm} /> ) : ( - + ); } diff --git a/src/components/PasswordReset/tests/Request.test.tsx b/src/components/PasswordReset/tests/Request.test.tsx index 30014a10a3..34ef01ff5d 100644 --- a/src/components/PasswordReset/tests/Request.test.tsx +++ b/src/components/PasswordReset/tests/Request.test.tsx @@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event"; import MockCaptcha from "components/Login/tests/MockCaptcha"; import ResetRequest, { - PasswordRequestIds, + ResetRequestTextId, } from "components/PasswordReset/Request"; jest.mock("react-router", () => ({ @@ -40,14 +40,13 @@ describe("ResetRequest", () => { await renderUserSettings(); // Before - const login = screen.getByTestId(PasswordRequestIds.ButtonLogin); - const submit = screen.getByTestId(PasswordRequestIds.ButtonSubmit); + const login = screen.getByText(ResetRequestTextId.ButtonLogin); + const submit = screen.getByText(ResetRequestTextId.ButtonSubmit); expect(login).toBeEnabled(); expect(submit).toBeDisabled(); // Agent - const field = screen.getByTestId(PasswordRequestIds.FieldEmailOrUsername); - await agent.type(field, "a"); + await agent.type(screen.getByRole("textbox"), "a"); // After expect(login).toBeEnabled(); @@ -60,22 +59,17 @@ describe("ResetRequest", () => { await renderUserSettings(); // Before - expect( - screen.queryByTestId(PasswordRequestIds.FieldEmailOrUsername) - ).toBeTruthy(); - expect(screen.queryByTestId(PasswordRequestIds.ButtonLogin)).toBeTruthy(); - expect(screen.queryByTestId(PasswordRequestIds.ButtonSubmit)).toBeTruthy(); + expect(screen.queryByText(ResetRequestTextId.ButtonLogin)).toBeTruthy(); + const submit = screen.getByText(ResetRequestTextId.ButtonSubmit); + expect(submit).toBeTruthy(); // Agent - const field = screen.getByTestId(PasswordRequestIds.FieldEmailOrUsername); - await agent.type(field, "a"); - await agent.click(screen.getByTestId(PasswordRequestIds.ButtonSubmit)); + await agent.type(screen.getByRole("textbox"), "a"); + await agent.click(submit); // After - expect( - screen.queryByTestId(PasswordRequestIds.FieldEmailOrUsername) - ).toBeNull(); - expect(screen.queryByTestId(PasswordRequestIds.ButtonLogin)).toBeTruthy(); - expect(screen.queryByTestId(PasswordRequestIds.ButtonSubmit)).toBeNull(); + expect(screen.queryByRole("textbox")).toBeNull(); + expect(screen.queryByText(ResetRequestTextId.ButtonLogin)).toBeTruthy(); + expect(screen.queryByText(ResetRequestTextId.ButtonSubmit)).toBeNull(); }); }); diff --git a/src/components/PasswordReset/tests/ResetPage.test.tsx b/src/components/PasswordReset/tests/ResetPage.test.tsx index bc1b912091..f4c56778be 100644 --- a/src/components/PasswordReset/tests/ResetPage.test.tsx +++ b/src/components/PasswordReset/tests/ResetPage.test.tsx @@ -12,7 +12,7 @@ import { MemoryRouter, Route, Routes } from "react-router"; import configureMockStore from "redux-mock-store"; import PasswordReset, { - PasswordResetIds, + PasswordResetTextId, } from "components/PasswordReset/ResetPage"; import { Path } from "types/path"; @@ -67,14 +67,17 @@ describe("PasswordReset", () => { await customRender(); const shortPassword = "foo"; - const passwdField = screen.getByTestId(PasswordResetIds.Password); - const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword); + const passwdField = screen.getByLabelText( + PasswordResetTextId.FieldPassword1 + ); + const passwdConfirm = screen.getByLabelText( + PasswordResetTextId.FieldPassword2 + ); await user.type(passwdField, shortPassword); await user.type(passwdConfirm, shortPassword); - const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton); - expect(submitButton.closest("button")).toBeDisabled(); + expect(screen.getByRole("button")).toBeDisabled(); }); it("disables button when passwords don't match", async () => { @@ -83,14 +86,23 @@ describe("PasswordReset", () => { const passwordEntry = "password"; const confirmEntry = "passward"; - const passwdField = screen.getByTestId(PasswordResetIds.Password); - const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword); + const passwdField = screen.getByLabelText( + PasswordResetTextId.FieldPassword1 + ); + const passwdConfirm = screen.getByLabelText( + PasswordResetTextId.FieldPassword2 + ); + expect( + screen.queryByText(PasswordResetTextId.FieldPassword2Error) + ).toBeNull(); await user.type(passwdField, passwordEntry); await user.type(passwdConfirm, confirmEntry); - const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton); - expect(submitButton.closest("button")).toBeDisabled(); + expect(screen.getByRole("button")).toBeDisabled(); + expect( + screen.queryByText(PasswordResetTextId.FieldPassword2Error) + ).toBeTruthy(); }); it("enables button when passwords are long enough and match", async () => { @@ -98,23 +110,24 @@ describe("PasswordReset", () => { await customRender(); const passwordEntry = "password"; - const passwdField = screen.getByTestId(PasswordResetIds.Password); - const passwdConfirm = screen.getByTestId(PasswordResetIds.ConfirmPassword); + const passwdField = screen.getByLabelText( + PasswordResetTextId.FieldPassword1 + ); + const passwdConfirm = screen.getByLabelText( + PasswordResetTextId.FieldPassword2 + ); await user.type(passwdField, passwordEntry); await user.type(passwdConfirm, passwordEntry); - const submitButton = screen.getByTestId(PasswordResetIds.SubmitButton); - expect(submitButton.closest("button")).toBeEnabled(); + expect(screen.getByRole("button")).toBeEnabled(); }); it("renders the InvalidLink component if token not valid", async () => { mockValidateResetToken.mockResolvedValueOnce(false); await customRender(); - for (const id of Object.values(PasswordResetIds)) { - expect(screen.queryAllByTestId(id)).toHaveLength(0); - } - // The textId will show up as text because t() is mocked to return its input. - expect(screen.queryAllByText("passwordReset.invalidURL")).toBeTruthy(); + + expect(screen.queryByRole("textbox")).toBeNull(); + expect(screen.getByText(PasswordResetTextId.Invalid)).toBeTruthy(); }); }); diff --git a/src/components/ProjectSettings/ProjectDomains.tsx b/src/components/ProjectSettings/ProjectDomains.tsx index 5113204f46..04c82bdcfa 100644 --- a/src/components/ProjectSettings/ProjectDomains.tsx +++ b/src/components/ProjectSettings/ProjectDomains.tsx @@ -376,9 +376,6 @@ export function AddDomainDialog(props: AddDomainDialogProps): ReactElement { setName(e.target.value)} value={name} diff --git a/src/components/ProjectSettings/ProjectLanguages.tsx b/src/components/ProjectSettings/ProjectLanguages.tsx index e876695953..2d267f180f 100644 --- a/src/components/ProjectSettings/ProjectLanguages.tsx +++ b/src/components/ProjectSettings/ProjectLanguages.tsx @@ -261,9 +261,6 @@ export default function ProjectLanguages( { setChangeVernName(false); setNewVernName(props.project.vernacularWritingSystem.name); diff --git a/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx b/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx index 9be03590c0..2d3412e265 100644 --- a/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx +++ b/src/components/ProjectSettings/tests/ProjectLanguages.test.tsx @@ -55,9 +55,7 @@ describe("ProjectLanguages", () => { await userEvent.click( screen.getByTestId(ProjectLanguagesId.ButtonEditVernacularName) ); - const vernField = screen.getByTestId( - ProjectLanguagesId.FieldEditVernacularName - ); + const vernField = screen.getByRole("textbox"); await userEvent.clear(vernField); await userEvent.type(vernField, newName); await userEvent.click( diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace.tsx index 94a4a5e853..b0234f74ba 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace.tsx @@ -72,10 +72,7 @@ export default function FindAndReplace( variant="outlined" style={{ width: "100%" }} margin="normal" - inputProps={{ - "data-testid": FindAndReplaceId.FieldFind, - maxLength: 100, - }} + slotProps={{ htmlInput: { maxLength: 100 } }} vernacular /> {/* Cancel yes/no dialog */} @@ -106,7 +100,7 @@ export default function CharacterEntry(): ReactElement { } onClick={() => setAdvancedOpen((prev) => !prev)} > - {t("charInventory.characterSet.advanced")} + {t(CharacterEntryTextId.ToggleAdvanced)} @@ -114,16 +108,14 @@ export default function CharacterEntry(): ReactElement { {/* Input for accepted characters */} dispatch(setValidCharacters(chars))} /> {/* Input for rejected characters */} dispatch(setRejectedCharacters(chars))} /> @@ -144,24 +136,28 @@ function CharactersInput(props: CharactersInputProps): ReactElement { autoComplete="off" fullWidth id={props.id} - 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("")) } + slotProps={{ + htmlInput: { spellCheck: false, style: { letterSpacing: 5 } }, + }} style={{ marginTop: theme.spacing(2) }} value={props.characters.join("")} - variant="outlined" vernacular /> ); } +export enum CancelDialogTextId { + ButtonNo = "charInventory.dialog.no", + ButtonYes = "charInventory.dialog.yes", + Content = "charInventory.dialog.content", + Title = "charInventory.dialog.title", +} + interface CancelDialogProps { open: boolean; onClose: () => void; @@ -172,38 +168,25 @@ function CancelDialog(props: CancelDialogProps): ReactElement { const { t } = useTranslation(); return ( - props.onClose()} - open={props.open} - > - {t("charInventory.dialog.title")} + props.onClose()} open={props.open}> + {t(CancelDialogTextId.Title)} - - {t("charInventory.dialog.content")} - + {t(CancelDialogTextId.Content)} - diff --git a/src/goals/CharacterInventory/CharInv/tests/index.test.tsx b/src/goals/CharacterInventory/CharInv/tests/index.test.tsx index 91d8465ac4..0e52e937a6 100644 --- a/src/goals/CharacterInventory/CharInv/tests/index.test.tsx +++ b/src/goals/CharacterInventory/CharInv/tests/index.test.tsx @@ -9,7 +9,10 @@ import { Provider } from "react-redux"; import configureMockStore from "redux-mock-store"; import CharInv from "goals/CharacterInventory/CharInv"; -import { CharInvCancelSaveIds } from "goals/CharacterInventory/CharInv/CharacterEntry"; +import { + CancelDialogTextId, + CharacterEntryTextId, +} from "goals/CharacterInventory/CharInv/CharacterEntry"; import { defaultState } from "rootRedux/types"; jest.mock("goals/CharacterInventory/Redux/CharacterInventoryActions", () => ({ @@ -50,34 +53,26 @@ describe("CharInv", () => { it("saves inventory on save", async () => { expect(mockUploadAndExit).toHaveBeenCalledTimes(0); - await userEvent.click(screen.getByTestId(CharInvCancelSaveIds.ButtonSave)); + await userEvent.click(screen.getByText(CharacterEntryTextId.ButtonSave)); expect(mockUploadAndExit).toHaveBeenCalledTimes(1); }); it("opens a dialogue on cancel, closes on no", async () => { expect(screen.queryByRole("dialog")).toBeNull(); - await userEvent.click( - screen.getByTestId(CharInvCancelSaveIds.ButtonCancel) - ); + await userEvent.click(screen.getByText(CharacterEntryTextId.ButtonCancel)); expect(screen.queryByRole("dialog")).toBeTruthy(); - await userEvent.click( - screen.getByTestId(CharInvCancelSaveIds.DialogCancelButtonNo) - ); + await userEvent.click(screen.getByText(CancelDialogTextId.ButtonNo)); // 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 () => { - await userEvent.click( - screen.getByTestId(CharInvCancelSaveIds.ButtonCancel) - ); + await userEvent.click(screen.getByText(CharacterEntryTextId.ButtonCancel)); expect(mockExit).toHaveBeenCalledTimes(0); - await userEvent.click( - screen.getByTestId(CharInvCancelSaveIds.DialogCancelButtonYes) - ); + await userEvent.click(screen.getByText(CancelDialogTextId.ButtonYes)); expect(mockExit).toHaveBeenCalledTimes(1); }); }); diff --git a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx index 0752029c54..6e8eccdd29 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog.tsx @@ -373,14 +373,11 @@ export default function EditDialog(props: EditDialogProps): ReactElement { {/* Vernacular */} - + setNewWord((prev) => ({ @@ -395,7 +392,7 @@ export default function EditDialog(props: EditDialogProps): ReactElement { {/* Senses */} - + 1 && ( @@ -425,7 +422,7 @@ export default function EditDialog(props: EditDialogProps): ReactElement { {/* Pronunciations */} - + {/* Note */} - + updateNoteText(e.target.value)} @@ -468,7 +464,7 @@ export default function EditDialog(props: EditDialogProps): ReactElement { {/* Flag */} - + @@ -480,7 +476,6 @@ export default function EditDialog(props: EditDialogProps): ReactElement { 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 4b8fcaacfc..a37c514fb1 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditSenseDialog.tsx @@ -222,7 +222,7 @@ export default function EditSenseDialog( {/* Definitions */} {definitionsEnabled && ( - + + + {newSense.grammaticalInfo.catGroup === @@ -273,7 +273,7 @@ export default function EditSenseDialog( )} {/* Semantic Domains */} - + @@ -342,7 +342,6 @@ 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" @@ -353,7 +352,6 @@ function DefinitionTextField(props: DefinitionTextFieldProps): ReactElement { ) } value={props.definition.text} - variant="outlined" /> ); } @@ -405,7 +403,6 @@ 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" @@ -414,7 +411,6 @@ function GlossTextField(props: GlossTextFieldProps): ReactElement { props.onChange(newGloss(event.target.value, props.gloss.language)) } value={props.gloss.def} - variant="outlined" /> ); } 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 a46a55038a..5114d47582 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditDialog.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditDialog.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from "@testing-library/react"; +import { act, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; import configureMockStore from "redux-mock-store"; @@ -7,6 +7,7 @@ import { type Word } from "api/models"; import { type CurrentProjectState } from "components/Project/ProjectReduxTypes"; import EditDialog, { EditDialogId, + EditDialogTextId, } from "goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/EditDialog"; import { newProject } from "types/project"; import { newSense, newWord } from "types/word"; @@ -61,6 +62,13 @@ const renderEditDialog = async (): Promise => ); }); +const getRegionField = (titleId: EditDialogTextId): HTMLElement => { + const region = screen + .getByText(titleId) + .closest('[role="region"]') as HTMLElement; + return within(region).getByRole("textbox"); +}; + beforeEach(async () => { jest.clearAllMocks(); mockUpdateWord.mockImplementation((w: Word) => @@ -83,7 +91,7 @@ describe("EditDialog", () => { test("cancel button opens dialog if changes", async () => { // Make a change - const noteField = screen.getByTestId(EditDialogId.TextFieldNote); + const noteField = getRegionField(EditDialogTextId.CardNote); await userEvent.type(noteField, "New note!"); // Click the cancel button and cancel the cancel @@ -121,7 +129,7 @@ describe("EditDialog", () => { test("save button saves changes and closes", async () => { // Make a change - const flagField = screen.getByTestId(EditDialogId.TextFieldFlag); + const flagField = getRegionField(EditDialogTextId.CardFlag); const newFlagText = "New flag!"; await userEvent.type(flagField, newFlagText); 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 06e42db000..e7db0758c7 100644 --- a/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSenseDialog.test.tsx +++ b/src/goals/ReviewEntries/ReviewEntriesTable/Cells/EditCell/tests/EditSenseDialog.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from "@testing-library/react"; +import { act, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; import configureMockStore from "redux-mock-store"; @@ -50,6 +50,13 @@ const renderEditSenseDialog = async ( }); }; +const getGlossFields = (): HTMLElement[] => { + const region = screen + .getByText(EditSenseDialogTextId.CardGlosses) + .closest('[role="region"]') as HTMLElement; + return within(region).getAllByRole("textbox"); +}; + beforeEach(async () => { jest.clearAllMocks(); }); @@ -71,8 +78,7 @@ describe("EditSenseDialog", () => { test("cancel button opens dialog if changes", async () => { // Make a change - const testId = `${EditSenseDialogId.TextFieldGlossPrefix}0`; - await userEvent.type(screen.getByTestId(testId), "glossier"); + await userEvent.type(getGlossFields()[0], "glossier"); // Click the cancel button and cancel the cancel await userEvent.click(screen.getByTestId(EditSenseDialogId.ButtonCancel)); @@ -106,8 +112,7 @@ describe("EditSenseDialog", () => { test("save button saves changes and closes", async () => { // Make a change - const testId = `${EditSenseDialogId.TextFieldGlossPrefix}0`; - const glossField = screen.getByTestId(testId); + const glossField = getGlossFields()[0]; await userEvent.clear(glossField); const newGlossText = "New gloss!"; await userEvent.type(glossField, newGlossText); From 1e4b9736a38151461975e2df4e6c71f4dc211fe3 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 11 Sep 2025 16:27:09 -0400 Subject: [PATCH 3/3] Reduce testid dependence; Remove default-specified props --- src/components/Buttons/FileInputButton.tsx | 2 +- src/components/Buttons/LoadingDoneButton.tsx | 2 +- src/components/Dialogs/UploadImage.tsx | 6 +-- src/components/Login/Signup.tsx | 32 ++++----------- src/components/Login/tests/Signup.test.tsx | 3 +- src/components/PasswordReset/Request.tsx | 1 - .../PasswordReset/tests/ResetPage.test.tsx | 38 ++++++----------- src/components/ProjectExport/ExportButton.tsx | 2 +- .../ProjectScreen/CreateProject.tsx | 33 ++------------- .../tests/CreateProject.test.tsx | 34 +++++++-------- .../ProjectSettings/ProjectImport.tsx | 41 ++++++++----------- .../tests/ProjectImport.test.tsx | 6 +-- src/components/ProjectUsers/EmailInvite.tsx | 8 +--- 13 files changed, 68 insertions(+), 140 deletions(-) diff --git a/src/components/Buttons/FileInputButton.tsx b/src/components/Buttons/FileInputButton.tsx index cc78fe59dc..217223a511 100644 --- a/src/components/Buttons/FileInputButton.tsx +++ b/src/components/Buttons/FileInputButton.tsx @@ -5,7 +5,7 @@ import { ReactElement, ReactNode } from "react"; interface BrowseProps { updateFile: (file: File) => void; accept?: string; - buttonProps?: ButtonProps & { "data-testid"?: string }; + buttonProps?: ButtonProps; children?: ReactNode; } diff --git a/src/components/Buttons/LoadingDoneButton.tsx b/src/components/Buttons/LoadingDoneButton.tsx index b35e20fda5..bc9b6872ab 100644 --- a/src/components/Buttons/LoadingDoneButton.tsx +++ b/src/components/Buttons/LoadingDoneButton.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; import { themeColors } from "types/theme"; interface LoadingDoneButtonProps { - buttonProps?: ButtonProps & { "data-testid"?: string }; + buttonProps?: ButtonProps; children?: ReactNode; disabled?: boolean; done?: boolean; diff --git a/src/components/Dialogs/UploadImage.tsx b/src/components/Dialogs/UploadImage.tsx index 713e674607..509b036e3a 100644 --- a/src/components/Dialogs/UploadImage.tsx +++ b/src/components/Dialogs/UploadImage.tsx @@ -63,11 +63,7 @@ export default function ImageUpload(props: ImageUploadProps): ReactElement { {t("buttons.browse")} - + {t("buttons.save")} diff --git a/src/components/Login/Signup.tsx b/src/components/Login/Signup.tsx index ef65c0219c..8c17e01675 100644 --- a/src/components/Login/Signup.tsx +++ b/src/components/Login/Signup.tsx @@ -70,23 +70,12 @@ export const signupFieldTextId: SignupText = { [SignupField.Username]: "login.username", }; -export enum SignupId { - ButtonLogIn = "signup-log-in-button", - ButtonSignUp = "signup-sign-up-button", - FieldEmail = "signup-email-field", - FieldName = "signup-name-field", - FieldPassword1 = "signup-password1-field", - FieldPassword2 = "signup-password2-field", - FieldUsername = "signup-username-field", - Form = "signup-form", -} - -export const signupFieldId: Record = { - [SignupField.Email]: SignupId.FieldEmail, - [SignupField.Name]: SignupId.FieldName, - [SignupField.Password1]: SignupId.FieldPassword1, - [SignupField.Password2]: SignupId.FieldPassword2, - [SignupField.Username]: SignupId.FieldUsername, +export const signupFieldId: SignupText = { + [SignupField.Email]: "signup-email-field", + [SignupField.Name]: "signup-name-field", + [SignupField.Password1]: "signup-password1-field", + [SignupField.Password2]: "signup-password2-field", + [SignupField.Username]: "signup-username-field", }; interface SignupProps { @@ -200,7 +189,7 @@ export default function Signup(props: SignupProps): ReactElement { /> - + {/* Name field */} ): Promise => { /** Clicks the submit button and checks that only the specified field errors. */ const submitAndCheckError = async (id?: SignupField): Promise => { // Submit the form. - await userEvent.click(screen.getByTestId(SignupId.ButtonSignUp)); + await userEvent.click(screen.getByText("login.signUp")); // Only the specified field should error. Object.values(SignupField).forEach((val) => { diff --git a/src/components/PasswordReset/Request.tsx b/src/components/PasswordReset/Request.tsx index 778b6995f5..bb4dec275f 100644 --- a/src/components/PasswordReset/Request.tsx +++ b/src/components/PasswordReset/Request.tsx @@ -99,7 +99,6 @@ export default function ResetRequest(): ReactElement { diff --git a/src/components/PasswordReset/tests/ResetPage.test.tsx b/src/components/PasswordReset/tests/ResetPage.test.tsx index f4c56778be..038d5391bd 100644 --- a/src/components/PasswordReset/tests/ResetPage.test.tsx +++ b/src/components/PasswordReset/tests/ResetPage.test.tsx @@ -67,15 +67,11 @@ describe("PasswordReset", () => { await customRender(); const shortPassword = "foo"; - const passwdField = screen.getByLabelText( - PasswordResetTextId.FieldPassword1 - ); - const passwdConfirm = screen.getByLabelText( - PasswordResetTextId.FieldPassword2 - ); + const pw1Field = screen.getByLabelText(PasswordResetTextId.FieldPassword1); + const pw2Field = screen.getByLabelText(PasswordResetTextId.FieldPassword2); - await user.type(passwdField, shortPassword); - await user.type(passwdConfirm, shortPassword); + await user.type(pw1Field, shortPassword); + await user.type(pw2Field, shortPassword); expect(screen.getByRole("button")).toBeDisabled(); }); @@ -86,18 +82,14 @@ describe("PasswordReset", () => { const passwordEntry = "password"; const confirmEntry = "passward"; - const passwdField = screen.getByLabelText( - PasswordResetTextId.FieldPassword1 - ); - const passwdConfirm = screen.getByLabelText( - PasswordResetTextId.FieldPassword2 - ); + const pw1Field = screen.getByLabelText(PasswordResetTextId.FieldPassword1); + const pw2Field = screen.getByLabelText(PasswordResetTextId.FieldPassword2); expect( screen.queryByText(PasswordResetTextId.FieldPassword2Error) ).toBeNull(); - await user.type(passwdField, passwordEntry); - await user.type(passwdConfirm, confirmEntry); + await user.type(pw1Field, passwordEntry); + await user.type(pw2Field, confirmEntry); expect(screen.getByRole("button")).toBeDisabled(); expect( @@ -110,15 +102,11 @@ describe("PasswordReset", () => { await customRender(); const passwordEntry = "password"; - const passwdField = screen.getByLabelText( - PasswordResetTextId.FieldPassword1 - ); - const passwdConfirm = screen.getByLabelText( - PasswordResetTextId.FieldPassword2 - ); - - await user.type(passwdField, passwordEntry); - await user.type(passwdConfirm, passwordEntry); + const pw1Field = screen.getByLabelText(PasswordResetTextId.FieldPassword1); + const pw2Field = screen.getByLabelText(PasswordResetTextId.FieldPassword2); + + await user.type(pw1Field, passwordEntry); + await user.type(pw2Field, passwordEntry); expect(screen.getByRole("button")).toBeEnabled(); }); diff --git a/src/components/ProjectExport/ExportButton.tsx b/src/components/ProjectExport/ExportButton.tsx index 0fb0b9e046..f1acb09356 100644 --- a/src/components/ProjectExport/ExportButton.tsx +++ b/src/components/ProjectExport/ExportButton.tsx @@ -17,7 +17,7 @@ import { type StoreState } from "rootRedux/types"; interface ExportButtonProps { projectId: string; - buttonProps?: ButtonProps & { "data-testid"?: string }; + buttonProps?: ButtonProps; } /** A button for exporting project to LIFT file. */ diff --git a/src/components/ProjectScreen/CreateProject.tsx b/src/components/ProjectScreen/CreateProject.tsx index d16719b30d..97891b284e 100644 --- a/src/components/ProjectScreen/CreateProject.tsx +++ b/src/components/ProjectScreen/CreateProject.tsx @@ -33,14 +33,6 @@ import theme from "types/theme"; import { newWritingSystem } from "types/writingSystem"; import { NormalizedTextField } from "utilities/fontComponents"; -export enum CreateProjectId { - ButtonSelectFile = "create-project-select-file", - ButtonSubmit = "create-project-submit", - FieldName = "create-project-name", - Form = "create-project-form", - SelectVern = "create-proj-select-vern", -} - export enum CreateProjectTextId { Create = "createProject.create", CreateSuccess = "createProject.success", @@ -181,12 +173,7 @@ export default function CreateProject(): ReactElement { }; return ( - {menuItems} ); @@ -224,11 +211,7 @@ export default function CreateProject(): ReactElement { return ( - createProject(e)} - > + createProject(e)}> {/* Title */} @@ -237,7 +220,6 @@ export default function CreateProject(): ReactElement { {/* Project name */} updateLanguageData(file)} accept=".zip" - buttonProps={{ - "data-testid": CreateProjectId.ButtonSelectFile, - id: CreateProjectId.ButtonSelectFile, - sx: { m: 1 }, - }} + buttonProps={{ sx: { m: 1 } }} > {t(CreateProjectTextId.UploadBrowse)} @@ -342,11 +320,6 @@ export default function CreateProject(): ReactElement { style={{ marginTop: theme.spacing(1) }} > ({ jest.mock("components/Buttons/FileInputButton", () => ({ __esModule: true, default: (props: { - /** Includes ids and styling. */ - buttonProps?: ButtonProps & { "data-testid"?: string }; + children?: ReactNode; /** Clicking will call this with a mock file with nonempty file name. */ updateFile: (file: File) => void; - }) => { - const { buttonProps, updateFile } = props; - return updateFile(mockFile)} />; - }, + }) => ( + props.updateFile(mockFile)}> + {props.children} + + ), })); jest.mock("i18n", () => ({ language: "" })); // else `thrown: "Error: AggregateError` @@ -68,12 +68,16 @@ describe("CreateProject", () => { // Error appears when duplicate name submitted. expect(screen.queryByText(CreateProjectTextId.NameTaken)).toBeNull(); mockProjectDuplicateCheck.mockResolvedValueOnce(true); - await userEvent.click(screen.getByTestId(CreateProjectId.ButtonSubmit)); + await userEvent.click( + screen.getByRole("button", { name: CreateProjectTextId.Create }) + ); expect(screen.queryByText(CreateProjectTextId.NameTaken)).toBeTruthy(); }); it("disables submit button when empty name or empty vern lang bcp code", async () => { - const button = screen.getByTestId(CreateProjectId.ButtonSubmit); + const button = screen.getByRole("button", { + name: CreateProjectTextId.Create, + }); const [nameInput, vernInput] = screen.getAllByRole("textbox"); // Start with empty name and vern language: button disabled. @@ -99,26 +103,24 @@ describe("CreateProject", () => { // File with no writing systems only disables analysis lang picker. mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce([]); - await userEvent.click(screen.getByTestId(CreateProjectId.ButtonSelectFile)); + await userEvent.click(screen.getByText(CreateProjectTextId.UploadBrowse)); expect(screen.queryAllByTestId(mockLangPickerId)).toHaveLength(1); // File with writing systems disables both lang pickers. mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce(mockLangs); - await userEvent.click(screen.getByTestId(CreateProjectId.ButtonSelectFile)); + await userEvent.click(screen.getByText(CreateProjectTextId.UploadBrowse)); expect(screen.queryAllByTestId(mockLangPickerId)).toHaveLength(0); }); it("offers vern langs when file has some", async () => { - // No vern selector by default. - expect(screen.queryByTestId(CreateProjectId.SelectVern)).toBeNull(); + // No vern combobox selector by default. expect(screen.queryByRole("combobox")).toBeNull(); // Mock-select a file with multiple languages. mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce(mockLangs); - await userEvent.click(screen.getByTestId(CreateProjectId.ButtonSelectFile)); + await userEvent.click(screen.getByText(CreateProjectTextId.UploadBrowse)); // Open the select, whose button is a combobox. - expect(screen.queryByTestId(CreateProjectId.SelectVern)).toBeTruthy(); await userEvent.click(screen.getByRole("combobox")); // Number of options is +2 for the menu title item and an "other" item. diff --git a/src/components/ProjectSettings/ProjectImport.tsx b/src/components/ProjectSettings/ProjectImport.tsx index 2c22263649..77d8a9a20d 100644 --- a/src/components/ProjectSettings/ProjectImport.tsx +++ b/src/components/ProjectSettings/ProjectImport.tsx @@ -20,11 +20,14 @@ enum UploadState { Done, } -export enum ProjectImportIds { - ButtonDialogCancel = "project-import-dialog-cancel-button", - ButtonDialogConfirm = "project-import-dialog-confirm-button", - ButtonFileSelect = "project-import-file-select-button", - ButtonFileSubmit = "project-import-file-submit-button", +export enum ProjectImportTextId { + ButtonChoose = "projectSettings.import.chooseFile", + ButtonUpload = "buttons.upload", + DialogLanguageMismatch = "projectSettings.import.liftLanguageMismatch", + FileSelected = "createProject.fileSelected", + Instructions = "projectSettings.import.body", + ToastFail = "projectSettings.import.noWordsUploaded", + ToastSuccess = "projectSettings.import.wordsUploaded", } export default function ProjectImport( @@ -63,9 +66,9 @@ export default function ProjectImport( // Toast the number of words uploaded. if (val) { - toast.success(t("projectSettings.import.wordsUploaded", { val })); + toast.success(t(ProjectImportTextId.ToastSuccess, { val })); } else { - toast.warning(t("projectSettings.import.noWordsUploaded")); + toast.warning(t(ProjectImportTextId.ToastFail)); } // Clean up. @@ -80,7 +83,7 @@ export default function ProjectImport( {/* Upload/LIFT instructions */} - {t("projectSettings.import.body")}{" "} + {t(ProjectImportTextId.Instructions)}{" "} FillerTextA @@ -95,46 +98,36 @@ export default function ProjectImport( - {t("projectSettings.import.chooseFile")} + {t(ProjectImportTextId.ButtonChoose)} {/* Upload button */} - {t("buttons.upload")} + {t(ProjectImportTextId.ButtonUpload)} {/* Name of the selected file */} {liftFile && ( - {t("createProject.fileSelected", { val: liftFile.name })} + {t(ProjectImportTextId.FileSelected, { val: liftFile.name })} )} {/* Dialog if LIFT contents don't match vernacular language */} {liftLangs && ( setLiftFile(undefined)} handleConfirm={() => setDialogOpen(false)} open={dialogOpen} - text={t("projectSettings.import.liftLanguageMismatch", { + text={t(ProjectImportTextId.DialogLanguageMismatch, { val1: liftLangs.map((ws) => ws.bcp47), val2: props.project.vernacularWritingSystem.bcp47, })} diff --git a/src/components/ProjectSettings/tests/ProjectImport.test.tsx b/src/components/ProjectSettings/tests/ProjectImport.test.tsx index b8f2ca9519..eb3f88058a 100644 --- a/src/components/ProjectSettings/tests/ProjectImport.test.tsx +++ b/src/components/ProjectSettings/tests/ProjectImport.test.tsx @@ -1,7 +1,7 @@ import { act, render, screen } from "@testing-library/react"; import ProjectImport, { - ProjectImportIds, + ProjectImportTextId, } from "components/ProjectSettings/ProjectImport"; import { randomProject } from "types/project"; @@ -14,9 +14,9 @@ const renderImport = async (): Promise => { describe("ProjectImport", () => { it("renders with file select button and disabled upload button", async () => { await renderImport(); - const fileButton = screen.getByTestId(ProjectImportIds.ButtonFileSelect); + const fileButton = screen.getByText(ProjectImportTextId.ButtonChoose); expect(fileButton.classList.toString()).not.toContain("Mui-disabled"); - const uploadButton = screen.getByTestId(ProjectImportIds.ButtonFileSubmit); + const uploadButton = screen.getByText(ProjectImportTextId.ButtonUpload); expect(uploadButton.classList.toString()).toContain("Mui-disabled"); }); }); diff --git a/src/components/ProjectUsers/EmailInvite.tsx b/src/components/ProjectUsers/EmailInvite.tsx index c84b91adf6..f37a43e2bb 100644 --- a/src/components/ProjectUsers/EmailInvite.tsx +++ b/src/components/ProjectUsers/EmailInvite.tsx @@ -85,14 +85,10 @@ export default function EmailInvite(props: InviteProps): ReactElement { {/* Submit button */} onSubmit(), - variant: "contained", - }} + loading={isLoading} > {t(EmailInviteTextId.ButtonSubmit)}