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 userEvent from "@testing-library/user-event";
|
||||||
import { render } from "testHelpers/renderHelpers";
|
import { render } from "testHelpers/renderHelpers";
|
||||||
import { DeleteDialog } from "./DeleteDialog";
|
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", () => {
|
describe("DeleteDialog", () => {
|
||||||
it("disables confirm button when the text field is empty", () => {
|
it("disables confirm button when the text field is empty", () => {
|
||||||
@@ -14,6 +31,7 @@ describe("DeleteDialog", () => {
|
|||||||
name="MyTemplate"
|
name="MyTemplate"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||||
expect(confirmButton).toBeDisabled();
|
expect(confirmButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
@@ -28,8 +46,10 @@ describe("DeleteDialog", () => {
|
|||||||
name="MyTemplate"
|
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" });
|
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||||
expect(confirmButton).toBeDisabled();
|
expect(confirmButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
@@ -44,8 +64,10 @@ describe("DeleteDialog", () => {
|
|||||||
name="MyTemplate"
|
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" });
|
const confirmButton = screen.getByRole("button", { name: "Delete" });
|
||||||
expect(confirmButton).not.toBeDisabled();
|
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 TextField from "@mui/material/TextField";
|
||||||
import { ChangeEvent, useState, PropsWithChildren, FC } from "react";
|
|
||||||
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog";
|
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog";
|
||||||
|
|
||||||
export interface DeleteDialogProps {
|
export interface DeleteDialogProps {
|
||||||
@@ -22,51 +29,23 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
|
|||||||
name,
|
name,
|
||||||
confirmLoading,
|
confirmLoading,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const hookId = useId();
|
||||||
const [nameValue, setNameValue] = useState("");
|
const theme = useTheme();
|
||||||
const confirmed = name === nameValue;
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const [userConfirmationText, setUserConfirmationText] = useState("");
|
||||||
setNameValue(event.target.value);
|
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 = (
|
const hasError = !deletionConfirmed && userConfirmationText.length > 0;
|
||||||
<>
|
const displayErrorMessage = hasError && !isFocused;
|
||||||
<p>Deleting this {entity} is irreversible!</p>
|
const inputColor = hasError ? "error" : "primary";
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -76,19 +55,50 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
|
|||||||
title={`Delete ${entity}`}
|
title={`Delete ${entity}`}
|
||||||
onConfirm={onConfirm}
|
onConfirm={onConfirm}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
description={content}
|
|
||||||
confirmLoading={confirmLoading}
|
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