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");
});
});
});
});