From 8b26f042bae49450125a4f9a6fdcc602545b142f Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 13 Jan 2026 13:08:47 +1300 Subject: [PATCH] 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 --- .../__tests__/PromptEditorModal.test.tsx | 596 ++++++++++++++++++ 1 file changed, 596 insertions(+) create mode 100644 src/components/__tests__/PromptEditorModal.test.tsx diff --git a/src/components/__tests__/PromptEditorModal.test.tsx b/src/components/__tests__/PromptEditorModal.test.tsx new file mode 100644 index 00000000..641a0cdd --- /dev/null +++ b/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( + + ); + + expect(screen.queryByText("Edit Prompt")).not.toBeInTheDocument(); + }); + + it("should render with title when isOpen is true", () => { + render( + + ); + + expect(screen.getByText("Edit Prompt")).toBeInTheDocument(); + }); + }); + + describe("Textarea", () => { + it("should display textarea with initial prompt value", () => { + render( + + ); + + const textarea = screen.getByPlaceholderText("Describe what to generate..."); + expect(textarea).toHaveValue("Initial test prompt"); + }); + + it("should update textarea value on typing", () => { + render( + + ); + + 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( + + ); + + const textarea = screen.getByPlaceholderText("Describe what to generate..."); + expect(textarea).toHaveValue("First prompt"); + + rerender( + + ); + + expect(textarea).toHaveValue("Updated prompt"); + }); + }); + + describe("Submit Button", () => { + it("should render Submit button", () => { + render( + + ); + + 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( + + ); + + 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( + + ); + + fireEvent.click(screen.getByText("Submit")); + + expect(onSubmit).toHaveBeenCalledWith("Same prompt"); + expect(onClose).toHaveBeenCalled(); + }); + }); + + describe("Cancel Button", () => { + it("should render Cancel button", () => { + render( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + // 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( + + ); + + 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( + + ); + + // 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( + + ); + + // 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( + + ); + + const fontSizeSelect = screen.getByRole("combobox"); + expect(fontSizeSelect).toHaveValue("14"); + }); + + it("should load saved font size from localStorage", () => { + mockLocalStorage.getItem.mockReturnValue("18"); + + render( + + ); + + const fontSizeSelect = screen.getByRole("combobox"); + expect(fontSizeSelect).toHaveValue("18"); + }); + + it("should update font size when selection changes", () => { + render( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + const fontSizeSelect = screen.getByRole("combobox"); + expect(fontSizeSelect).toHaveValue("14"); + }); + }); +});