mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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 “<strong>{name}</strong>” 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 “<strong>{name}</strong>” 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),
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user