import { describe, it, expect, vi, beforeEach, afterEach } 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'); }); }); }); });