import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AsciiDocMetadata } from "../../src/lib/utils/asciidoc_metadata"; // Mock all Svelte components and dependencies vi.mock("flowbite-svelte", () => ({ Textarea: vi.fn().mockImplementation((props) => { return { $$render: () => ``, $$bind: { value: props.bind, oninput: props.oninput }, }; }), Button: vi.fn().mockImplementation((props) => { return { $$render: () => ``, $$bind: { onclick: props.onclick }, }; }), })); vi.mock("flowbite-svelte-icons", () => ({ EyeOutline: vi.fn().mockImplementation(() => ({ $$render: () => ``, })), })); vi.mock("asciidoctor", () => ({ default: vi.fn(() => ({ convert: vi.fn((content, options) => { // Mock AsciiDoctor conversion - return simple HTML return content.replace(/^==\s+(.+)$/gm, "

$1

") .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\*(.+?)\*/g, "$1"); }), })), })); // Mock sessionStorage const mockSessionStorage = { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn(), }; Object.defineProperty(global, "sessionStorage", { value: mockSessionStorage, writable: true, }); // Mock window object for DOM manipulation Object.defineProperty(global, "window", { value: { sessionStorage: mockSessionStorage, document: { querySelector: vi.fn(), createElement: vi.fn(), }, }, writable: true, }); // Mock DOM methods const mockQuerySelector = vi.fn(); const mockCreateElement = vi.fn(); const mockAddEventListener = vi.fn(); const mockRemoveEventListener = vi.fn(); Object.defineProperty(global, "document", { value: { querySelector: mockQuerySelector, createElement: mockCreateElement, addEventListener: mockAddEventListener, removeEventListener: mockRemoveEventListener, }, writable: true, }); describe("ZettelEditor Component Logic", () => { let mockOnContentChange: ReturnType; let mockOnPreviewToggle: ReturnType; beforeEach(() => { vi.clearAllMocks(); mockOnContentChange = vi.fn(); mockOnPreviewToggle = vi.fn(); }); afterEach(() => { vi.clearAllMocks(); }); describe("Publication Format Detection Logic", () => { it("should detect document header format", () => { const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; // Test the regex pattern used in the component const hasDocumentHeader = contentWithDocumentHeader.match(/^=\s+/m); expect(hasDocumentHeader).toBeTruthy(); }); it("should detect index card format", () => { const contentWithIndexCard = "index card\n\n== Section 1\nContent"; // Test the logic used in the component const lines = contentWithIndexCard.split(/\r?\n/); let hasIndexCard = false; for (const line of lines) { if (line.trim().toLowerCase() === "index card") { hasIndexCard = true; break; } } expect(hasIndexCard).toBe(true); }); it("should not detect publication format for normal section content", () => { const normalContent = "== Section 1\nContent\n\n== Section 2\nMore content"; // Test the logic used in the component const lines = normalContent.split(/\r?\n/); let hasPublicationHeader = false; for (const line of lines) { if (line.match(/^=\s+(.+)$/)) { hasPublicationHeader = true; break; } if (line.trim().toLowerCase() === "index card") { hasPublicationHeader = true; break; } } expect(hasPublicationHeader).toBe(false); }); }); describe("Content Parsing Logic", () => { it("should parse sections with document header", () => { const content = "== Section 1\n:author: Test Author\n\nContent 1"; // Test the parsing logic const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); // This content doesn't have a document header // Test section splitting logic const sectionStrings = content.split(/(?=^==\s+)/gm).filter(( section: string, ) => section.trim()); expect(sectionStrings).toHaveLength(1); expect(sectionStrings[0]).toContain("== Section 1"); }); it("should parse sections without document header", () => { const content = "== Section 1\nContent 1"; // Test the parsing logic const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); // Test section splitting logic const sectionStrings = content.split(/(?=^==\s+)/gm).filter(( section: string, ) => section.trim()); expect(sectionStrings).toHaveLength(1); expect(sectionStrings[0]).toContain("== Section 1"); }); it("should handle empty content", () => { const content = ""; const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); }); }); describe("Content Conversion Logic", () => { it("should convert document title to section title", () => { const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; // Test the conversion logic let convertedContent = contentWithDocumentHeader.replace( /^=\s+(.+)$/gm, "== $1", ); convertedContent = convertedContent.replace(/^index card$/gim, ""); const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); expect(finalContent).toBe("== Document Title\n\n== Section 1\nContent"); }); it("should remove index card line", () => { const contentWithIndexCard = "index card\n\n== Section 1\nContent"; // Test the conversion logic let convertedContent = contentWithIndexCard.replace( /^=\s+(.+)$/gm, "== $1", ); convertedContent = convertedContent.replace(/^index card$/gim, ""); const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); expect(finalContent).toBe("\n\n== Section 1\nContent"); }); it("should clean up double newlines", () => { const contentWithExtraNewlines = "= Document Title\n\n\n== Section 1\nContent"; // Test the conversion logic let convertedContent = contentWithExtraNewlines.replace( /^=\s+(.+)$/gm, "== $1", ); convertedContent = convertedContent.replace(/^index card$/gim, ""); const finalContent = convertedContent.replace(/\n\s*\n\s*\n/g, "\n\n"); expect(finalContent).toBe("== Document Title\n\n== Section 1\nContent"); }); }); describe("SessionStorage Integration", () => { it("should store content in sessionStorage when switching to publication editor", () => { const contentWithDocumentHeader = "= Document Title\n\n== Section 1\nContent"; // Test the sessionStorage logic mockSessionStorage.setItem( "zettelEditorContent", contentWithDocumentHeader, ); mockSessionStorage.setItem("zettelEditorSource", "publication-format"); expect(mockSessionStorage.setItem).toHaveBeenCalledWith( "zettelEditorContent", contentWithDocumentHeader, ); expect(mockSessionStorage.setItem).toHaveBeenCalledWith( "zettelEditorSource", "publication-format", ); }); }); describe("Event Count Logic", () => { it("should calculate correct event count for single section", () => { const sections = [{ title: "Section 1", content: "Content 1", tags: [] }]; const eventCount = sections.length; const eventText = `${eventCount} event${eventCount !== 1 ? "s" : ""}`; expect(eventCount).toBe(1); expect(eventText).toBe("1 event"); }); it("should calculate correct event count for multiple sections", () => { const sections = [ { title: "Section 1", content: "Content 1", tags: [] }, { title: "Section 2", content: "Content 2", tags: [] }, ]; const eventCount = sections.length; const eventText = `${eventCount} event${eventCount !== 1 ? "s" : ""}`; expect(eventCount).toBe(2); expect(eventText).toBe("2 events"); }); }); describe("Tag Processing Logic", () => { it("should process tags correctly", () => { // Mock the metadataToTags function const mockMetadataToTags = vi.fn().mockReturnValue([[ "author", "Test Author", ]]); const mockMetadata = { title: "Section 1", author: "Test Author", } as AsciiDocMetadata; const tags = mockMetadataToTags(mockMetadata); expect(tags).toEqual([["author", "Test Author"]]); expect(mockMetadataToTags).toHaveBeenCalledWith(mockMetadata); }); it("should handle empty tags", () => { // Mock the metadataToTags function const mockMetadataToTags = vi.fn().mockReturnValue([]); const mockMetadata = { title: "Section 1" } as AsciiDocMetadata; const tags = mockMetadataToTags(mockMetadata); expect(tags).toEqual([]); }); }); describe("AsciiDoctor Processing", () => { it("should process AsciiDoc content correctly", () => { // Mock the asciidoctor conversion const mockConvert = vi.fn((content, options) => { return content.replace(/^==\s+(.+)$/gm, "

$1

") .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\*(.+?)\*/g, "$1"); }); const content = "== Test Section\n\nThis is **bold** and *italic* text."; const processedContent = mockConvert(content, { standalone: false, doctype: "article", attributes: { showtitle: true, sectids: true, }, }); expect(processedContent).toContain("

Test Section

"); expect(processedContent).toContain("bold"); expect(processedContent).toContain("italic"); }); }); describe("Error Handling", () => { it("should handle parsing errors gracefully", () => { // Mock a function that might throw an error const mockParseFunction = vi.fn().mockImplementation(() => { throw new Error("Parsing error"); }); const content = "== Section 1\nContent 1"; // Should not throw error when called expect(() => { try { mockParseFunction(content); } catch (error) { // Expected error, but should be handled gracefully } }).not.toThrow(); }); it("should handle empty content without errors", () => { const content = ""; const hasDocumentHeader = content.match(/^=\s+/m); expect(hasDocumentHeader).toBeFalsy(); }); }); describe("Component Props Interface", () => { it("should have correct prop types", () => { // Test that the component props interface is correctly defined const expectedProps = { content: "", placeholder: "Default placeholder", showPreview: false, onContentChange: vi.fn(), onPreviewToggle: vi.fn(), }; expect(expectedProps).toHaveProperty("content"); expect(expectedProps).toHaveProperty("placeholder"); expect(expectedProps).toHaveProperty("showPreview"); expect(expectedProps).toHaveProperty("onContentChange"); expect(expectedProps).toHaveProperty("onPreviewToggle"); }); }); describe("Utility Function Integration", () => { it("should integrate with ZettelParser utilities", () => { // Mock the parseAsciiDocSections function const mockParseAsciiDocSections = vi.fn().mockReturnValue([ { title: "Section 1", content: "Content 1", tags: [] }, ]); const content = "== Section 1\nContent 1"; const sections = mockParseAsciiDocSections(content, 2); expect(sections).toHaveLength(1); expect(sections[0].title).toBe("Section 1"); }); it("should integrate with asciidoc_metadata utilities", () => { // Mock the utility functions const mockExtractDocumentMetadata = vi.fn().mockReturnValue({ metadata: { title: "Document Title" } as AsciiDocMetadata, content: "Document content", }); const mockExtractSectionMetadata = vi.fn().mockReturnValue({ metadata: { title: "Section Title" } as AsciiDocMetadata, content: "Section content", title: "Section Title", }); const documentContent = "= Document Title\nDocument content"; const sectionContent = "== Section Title\nSection content"; const documentResult = mockExtractDocumentMetadata(documentContent); const sectionResult = mockExtractSectionMetadata(sectionContent); expect(documentResult.metadata.title).toBe("Document Title"); expect(sectionResult.title).toBe("Section Title"); }); }); describe("Content Validation", () => { it("should validate content structure", () => { const validContent = "== Section 1\nContent here\n\n== Section 2\nMore content"; const invalidContent = "Just some text without sections"; // Test section detection const validSections = validContent.split(/(?=^==\s+)/gm).filter(( section: string, ) => section.trim()); const invalidSections = invalidContent.split(/(?=^==\s+)/gm).filter(( section: string, ) => section.trim()); expect(validSections.length).toBeGreaterThan(0); // The invalid content will have one section (the entire content) since it doesn't start with == expect(invalidSections.length).toBe(1); }); it("should handle mixed content types", () => { const mixedContent = "= Document Title\n\n== Section 1\nContent\n\n== Section 2\nMore content"; // Test document header detection const hasDocumentHeader = mixedContent.match(/^=\s+/m); expect(hasDocumentHeader).toBeTruthy(); // Test section extraction const sections = mixedContent.split(/(?=^==\s+)/gm).filter(( section: string, ) => section.trim()); expect(sections.length).toBeGreaterThan(0); }); }); describe("String Manipulation", () => { it("should handle string replacements correctly", () => { const originalContent = "= Title\n\n== Section\nContent"; // Test various string manipulations const convertedContent = originalContent .replace(/^=\s+(.+)$/gm, "== $1") .replace(/^index card$/gim, "") .replace(/\n\s*\n\s*\n/g, "\n\n"); expect(convertedContent).toBe("== Title\n\n== Section\nContent"); }); it("should handle edge cases in string manipulation", () => { const edgeCases = [ "= Title\n\n\n== Section\nContent", // Multiple newlines "index card\n\n== Section\nContent", // Index card "= Title\nindex card\n== Section\nContent", // Both ]; edgeCases.forEach((content) => { const converted = content .replace(/^=\s+(.+)$/gm, "== $1") .replace(/^index card$/gim, "") .replace(/\n\s*\n\s*\n/g, "\n\n"); expect(converted).toBeDefined(); expect(typeof converted).toBe("string"); }); }); }); });