feat(web): add markdown round-trip and replace textarea with Tiptap (#501)
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #501.
This commit is contained in:
2026-02-24 01:40:34 +00:00
committed by jason.woltje
parent a81c4a5edd
commit d5ecc0b107
6 changed files with 132 additions and 182 deletions

View File

@@ -1,13 +1,29 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { EntryEditor } from "../EntryEditor";
// Mock the LinkAutocomplete component
vi.mock("../LinkAutocomplete", () => ({
LinkAutocomplete: (): React.JSX.Element => (
<div data-testid="link-autocomplete">LinkAutocomplete</div>
// Mock KnowledgeEditor since Tiptap requires a full DOM
vi.mock("../KnowledgeEditor", () => ({
KnowledgeEditor: ({
content,
onChange,
placeholder,
}: {
content: string;
onChange: (md: string) => void;
placeholder?: string;
}): React.JSX.Element => (
<div data-testid="knowledge-editor" data-content={content} data-placeholder={placeholder}>
<button
data-testid="trigger-change"
onClick={(): void => {
onChange("updated content");
}}
>
Change
</button>
</div>
),
}));
@@ -21,133 +37,50 @@ describe("EntryEditor", (): void => {
vi.clearAllMocks();
});
it("should render textarea in edit mode by default", (): void => {
it("should render KnowledgeEditor component", (): void => {
render(<EntryEditor {...defaultProps} />);
const textarea = screen.getByPlaceholderText(/Write your content here/);
expect(textarea).toBeInTheDocument();
expect(textarea.tagName).toBe("TEXTAREA");
expect(screen.getByTestId("knowledge-editor")).toBeInTheDocument();
});
it("should display current content in textarea", (): void => {
it("should have a content label", (): void => {
render(<EntryEditor {...defaultProps} />);
expect(screen.getByText("Content")).toBeInTheDocument();
});
it("should pass content to KnowledgeEditor", (): void => {
const content = "# Test Content\n\nThis is a test.";
render(<EntryEditor {...defaultProps} content={content} />);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const textarea = screen.getByPlaceholderText(/Write your content here/) as HTMLTextAreaElement;
expect(textarea.value).toBe(content);
const editor = screen.getByTestId("knowledge-editor");
expect(editor).toHaveAttribute("data-content", content);
});
it("should call onChange when content is modified", async (): Promise<void> => {
it("should pass placeholder to KnowledgeEditor", (): void => {
render(<EntryEditor {...defaultProps} />);
const editor = screen.getByTestId("knowledge-editor");
expect(editor).toHaveAttribute(
"data-placeholder",
"Write your content here... Supports markdown formatting."
);
});
it("should forward onChange to KnowledgeEditor", async (): Promise<void> => {
const { default: userEvent } = await import("@testing-library/user-event");
const user = userEvent.setup();
const onChangeMock = vi.fn();
render(<EntryEditor {...defaultProps} onChange={onChangeMock} />);
const textarea = screen.getByPlaceholderText(/Write your content here/);
await user.type(textarea, "Hello");
expect(onChangeMock).toHaveBeenCalled();
await user.click(screen.getByTestId("trigger-change"));
expect(onChangeMock).toHaveBeenCalledWith("updated content");
});
it("should toggle between edit and preview modes", async (): Promise<void> => {
const user = userEvent.setup();
const content = "# Test\n\nPreview this content.";
it("should render with entry-editor wrapper class", (): void => {
const { container } = render(<EntryEditor {...defaultProps} />);
render(<EntryEditor {...defaultProps} content={content} />);
// Initially in edit mode
expect(screen.getByPlaceholderText(/Write your content here/)).toBeInTheDocument();
expect(screen.getByText("Preview")).toBeInTheDocument();
// Switch to preview mode
const previewButton = screen.getByText("Preview");
await user.click(previewButton);
// Should show preview
expect(screen.queryByPlaceholderText(/Write your content here/)).not.toBeInTheDocument();
expect(screen.getByText("Edit")).toBeInTheDocument();
// Check for partial content (newlines may split text across elements)
expect(screen.getByText(/Test/)).toBeInTheDocument();
expect(screen.getByText(/Preview this content/)).toBeInTheDocument();
// Switch back to edit mode
const editButton = screen.getByText("Edit");
await user.click(editButton);
// Should show textarea again
expect(screen.getByPlaceholderText(/Write your content here/)).toBeInTheDocument();
expect(screen.getByText("Preview")).toBeInTheDocument();
});
it("should render LinkAutocomplete component in edit mode", (): void => {
render(<EntryEditor {...defaultProps} />);
expect(screen.getByTestId("link-autocomplete")).toBeInTheDocument();
});
it("should not render LinkAutocomplete in preview mode", async (): Promise<void> => {
const user = userEvent.setup();
render(<EntryEditor {...defaultProps} />);
// LinkAutocomplete should be present in edit mode
expect(screen.getByTestId("link-autocomplete")).toBeInTheDocument();
// Switch to preview mode
const previewButton = screen.getByText("Preview");
await user.click(previewButton);
// LinkAutocomplete should not be in preview mode
expect(screen.queryByTestId("link-autocomplete")).not.toBeInTheDocument();
});
it("should show help text about wiki-link syntax", (): void => {
render(<EntryEditor {...defaultProps} />);
expect(screen.getByText(/Type/)).toBeInTheDocument();
expect(screen.getByText(/\[\[/)).toBeInTheDocument();
expect(screen.getByText(/to insert links/)).toBeInTheDocument();
});
it("should maintain content when toggling between modes", async (): Promise<void> => {
const user = userEvent.setup();
const content = "# My Content\n\nThis should persist.";
render(<EntryEditor {...defaultProps} content={content} />);
// Verify content in edit mode
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const textarea = screen.getByPlaceholderText(/Write your content here/) as HTMLTextAreaElement;
expect(textarea.value).toBe(content);
// Toggle to preview
await user.click(screen.getByText("Preview"));
// Check for partial content (newlines may split text across elements)
expect(screen.getByText(/My Content/)).toBeInTheDocument();
expect(screen.getByText(/This should persist/)).toBeInTheDocument();
// Toggle back to edit
await user.click(screen.getByText("Edit"));
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const textareaAfter = screen.getByPlaceholderText(
/Write your content here/
) as HTMLTextAreaElement;
expect(textareaAfter.value).toBe(content);
});
it("should apply correct styling classes", (): void => {
render(<EntryEditor {...defaultProps} />);
const textarea = screen.getByPlaceholderText(/Write your content here/);
expect(textarea).toHaveClass("font-mono");
expect(textarea).toHaveClass("text-sm");
expect(textarea).toHaveClass("min-h-[300px]");
});
it("should have label for content field", (): void => {
render(<EntryEditor {...defaultProps} />);
expect(screen.getByText("Content (Markdown)")).toBeInTheDocument();
expect(container.querySelector(".entry-editor")).toBeInTheDocument();
});
});