clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

429 lines
15 KiB

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: () => `<textarea data-testid="textarea" class="${props.class || ''}" rows="${props.rows || 12}" ${props.disabled ? 'disabled' : ''} placeholder="${props.placeholder || ''}"></textarea>`,
$$bind: { value: props.bind, oninput: props.oninput }
};
}),
Button: vi.fn().mockImplementation((props) => {
return {
$$render: () => `<button data-testid="preview-button" class="${props.class || ''}" ${props.disabled ? 'disabled' : ''} onclick="${props.onclick || ''}">${props.children || ''}</button>`,
$$bind: { onclick: props.onclick }
};
})
}));
vi.mock("flowbite-svelte-icons", () => ({
EyeOutline: vi.fn().mockImplementation(() => ({
$$render: () => `<svg data-testid="eye-icon"></svg>`
}))
}));
vi.mock("asciidoctor", () => ({
default: vi.fn(() => ({
convert: vi.fn((content, options) => {
// Mock AsciiDoctor conversion - return simple HTML
return content.replace(/^==\s+(.+)$/gm, '<h2>$1</h2>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>');
})
}))
}));
// 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<typeof vi.fn>;
let mockOnPreviewToggle: ReturnType<typeof vi.fn>;
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, '<h2>$1</h2>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>');
});
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('<h2>Test Section</h2>');
expect(processedContent).toContain('<strong>bold</strong>');
expect(processedContent).toContain('<em>italic</em>');
});
});
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');
});
});
});
});