import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { loadAllAppCss } from "../../test/cssFixture"; import { FileEditor } from "../FileEditor"; describe("FileEditor", () => { it("renders textarea with correct class names", () => { render(); const textarea = screen.getByRole("textbox"); expect(textarea.classList.contains("file-editor-textarea")).toBe(true); }); it("renders with content prop value", () => { const content = "const x = 42;"; render(); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe(content); }); it("calls onChange when text is modified", () => { const onChange = vi.fn(); render(); const textarea = screen.getByRole("textbox"); fireEvent.change(textarea, { target: { value: "new content" } }); expect(onChange).toHaveBeenCalledWith("new content"); }); it("respects readOnly prop", () => { render(); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; expect(textarea.readOnly).toBe(true); }); it("has correct aria-label based on filePath prop", () => { const filePath = "src/components/App.tsx"; render(); const textarea = screen.getByRole("textbox"); expect(textarea.getAttribute("aria-label")).toBe(`Editor for ${filePath}`); }); it("has default aria-label when filePath is not provided", () => { render(); const textarea = screen.getByRole("textbox"); expect(textarea.getAttribute("aria-label")).toBe("File editor"); }); it("has spellCheck disabled", () => { render(); const textarea = screen.getByRole("textbox"); expect(textarea.getAttribute("spellcheck")).toBe("false"); }); it("is not readOnly by default", () => { render(); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; expect(textarea.readOnly).toBe(false); }); describe("markdown preview", () => { it("shows edit/preview toggle for .md files", () => { render(); expect(screen.getByRole("button", { name: /edit/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /preview/i })).toBeInTheDocument(); }); it("shows edit/preview toggle for .markdown files", () => { render(); expect(screen.getByRole("button", { name: /edit/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /preview/i })).toBeInTheDocument(); }); it("shows edit/preview toggle for .mdx files", () => { render(); expect(screen.getByRole("button", { name: /edit/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /preview/i })).toBeInTheDocument(); }); it("does not show edit/preview toggle for non-markdown files", () => { render(); expect(screen.queryByRole("button", { name: /edit/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /preview/i })).not.toBeInTheDocument(); }); it("does not show edit/preview toggle when filePath is not provided", () => { render(); expect(screen.queryByRole("button", { name: /edit/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /preview/i })).not.toBeInTheDocument(); }); it("defaults to edit mode for markdown files", () => { render(); // Textarea should be visible const textarea = screen.getByRole("textbox"); expect(textarea).toBeInTheDocument(); expect(textarea.tagName.toLowerCase()).toBe("textarea"); }); it("switches to preview mode when preview button is clicked", () => { render(); // Click preview button const previewButton = screen.getByRole("button", { name: /preview/i }); fireEvent.click(previewButton); // Preview should be visible (no textarea) expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); expect(document.querySelector(".file-editor-preview")).toBeInTheDocument(); }); it("switches back to edit mode when edit button is clicked", () => { render(); // Switch to preview first const previewButton = screen.getByRole("button", { name: /preview/i }); fireEvent.click(previewButton); // Then switch back to edit const editButton = screen.getByRole("button", { name: /edit/i }); fireEvent.click(editButton); // Textarea should be visible again const textarea = screen.getByRole("textbox"); expect(textarea).toBeInTheDocument(); }); it("renders markdown content in preview mode", () => { render(); // Switch to preview const previewButton = screen.getByRole("button", { name: /preview/i }); fireEvent.click(previewButton); // Check that the markdown is rendered (heading should be present) expect(document.querySelector(".file-editor-preview")).toBeInTheDocument(); }); it("hides edit button in readOnly mode for markdown files", () => { render(); // Edit button should not be visible expect(screen.queryByRole("button", { name: /edit/i })).not.toBeInTheDocument(); // Preview button should still be visible expect(screen.getByRole("button", { name: /preview/i })).toBeInTheDocument(); }); it("defaults to preview mode in readOnly mode for markdown files", () => { render(); // Preview should be active by default (no textarea in readOnly) expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); expect(document.querySelector(".file-editor-preview")).toBeInTheDocument(); }); it("preview button is disabled when already in preview mode", () => { render(); // Switch to preview const previewButton = screen.getByRole("button", { name: /preview/i }); fireEvent.click(previewButton); // Preview button should now be disabled expect(previewButton).toBeDisabled(); }); it("edit button is disabled when already in edit mode", () => { render(); const editButton = screen.getByRole("button", { name: /edit/i }); // Edit button should be disabled in edit mode expect(editButton).toBeDisabled(); }); }); describe("word wrap toggle", () => { it("shows word wrap toggle button for markdown files in edit mode", () => { render(); expect(screen.getByRole("button", { name: /toggle word wrap/i })).toBeInTheDocument(); }); it("shows word wrap toggle button for non-markdown files", () => { render(); expect(screen.getByRole("button", { name: /toggle word wrap/i })).toBeInTheDocument(); }); it("does not show word wrap toggle button in readOnly mode", () => { render(); expect(screen.queryByRole("button", { name: /toggle word wrap/i })).not.toBeInTheDocument(); }); it("word wrap is enabled by default", () => { render(); const textarea = screen.getByRole("textbox"); expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(true); }); it("toggle button shows active state when word wrap is enabled", () => { render(); const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i }); expect(wrapButton.classList.contains("btn-primary")).toBe(true); }); it("clicking toggle button disables word wrap", () => { render(); const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i }); fireEvent.click(wrapButton); const textarea = screen.getByRole("textbox"); expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(false); }); it("clicking toggle button again re-enables word wrap", () => { render(); const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i }); fireEvent.click(wrapButton); // turn off fireEvent.click(wrapButton); // turn on const textarea = screen.getByRole("textbox"); expect(textarea.classList.contains("file-editor-textarea--wrap")).toBe(true); }); it("toggle button loses active state when word wrap is disabled", () => { render(); const wrapButton = screen.getByRole("button", { name: /toggle word wrap/i }); fireEvent.click(wrapButton); expect(wrapButton.classList.contains("btn-primary")).toBe(false); }); }); describe("line numbers", () => { it("shows the line number toggle button when toggle support is provided", () => { render( , ); expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("aria-pressed", "false"); expect(screen.getByRole("button", { name: /toggle line numbers/i })).toHaveAttribute("title", "Toggle line numbers"); }); it("hides the line number toggle button when toggle support is not provided", () => { render(); expect(screen.queryByRole("button", { name: /toggle line numbers/i })).not.toBeInTheDocument(); }); it("calls onToggleLineNumbers when the toggle button is clicked", () => { const onToggleLineNumbers = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: /toggle line numbers/i })); expect(onToggleLineNumbers).toHaveBeenCalledTimes(1); }); it("hides the line number toggle button for read-only files", () => { render( , ); expect(screen.queryByRole("button", { name: /toggle line numbers/i })).not.toBeInTheDocument(); }); it("shows line numbers for editable text mode when enabled", () => { render( , ); const gutter = document.querySelector(".file-editor-line-numbers"); expect(gutter).toBeInTheDocument(); expect(screen.getByText("1")).toBeInTheDocument(); expect(screen.getByText("2")).toBeInTheDocument(); expect(screen.getByText("3")).toBeInTheDocument(); }); it("hides line numbers in markdown preview mode", () => { render( , ); fireEvent.click(screen.getByRole("button", { name: /preview mode/i })); expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /toggle line numbers/i })).not.toBeInTheDocument(); }); it("hides line numbers for read-only files", () => { render( , ); expect(document.querySelector(".file-editor-line-numbers")).not.toBeInTheDocument(); }); }); describe("mobile toolbar CSS", () => { it("keeps file editor toolbar action buttons at a shared mobile touch target size", () => { const css = loadAllAppCss(); const selectorIndex = css.indexOf(".file-editor-toolbar-actions .btn"); expect(selectorIndex).toBeGreaterThanOrEqual(0); const mediaIndex = css.lastIndexOf("@media (max-width: 768px)", selectorIndex); expect(mediaIndex).toBeGreaterThanOrEqual(0); const openBraceIndex = css.indexOf("{", mediaIndex); let depth = 1; let cursor = openBraceIndex + 1; while (cursor < css.length && depth > 0) { if (css[cursor] === "{") { depth += 1; } else if (css[cursor] === "}") { depth -= 1; } cursor += 1; } const mobileCss = css.slice(openBraceIndex + 1, cursor - 1); expect(mobileCss).toMatch( /\.file-editor-toolbar-actions\s+\.btn\s*\{[^}]*min-height:\s*var\(--mobile-nav-height\);[^}]*min-width:\s*var\(--mobile-nav-height\);[^}]*\}/, ); }); }); describe("markdown preview scrollability", () => { it("preview container has correct CSS classes for scrolling", () => { render(); // Switch to preview mode const previewButton = screen.getByRole("button", { name: /preview/i }); fireEvent.click(previewButton); const preview = document.querySelector(".file-editor-preview"); expect(preview).toBeInTheDocument(); // The class includes scrollable styles: flex: 1, min-height: 0, overflow-y: auto expect(preview?.classList.contains("file-editor-preview")).toBe(true); }); }); });