Delete resource modal dark mode

This commit is contained in:
yuneng-jiang 2026-01-30 18:45:09 -08:00
parent 267993b388
commit b8e2ac46d1
2 changed files with 180 additions and 41 deletions

View file

@ -1,68 +1,193 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../tests/test-utils";
import { renderWithProviders, screen } from "../../../tests/test-utils";
import DeleteResourceModal from "./DeleteResourceModal";
describe("DeleteResourceModal", () => {
const mockOnCancel = vi.fn();
const mockOnOk = vi.fn();
const defaultProps = {
isOpen: true,
title: "Delete Resource",
message: "Are you sure you want to delete this resource?",
onCancel: vi.fn(),
onOk: vi.fn(),
onCancel: mockOnCancel,
onOk: mockOnOk,
confirmLoading: false,
};
it("renders", () => {
const { getByText } = renderWithProviders(<DeleteResourceModal {...defaultProps} />);
expect(getByText("Delete Resource")).toBeInTheDocument();
beforeEach(() => {
vi.clearAllMocks();
});
it("renders the title correctly", () => {
const { getByText } = renderWithProviders(<DeleteResourceModal {...defaultProps} title="Custom Delete Title" />);
expect(getByText("Custom Delete Title")).toBeInTheDocument();
it("should render", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} />);
expect(screen.getByText("Delete Resource")).toBeInTheDocument();
});
it("renders the message correctly", () => {
const { getByText } = renderWithProviders(
<DeleteResourceModal {...defaultProps} message="This is a custom message" />,
);
expect(getByText("This is a custom message")).toBeInTheDocument();
it("should render the title correctly", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} title="Custom Delete Title" />);
expect(screen.getByText("Custom Delete Title")).toBeInTheDocument();
});
it("renders the resourceInformation and resourceInformationTitle correctly", () => {
it("should render the message correctly", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} message="This is a custom message" />);
expect(screen.getByText("This is a custom message")).toBeInTheDocument();
});
it("should render alert message when provided", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} alertMessage="Warning: This action cannot be undone" />);
expect(screen.getByText("Warning: This action cannot be undone")).toBeInTheDocument();
});
it("should not render alert message when not provided", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} />);
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
it("should render resourceInformation and resourceInformationTitle correctly", () => {
const resourceInformation = [
{ label: "Name", value: "Test Resource" },
{ label: "ID", value: "123" },
];
const { getByText } = renderWithProviders(
renderWithProviders(
<DeleteResourceModal
{...defaultProps}
resourceInformationTitle="Resource Details"
resourceInformation={resourceInformation}
/>,
);
expect(getByText("Resource Details")).toBeInTheDocument();
expect(getByText("Name")).toBeInTheDocument();
expect(getByText("Test Resource")).toBeInTheDocument();
expect(getByText("ID")).toBeInTheDocument();
expect(getByText("123")).toBeInTheDocument();
expect(screen.getByText("Resource Details")).toBeInTheDocument();
expect(screen.getByText("Name")).toBeInTheDocument();
expect(screen.getByText("Test Resource")).toBeInTheDocument();
expect(screen.getByText("ID")).toBeInTheDocument();
expect(screen.getByText("123")).toBeInTheDocument();
});
it("disables the delete button when requiredConfirmation is not in the input (empty state)", async () => {
const { getByRole } = renderWithProviders(<DeleteResourceModal {...defaultProps} requiredConfirmation="DELETE" />);
const deleteButton = getByRole("button", { name: /delete/i });
it("should render dash for null or undefined resource information values", () => {
const resourceInformation = [
{ label: "Name", value: null },
{ label: "ID", value: undefined },
{ label: "Status", value: "Active" },
];
renderWithProviders(
<DeleteResourceModal {...defaultProps} resourceInformation={resourceInformation} />,
);
expect(screen.getAllByText("-")).toHaveLength(2);
expect(screen.getByText("Active")).toBeInTheDocument();
});
it("should render resource information with number values", () => {
const resourceInformation = [{ label: "Count", value: 42 }];
renderWithProviders(<DeleteResourceModal {...defaultProps} resourceInformation={resourceInformation} />);
expect(screen.getByText("42")).toBeInTheDocument();
});
it("should call onCancel when cancel button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<DeleteResourceModal {...defaultProps} />);
const cancelButton = screen.getByRole("button", { name: "Cancel" });
await user.click(cancelButton);
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
it("should call onOk when delete button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<DeleteResourceModal {...defaultProps} />);
const deleteButton = screen.getByRole("button", { name: /delete/i });
await user.click(deleteButton);
expect(mockOnOk).toHaveBeenCalledTimes(1);
});
it("should disable delete button when requiredConfirmation is not entered", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} requiredConfirmation="DELETE" />);
const deleteButton = screen.getByRole("button", { name: /delete/i });
expect(deleteButton).toBeDisabled();
});
it("enables the delete button when the input equals requiredConfirmation", async () => {
it("should disable delete button when requiredConfirmation input does not match exactly", async () => {
const user = userEvent.setup();
const { getByRole, getByPlaceholderText } = renderWithProviders(
<DeleteResourceModal {...defaultProps} requiredConfirmation="DELETE" />,
);
const input = getByPlaceholderText("DELETE");
renderWithProviders(<DeleteResourceModal {...defaultProps} requiredConfirmation="DELETE" />);
const input = screen.getByPlaceholderText("DELETE");
await user.type(input, "DELET");
const deleteButton = screen.getByRole("button", { name: /delete/i });
expect(deleteButton).toBeDisabled();
});
it("should enable delete button when requiredConfirmation input matches exactly", async () => {
const user = userEvent.setup();
renderWithProviders(<DeleteResourceModal {...defaultProps} requiredConfirmation="DELETE" />);
const input = screen.getByPlaceholderText("DELETE");
await user.type(input, "DELETE");
const deleteButton = getByRole("button", { name: /delete/i });
const deleteButton = screen.getByRole("button", { name: /delete/i });
expect(deleteButton).not.toBeDisabled();
});
it("should reset requiredConfirmation input when modal opens", async () => {
const user = userEvent.setup();
const { rerender } = renderWithProviders(
<DeleteResourceModal {...defaultProps} requiredConfirmation="DELETE" />,
);
const input = screen.getByPlaceholderText("DELETE");
await user.type(input, "DELETE");
expect(input).toHaveValue("DELETE");
rerender(<DeleteResourceModal {...defaultProps} isOpen={false} requiredConfirmation="DELETE" />);
rerender(<DeleteResourceModal {...defaultProps} isOpen={true} requiredConfirmation="DELETE" />);
const newInput = screen.getByPlaceholderText("DELETE");
expect(newInput).toHaveValue("");
});
it("should display deleting text on delete button when confirmLoading is true", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} confirmLoading={true} />);
expect(screen.getByText("Deleting...")).toBeInTheDocument();
});
it("should display delete text on delete button when confirmLoading is false", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} confirmLoading={false} />);
const deleteButton = screen.getByRole("button", { name: /delete/i });
expect(deleteButton).toBeInTheDocument();
expect(screen.queryByText("Deleting...")).not.toBeInTheDocument();
});
it("should disable delete button when confirmLoading is true", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} confirmLoading={true} />);
const deleteButton = screen.getByText("Deleting...").closest("button");
expect(deleteButton).toBeDisabled();
});
it("should disable cancel button when confirmLoading is true", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} confirmLoading={true} />);
const cancelButton = screen.getByRole("button", { name: "Cancel" });
expect(cancelButton).toBeDisabled();
});
it("should disable delete button when confirmLoading is true even if requiredConfirmation matches", async () => {
const user = userEvent.setup();
renderWithProviders(
<DeleteResourceModal {...defaultProps} confirmLoading={true} requiredConfirmation="DELETE" />,
);
const input = screen.getByPlaceholderText("DELETE");
await user.type(input, "DELETE");
const deleteButton = screen.getByText("Deleting...").closest("button");
expect(deleteButton).toBeDisabled();
});
it("should render required confirmation prompt with correct text", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} requiredConfirmation="DELETE" />);
expect(screen.getByText(/Type/i)).toBeInTheDocument();
expect(screen.getByText("DELETE")).toBeInTheDocument();
expect(screen.getByText(/to confirm deletion/i)).toBeInTheDocument();
});
it("should not render required confirmation section when not provided", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} />);
expect(screen.queryByPlaceholderText("DELETE")).not.toBeInTheDocument();
});
it("should not render modal when isOpen is false", () => {
renderWithProviders(<DeleteResourceModal {...defaultProps} isOpen={false} />);
expect(screen.queryByText("Delete Resource")).not.toBeInTheDocument();
});
});

View file

@ -1,4 +1,5 @@
import { Alert, Descriptions, Input, Modal, Typography } from "antd";
import { Alert, Card, Descriptions, Input, Modal, Typography, theme } from "antd";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import React, { useState, useEffect } from "react";
interface DeleteResourceModalProps {
@ -32,6 +33,7 @@ export default function DeleteResourceModal({
requiredConfirmation,
}: DeleteResourceModalProps) {
const { Title, Text } = Typography;
const { token } = theme.useToken();
const [requiredConfirmationInput, setRequiredConfirmationInput] = useState("");
useEffect(() => {
@ -57,25 +59,36 @@ export default function DeleteResourceModal({
>
<div className="space-y-4">
{alertMessage && <Alert message={alertMessage} type="warning" />}
<div className="mt-4 p-4 bg-red-50 rounded-lg border border-red-200">
<Title level={5} className="mb-3 text-gray-900">
{resourceInformationTitle}
</Title>
<Card
title={resourceInformationTitle}
className="mt-4"
styles={{
body: { padding: "16px" },
header: {
backgroundColor: token.colorErrorBg,
borderColor: token.colorErrorBorder,
},
}}
style={{
backgroundColor: token.colorErrorBg,
borderColor: token.colorErrorBorder,
}}
>
<Descriptions column={1} size="small">
{resourceInformation &&
resourceInformation.map(({ label, value, ...textProps }) => (
<Descriptions.Item key={label} label={<span className="font-semibold text-gray-700">{label}</span>}>
<Descriptions.Item key={label} label={<span className="font-semibold">{label}</span>}>
<Text {...textProps}>{value ?? "-"}</Text>
</Descriptions.Item>
))}
</Descriptions>
</div>
</Card>
<div>
<Text>{message}</Text>
</div>
{requiredConfirmation && (
<div className="mb-6 mt-4 pt-4 border-t border-gray-200">
<Text className="block text-base font-medium text-gray-700 mb-2">
<div className="mb-6 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<Text className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
<Text>Type </Text>
<Text strong type="danger">
{requiredConfirmation}
@ -86,7 +99,8 @@ export default function DeleteResourceModal({
value={requiredConfirmationInput}
onChange={(e) => setRequiredConfirmationInput(e.target.value)}
placeholder={requiredConfirmation}
className="rounded-md text-base border-gray-200"
className="rounded-md"
prefix={<ExclamationCircleOutlined style={{ color: token.colorError }} />}
autoFocus
/>
</div>