import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import { SpecEditor } from "../SpecEditor"; describe("SpecEditor", () => { const mockContent = `# KB-001 - Test Task **Created:** 2026-01-01 **Size:** M ## Mission Test mission description. ## Steps ### Step 1: Implementation - [ ] Do something `; const mockOnSave = vi.fn().mockResolvedValue(undefined); const mockOnRequestRevision = vi.fn().mockResolvedValue(undefined); beforeEach(() => { vi.clearAllMocks(); }); it("renders markdown content in view mode by default", () => { render(); // Should show "View" button as active const viewButton = screen.getByText("View"); expect(viewButton.classList.contains("btn-primary")).toBe(true); // Should render markdown content (without leading heading) expect(screen.getByText("Test mission description.")).toBeTruthy(); // Should not show textarea in view mode expect(screen.queryByRole("textbox")).toBeNull(); }); it("strips leading heading from markdown rendering", () => { render(); // The h1 heading should be stripped expect(screen.queryByRole("heading", { level: 1 })).toBeNull(); // But other content should be rendered expect(screen.getByText("Mission")).toBeTruthy(); }); it("shows empty state when no content", () => { render(); expect(screen.getByText("(no specification)")).toBeTruthy(); }); it("switches to edit mode when clicking Edit button", () => { render(); fireEvent.click(screen.getByText("Edit")); // Edit button should now be active const editButton = screen.getByText("Edit"); expect(editButton.classList.contains("btn-primary")).toBe(true); // Textarea should be visible with full content (including heading) const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; expect(textarea).toBeTruthy(); expect(textarea.value).toBe(mockContent); }); it("switches back to view mode when clicking View button", () => { render(); // Enter edit mode fireEvent.click(screen.getByText("Edit")); expect(screen.getByRole("textbox")).toBeTruthy(); // Click View to cancel fireEvent.click(screen.getByText("View")); // Should be back in view mode expect(screen.queryByRole("textbox")).toBeNull(); expect(screen.getByText("Test mission description.")).toBeTruthy(); }); it("enters edit mode on double-click when not in readOnly mode", () => { render(); // Double-click on the markdown body const markdownBody = document.querySelector(".markdown-body"); expect(markdownBody).toBeTruthy(); fireEvent.doubleClick(markdownBody!); // Should now be in edit mode expect(screen.getByRole("textbox")).toBeTruthy(); const editButton = screen.getByText("Edit") as HTMLButtonElement; expect(editButton.classList.contains("btn-primary")).toBe(true); }); it("does not enter edit mode on double-click when readOnly is true", () => { render(); // Double-click on the markdown body const markdownBody = document.querySelector(".markdown-body"); expect(markdownBody).toBeTruthy(); fireEvent.doubleClick(markdownBody!); // Should NOT be in edit mode (no textarea) expect(screen.queryByRole("textbox")).toBeNull(); }); it("does not enter edit mode on double-click when already in edit mode", () => { render(); // Enter edit mode via Edit button fireEvent.click(screen.getByText("Edit")); expect(screen.getByRole("textbox")).toBeTruthy(); // Double-clicking again should not break anything const markdownBody = document.querySelector(".markdown-body"); // Note: in edit mode, markdown-body is not rendered, so this query returns null expect(markdownBody).toBeNull(); }); it("cancels edit mode when pressing Escape key", () => { render(); // Enter edit mode fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; // Make a change fireEvent.change(textarea, { target: { value: "Changed content" } }); // Press Escape fireEvent.keyDown(textarea, { key: "Escape" }); // Should be back in view mode expect(screen.queryByRole("textbox")).toBeNull(); expect(screen.getByText("Test mission description.")).toBeTruthy(); }); it("disables Edit button when already in edit mode", () => { render(); fireEvent.click(screen.getByText("Edit")); const editButton = screen.getByText("Edit") as HTMLButtonElement; expect(editButton.disabled).toBe(true); }); it("disables View button when in view mode", () => { render(); const viewButton = screen.getByText("View") as HTMLButtonElement; expect(viewButton.disabled).toBe(true); }); it("calls onSave with edited content when clicking Save", async () => { render(); // Enter edit mode fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; const newContent = "# Updated Content\n\nNew description."; fireEvent.change(textarea, { target: { value: newContent } }); // Save button should be enabled after changes const saveButton = screen.getByText("Save") as HTMLButtonElement; expect(saveButton.disabled).toBe(false); fireEvent.click(saveButton); await waitFor(() => { expect(mockOnSave).toHaveBeenCalledWith(newContent); }); }); it("disables save button when content is unchanged", () => { render(); fireEvent.click(screen.getByText("Edit")); const saveButton = screen.getByText("Save") as HTMLButtonElement; expect(saveButton.disabled).toBe(true); }); it("disables save button when isSaving is true", () => { render(); fireEvent.click(screen.getByText("Edit")); // Make a change const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "Changed" } }); const saveButton = screen.getByText("Saving…") as HTMLButtonElement; expect(saveButton.disabled).toBe(true); }); it("shows saving state on save button", () => { render(); fireEvent.click(screen.getByText("Edit")); expect(screen.getByText("Saving…")).toBeTruthy(); }); it("resets content to original when canceling edit", () => { render(); fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "Changed content" } }); fireEvent.click(screen.getByText("Cancel")); // Re-enter edit mode fireEvent.click(screen.getByText("Edit")); // Should show original content expect((screen.getByRole("textbox") as HTMLTextAreaElement). value).toBe(mockContent); }); it("does not show edit controls in readOnly mode", () => { render(); expect(screen.queryByText("Edit")).toBeNull(); expect(screen.queryByText("Save")).toBeNull(); expect(screen.queryByText("Cancel")).toBeNull(); }); it("shows AI revision section when onRequestRevision is provided", () => { render( ); expect(screen.getByText("Ask AI to Revise")).toBeTruthy(); expect(screen.getByPlaceholderText(/e.g., 'Add more details/)).toBeTruthy(); expect(screen.getByText("Request AI Revision")).toBeTruthy(); }); it("does not show AI revision section when onRequestRevision is not provided", () => { render(); expect(screen.queryByText("Ask AI to Revise")).toBeNull(); expect(screen.queryByText("Request AI Revision")).toBeNull(); }); it("does not show AI revision section in readOnly mode", () => { render( ); expect(screen.queryByText("Ask AI to Revise")).toBeNull(); }); it("calls onRequestRevision with feedback when clicking Request AI Revision", async () => { render( ); const feedbackInput = screen.getByPlaceholderText(/e.g., 'Add more details/) as HTMLTextAreaElement; const feedbackText = "Please add more details about error handling"; fireEvent.change(feedbackInput, { target: { value: feedbackText } }); fireEvent.click(screen.getByText("Request AI Revision")); await waitFor(() => { expect(mockOnRequestRevision).toHaveBeenCalledWith(feedbackText); }); }); it("clears feedback after successful revision request", async () => { render( ); const feedbackInput = screen.getByPlaceholderText(/e.g., 'Add more details/) as HTMLTextAreaElement; fireEvent.change(feedbackInput, { target: { value: "Some feedback" } }); fireEvent.click(screen.getByText("Request AI Revision")); await waitFor(() => { expect(feedbackInput.value).toBe(""); }); }); it("disables revision request button when feedback is empty", () => { render( ); const button = screen.getByText("Request AI Revision") as HTMLButtonElement; expect(button.disabled).toBe(true); }); it("disables revision request button when isRequesting is true", () => { render( ); const feedbackInput = screen.getByPlaceholderText(/e.g., 'Add more details/) as HTMLTextAreaElement; fireEvent.change(feedbackInput, { target: { value: "Some feedback" } }); const button = screen.getByText("Requesting…") as HTMLButtonElement; expect(button.disabled).toBe(true); }); it("shows requesting state on revision button", () => { render( ); expect(screen.getByText("Requesting…")).toBeTruthy(); }); it("shows character count for feedback", () => { render( ); const feedbackInput = screen.getByPlaceholderText(/e.g., 'Add more details/) as HTMLTextAreaElement; fireEvent.change(feedbackInput, { target: { value: "Hello" } }); expect(screen.getByText("5/2000")).toBeTruthy(); }); it("enforces maxLength on feedback textarea", () => { render( ); const feedbackInput = screen.getByPlaceholderText(/e.g., 'Add more details/) as HTMLTextAreaElement; expect(feedbackInput.getAttribute("maxLength")).toBe("2000"); }); it("triggers save on Ctrl+Enter keyboard shortcut in edit mode", async () => { render(); fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; const newContent = "# Updated Content\n\nNew description."; fireEvent.change(textarea, { target: { value: newContent } }); // Simulate Ctrl+Enter await act(async () => { fireEvent.keyDown(document, { key: "Enter", ctrlKey: true }); }); await waitFor(() => { expect(mockOnSave).toHaveBeenCalledWith(newContent); }); }); it("triggers save on Cmd+Enter keyboard shortcut in edit mode", async () => { render(); fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; const newContent = "# Updated Content\n\nNew description."; fireEvent.change(textarea, { target: { value: newContent } }); // Simulate Cmd+Enter await act(async () => { fireEvent.keyDown(document, { key: "Enter", metaKey: true }); }); await waitFor(() => { expect(mockOnSave).toHaveBeenCalledWith(newContent); }); }); it("does not trigger save on Ctrl+Enter when not in edit mode", async () => { render(); // Not in edit mode await act(async () => { fireEvent.keyDown(document, { key: "Enter", ctrlKey: true }); }); expect(mockOnSave).not.toHaveBeenCalled(); }); it("does not trigger save on Ctrl+Enter when save button is disabled", async () => { render(); fireEvent.click(screen.getByText("Edit")); // No changes made, so save should be disabled await act(async () => { fireEvent.keyDown(document, { key: "Enter", ctrlKey: true }); }); expect(mockOnSave).not.toHaveBeenCalled(); }); it("prevents default on Ctrl+Enter keyboard shortcut", async () => { render(); fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "Changed" } }); const keyDownEvent = new KeyboardEvent("keydown", { key: "Enter", ctrlKey: true, bubbles: true, }); const preventDefaultSpy = vi.spyOn(keyDownEvent, "preventDefault"); document.dispatchEvent(keyDownEvent); expect(preventDefaultSpy).toHaveBeenCalled(); }); it("updates content when prop changes while in view mode", () => { const { rerender } = render(); expect(screen.getByText("Test mission description.")).toBeTruthy(); const newContent = "# New Task\n\nUpdated description."; rerender(); expect(screen.getByText("Updated description.")).toBeTruthy(); }); it("does not update textarea content when prop changes while in edit mode", () => { const { rerender } = render(); fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "User edited content" } }); // Re-render with different content rerender(); // Textarea should still show user's edits expect(textarea.value).toBe("User edited content"); }); it("shows keyboard hint in edit mode", () => { render(); fireEvent.click(screen.getByText("Edit")); // Check for keyboard hint text (use a more specific query) expect(screen.getByText(/Press/)).toBeTruthy(); // Check for individual kbd elements const kbdElements = screen.getAllByText("Enter"); expect(kbdElements.length).toBeGreaterThanOrEqual(2); expect(screen.getByText(/Ctrl/)).toBeTruthy(); expect(screen.getByText(/Cmd/)).toBeTruthy(); }); it("does not show keyboard hint in view mode", () => { render(); expect(screen.queryByText(/Ctrl/)).toBeNull(); }); it("textarea fills available width in edit mode", () => { render(); fireEvent.click(screen.getByText("Edit")); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; expect(textarea.classList.contains("spec-editor-textarea")).toBe(true); // Verify textarea has the CSS class for proper sizing expect(getComputedStyle(textarea).width).toBeDefined(); }); it("spec-editor-content has correct flex direction", () => { const { container } = render(); const contentDiv = container.querySelector(".spec-editor-content"); expect(contentDiv).toBeTruthy(); }); it("toolbar remains visible when content is long", () => { const longContent = "# Title\n\n" + "Content line\n".repeat(100); render(); fireEvent.click(screen.getByText("Edit")); // Toolbar should still be visible const toolbar = document.querySelector(".spec-editor-toolbar"); expect(toolbar).toBeTruthy(); // View and Edit buttons should still be present expect(screen.getByText("View")).toBeTruthy(); expect(screen.getByText("Edit")).toBeTruthy(); }); });