fix: update ErrorDialog logic and tests (#10111)

* fix: make error text less naggy

* fix: make input colors sync with confirmation text state

* fix: more color sync fixes

* fix: remove flaky warning messages in test

* fix: remove needless braces

* refactor: clean up code

* refactor: clean up code more
This commit is contained in:
Michael Smith
2023-10-06 19:40:37 -04:00
committed by GitHub
parent ae113179b3
commit 38bb854c8b
2 changed files with 93 additions and 61 deletions
@@ -2,6 +2,23 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { render } from "testHelpers/renderHelpers";
import { DeleteDialog } from "./DeleteDialog";
import { act } from "react-dom/test-utils";
const inputTestId = "delete-dialog-name-confirmation";
async function fillInputField(inputElement: HTMLElement, text: string) {
// 2023-10-06 - There's something wonky with MUI's ConfirmDialog that causes
// its state to update after a typing event gets fired, and React Testing
// Library isn't able to catch it, making React DOM freak out because an
// "unexpected" state change happened. It won't fail the test, but it makes
// the console look really scary because it'll spit out a big warning message.
// Tried everything under the sun to catch the state changes the proper way,
// but the only way to get around it for now might be to manually make React
// DOM aware of the changes
// eslint-disable-next-line testing-library/no-unnecessary-act -- have to make sure state updates don't slip through cracks
return act(() => userEvent.type(inputElement, text));
}
describe("DeleteDialog", () => {
it("disables confirm button when the text field is empty", () => {
@@ -14,6 +31,7 @@ describe("DeleteDialog", () => {
name="MyTemplate"
/>,
);
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
});
@@ -28,8 +46,10 @@ describe("DeleteDialog", () => {
name="MyTemplate"
/>,
);
const textField = screen.getByTestId("delete-dialog-name-confirmation");
await userEvent.type(textField, "MyTemplateWrong");
const textField = screen.getByTestId(inputTestId);
await fillInputField(textField, "MyTemplateButWrong");
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
});
@@ -44,8 +64,10 @@ describe("DeleteDialog", () => {
name="MyTemplate"
/>,
);
const textField = screen.getByTestId("delete-dialog-name-confirmation");
await userEvent.type(textField, "MyTemplate");
const textField = screen.getByTestId(inputTestId);
await fillInputField(textField, "MyTemplate");
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).not.toBeDisabled();
});
@@ -1,6 +1,13 @@
import makeStyles from "@mui/styles/makeStyles";
import {
type FC,
type FormEvent,
type PropsWithChildren,
useId,
useState,
} from "react";
import { useTheme } from "@emotion/react";
import TextField from "@mui/material/TextField";
import { ChangeEvent, useState, PropsWithChildren, FC } from "react";
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog";
export interface DeleteDialogProps {
@@ -22,51 +29,23 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
name,
confirmLoading,
}) => {
const styles = useStyles();
const [nameValue, setNameValue] = useState("");
const confirmed = name === nameValue;
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setNameValue(event.target.value);
const hookId = useId();
const theme = useTheme();
const [userConfirmationText, setUserConfirmationText] = useState("");
const [isFocused, setIsFocused] = useState(false);
const deletionConfirmed = name === userConfirmationText;
const onSubmit = (event: FormEvent) => {
event.preventDefault();
if (deletionConfirmed) {
onConfirm();
}
};
const hasError = nameValue.length > 0 && !confirmed;
const content = (
<>
<p>Deleting this {entity} is irreversible!</p>
{Boolean(info) && <p className={styles.warning}>{info}</p>}
<p>Are you sure you want to proceed?</p>
<p>
Type &ldquo;<strong>{name}</strong>&rdquo; below to confirm.
</p>
<form
onSubmit={(e) => {
e.preventDefault();
if (confirmed) {
onConfirm();
}
}}
>
<TextField
fullWidth
autoFocus
className={styles.textField}
name="confirmation"
autoComplete="off"
id="confirmation"
placeholder={name}
value={nameValue}
onChange={handleChange}
label={`Name of the ${entity} to delete`}
error={hasError}
helperText={
hasError && `${nameValue} does not match the name of this ${entity}`
}
inputProps={{ ["data-testid"]: "delete-dialog-name-confirmation" }}
/>
</form>
</>
);
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
const displayErrorMessage = hasError && !isFocused;
const inputColor = hasError ? "error" : "primary";
return (
<ConfirmDialog
@@ -76,19 +55,50 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
title={`Delete ${entity}`}
onConfirm={onConfirm}
onClose={onCancel}
description={content}
confirmLoading={confirmLoading}
disabled={!confirmed}
disabled={!deletionConfirmed}
description={
<>
<p>Deleting this {entity} is irreversible!</p>
{Boolean(info) && (
<p css={{ color: theme.palette.warning.light }}>{info}</p>
)}
<p>Are you sure you want to proceed?</p>
<p>
Type &ldquo;<strong>{name}</strong>&rdquo; below to confirm.
</p>
<form onSubmit={onSubmit}>
<TextField
fullWidth
autoFocus
sx={{ marginTop: theme.spacing(3) }}
name="confirmation"
autoComplete="off"
id={`${hookId}-confirm`}
placeholder={name}
value={userConfirmationText}
onChange={(event) => setUserConfirmationText(event.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
label={`Name of the ${entity} to delete`}
color={inputColor}
error={displayErrorMessage}
helperText={
displayErrorMessage &&
`${userConfirmationText} does not match the name of this ${entity}`
}
InputProps={{ color: inputColor }}
inputProps={{
"data-testid": "delete-dialog-name-confirmation",
}}
/>
</form>
</>
}
/>
);
};
const useStyles = makeStyles((theme) => ({
warning: {
color: theme.palette.warning.light,
},
textField: {
marginTop: theme.spacing(3),
},
}));