Browse Source

test(17-09): add PromptEditorModal component tests

- Test visibility (render/not render based on isOpen)
- Test textarea with initial prompt value and typing updates
- Test Submit button calls onSubmit with updated prompt
- Test Cancel button behavior with/without unsaved changes
- Test unsaved changes confirmation dialog
- Test Escape key and backdrop click behaviors
- Test font size dropdown persistence with localStorage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
8b26f042ba
  1. 596
      src/components/__tests__/PromptEditorModal.test.tsx

596
src/components/__tests__/PromptEditorModal.test.tsx

@ -0,0 +1,596 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { PromptEditorModal } from "@/components/modals/PromptEditorModal";
// Mock localStorage
const mockLocalStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
Object.defineProperty(window, "localStorage", {
value: mockLocalStorage,
writable: true,
});
describe("PromptEditorModal", () => {
beforeEach(() => {
vi.clearAllMocks();
mockLocalStorage.getItem.mockReturnValue(null);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Visibility", () => {
it("should not render when isOpen is false", () => {
render(
<PromptEditorModal
isOpen={false}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
expect(screen.queryByText("Edit Prompt")).not.toBeInTheDocument();
});
it("should render with title when isOpen is true", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
expect(screen.getByText("Edit Prompt")).toBeInTheDocument();
});
});
describe("Textarea", () => {
it("should display textarea with initial prompt value", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Initial test prompt"
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
expect(textarea).toHaveValue("Initial test prompt");
});
it("should update textarea value on typing", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "New prompt text" } });
expect(textarea).toHaveValue("New prompt text");
});
it("should update local state when initialPrompt prop changes", () => {
const { rerender } = render(
<PromptEditorModal
isOpen={true}
initialPrompt="First prompt"
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
expect(textarea).toHaveValue("First prompt");
rerender(
<PromptEditorModal
isOpen={true}
initialPrompt="Updated prompt"
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
expect(textarea).toHaveValue("Updated prompt");
});
});
describe("Submit Button", () => {
it("should render Submit button", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
expect(screen.getByText("Submit")).toBeInTheDocument();
});
it("should call onSubmit with updated prompt when Submit is clicked", () => {
const onSubmit = vi.fn();
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original prompt"
onSubmit={onSubmit}
onClose={onClose}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Modified prompt" } });
fireEvent.click(screen.getByText("Submit"));
expect(onSubmit).toHaveBeenCalledWith("Modified prompt");
expect(onClose).toHaveBeenCalled();
});
it("should call onSubmit with unchanged prompt if no edits made", () => {
const onSubmit = vi.fn();
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Same prompt"
onSubmit={onSubmit}
onClose={onClose}
/>
);
fireEvent.click(screen.getByText("Submit"));
expect(onSubmit).toHaveBeenCalledWith("Same prompt");
expect(onClose).toHaveBeenCalled();
});
});
describe("Cancel Button", () => {
it("should render Cancel button", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
expect(screen.getByText("Cancel")).toBeInTheDocument();
});
it("should call onClose without saving when Cancel is clicked and no changes", () => {
const onSubmit = vi.fn();
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original prompt"
onSubmit={onSubmit}
onClose={onClose}
/>
);
fireEvent.click(screen.getByText("Cancel"));
expect(onSubmit).not.toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
});
it("should show confirmation dialog when Cancel is clicked with unsaved changes", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original prompt"
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Modified prompt" } });
fireEvent.click(screen.getByText("Cancel"));
expect(screen.getByText("You have unsaved changes")).toBeInTheDocument();
});
});
describe("Unsaved Changes Confirmation Dialog", () => {
it("should show Discard and Submit buttons in confirmation dialog", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original"
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Changed" } });
fireEvent.click(screen.getByText("Cancel"));
expect(screen.getByText("You have unsaved changes")).toBeInTheDocument();
expect(screen.getByText("Discard")).toBeInTheDocument();
// Submit button is in both the main form and the confirmation dialog
const submitButtons = screen.getAllByText("Submit");
expect(submitButtons.length).toBe(2);
});
it("should close without saving when Discard is clicked", () => {
const onSubmit = vi.fn();
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original"
onSubmit={onSubmit}
onClose={onClose}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Changed" } });
fireEvent.click(screen.getByText("Cancel"));
fireEvent.click(screen.getByText("Discard"));
expect(onSubmit).not.toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
});
it("should save and close when Submit is clicked in confirmation dialog", () => {
const onSubmit = vi.fn();
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original"
onSubmit={onSubmit}
onClose={onClose}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Changed" } });
fireEvent.click(screen.getByText("Cancel"));
// Click the Submit button in the confirmation dialog
const submitButtons = screen.getAllByText("Submit");
fireEvent.click(submitButtons[1]); // Second Submit button is in the confirmation dialog
expect(onSubmit).toHaveBeenCalledWith("Changed");
expect(onClose).toHaveBeenCalled();
});
it("should dismiss confirmation dialog when close button is clicked", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original"
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Changed" } });
fireEvent.click(screen.getByText("Cancel"));
expect(screen.getByText("You have unsaved changes")).toBeInTheDocument();
// Click the X button to close the confirmation
const closeButton = screen.getByLabelText("Close");
fireEvent.click(closeButton);
expect(screen.queryByText("You have unsaved changes")).not.toBeInTheDocument();
});
it("should dismiss confirmation dialog when clicking outside of it", async () => {
const { container } = render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original"
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Changed" } });
fireEvent.click(screen.getByText("Cancel"));
expect(screen.getByText("You have unsaved changes")).toBeInTheDocument();
// Click on the confirmation backdrop
const confirmationBackdrop = container.querySelector(".absolute.inset-0.flex.items-center.justify-center.bg-black\\/60");
fireEvent.click(confirmationBackdrop!);
await waitFor(() => {
expect(screen.queryByText("You have unsaved changes")).not.toBeInTheDocument();
});
});
});
describe("Escape Key", () => {
it("should close modal without saving when Escape is pressed and no changes", () => {
const onSubmit = vi.fn();
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original prompt"
onSubmit={onSubmit}
onClose={onClose}
/>
);
fireEvent.keyDown(window, { key: "Escape" });
expect(onSubmit).not.toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
});
it("should show confirmation dialog when Escape is pressed with unsaved changes", () => {
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original prompt"
onSubmit={vi.fn()}
onClose={onClose}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Changed prompt" } });
fireEvent.keyDown(window, { key: "Escape" });
expect(screen.getByText("You have unsaved changes")).toBeInTheDocument();
expect(onClose).not.toHaveBeenCalled();
});
});
describe("Backdrop Click", () => {
it("should close modal when backdrop is clicked and no changes", () => {
const onClose = vi.fn();
const { container } = render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original prompt"
onSubmit={vi.fn()}
onClose={onClose}
/>
);
// Click on the backdrop (outer div with bg-black/50)
const backdrop = container.querySelector(".fixed.inset-0");
fireEvent.click(backdrop!);
expect(onClose).toHaveBeenCalled();
});
it("should show confirmation when backdrop is clicked with unsaved changes", () => {
const onClose = vi.fn();
const { container } = render(
<PromptEditorModal
isOpen={true}
initialPrompt="Original"
onSubmit={vi.fn()}
onClose={onClose}
/>
);
const textarea = screen.getByPlaceholderText("Describe what to generate...");
fireEvent.change(textarea, { target: { value: "Changed" } });
const backdrop = container.querySelector(".fixed.inset-0");
fireEvent.click(backdrop!);
expect(screen.getByText("You have unsaved changes")).toBeInTheDocument();
expect(onClose).not.toHaveBeenCalled();
});
it("should not close when clicking inside the dialog", () => {
const onClose = vi.fn();
render(
<PromptEditorModal
isOpen={true}
initialPrompt="Test"
onSubmit={vi.fn()}
onClose={onClose}
/>
);
// Click on the title (inside the dialog)
fireEvent.click(screen.getByText("Edit Prompt"));
expect(onClose).not.toHaveBeenCalled();
});
});
describe("Font Size", () => {
it("should render font size dropdown", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
// Look for a select element with font size options
const fontSizeSelect = screen.getByRole("combobox");
expect(fontSizeSelect).toBeInTheDocument();
});
it("should use default font size of 14px", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const fontSizeSelect = screen.getByRole("combobox");
expect(fontSizeSelect).toHaveValue("14");
});
it("should load saved font size from localStorage", () => {
mockLocalStorage.getItem.mockReturnValue("18");
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const fontSizeSelect = screen.getByRole("combobox");
expect(fontSizeSelect).toHaveValue("18");
});
it("should update font size when selection changes", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const fontSizeSelect = screen.getByRole("combobox");
fireEvent.change(fontSizeSelect, { target: { value: "20" } });
expect(fontSizeSelect).toHaveValue("20");
});
it("should save font size to localStorage when changed", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const fontSizeSelect = screen.getByRole("combobox");
fireEvent.change(fontSizeSelect, { target: { value: "20" } });
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
"prompt-editor-font-size",
"20"
);
});
it("should render all font size options", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
expect(screen.getByText("10px")).toBeInTheDocument();
expect(screen.getByText("12px")).toBeInTheDocument();
expect(screen.getByText("14px")).toBeInTheDocument();
expect(screen.getByText("16px")).toBeInTheDocument();
expect(screen.getByText("18px")).toBeInTheDocument();
expect(screen.getByText("20px")).toBeInTheDocument();
expect(screen.getByText("24px")).toBeInTheDocument();
});
it("should apply font size to textarea", () => {
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const fontSizeSelect = screen.getByRole("combobox");
fireEvent.change(fontSizeSelect, { target: { value: "20" } });
const textarea = screen.getByPlaceholderText("Describe what to generate...");
expect(textarea).toHaveStyle({ fontSize: "20px" });
});
it("should fall back to default if localStorage has invalid value", () => {
mockLocalStorage.getItem.mockReturnValue("invalid");
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const fontSizeSelect = screen.getByRole("combobox");
expect(fontSizeSelect).toHaveValue("14");
});
it("should fall back to default if localStorage value is out of range", () => {
mockLocalStorage.getItem.mockReturnValue("5"); // Below MIN_FONT_SIZE
render(
<PromptEditorModal
isOpen={true}
initialPrompt=""
onSubmit={vi.fn()}
onClose={vi.fn()}
/>
);
const fontSizeSelect = screen.getByRole("combobox");
expect(fontSizeSelect).toHaveValue("14");
});
});
});
Loading…
Cancel
Save