import AutoStatusPlugin from '../src/main'; import { App, TFile, Notice } from './mock_obsidian'; import { FILE_PROCESSING_SCENARIOS } from './test-scenarios'; import { NOTES_WITH_PENDING_TASKS, NOTES_WITH_ALL_COMPLETED_TASKS, NOTES_WITH_NO_TASKS, NOTE_WITH_PENDING_TASKS_PROPERTY, NOTES_WITH_NESTED_TASKS, } from './sample-notes'; // Mock Notice constructor jest.mock('obsidian', () => { const originalModule = jest.requireActual('./mock_obsidian'); return { ...originalModule, Notice: jest.fn().mockImplementation((message: string) => ({ hide: jest.fn(), })), }; }); describe('File Processing Logic', () => { let plugin: AutoStatusPlugin; let mockApp: App; beforeEach(() => { mockApp = new App(); plugin = new AutoStatusPlugin(mockApp as any, {} as any); plugin.settings = { propertyName: 'Pending tasks', enableAutoProcessing: true, excludeFolders: [], includeFolders: [], debounceDelay: 500, enableTaskCounts: false, pendingTasksCountProperty: 'Pending tasks count', totalTasksCountProperty: 'Total tasks count', countRootTasksOnly: false, }; // Reset all mocks jest.clearAllMocks(); }); describe('shouldProcessFile', () => { test('should process markdown files', () => { const file = new TFile('test.md'); expect(plugin.shouldProcessFile(file)).toBe(true); }); test('should not process non-markdown files', () => { const txtFile = new TFile('test.txt'); const pdfFile = new TFile('document.pdf'); const imgFile = new TFile('image.png'); expect(plugin.shouldProcessFile(txtFile)).toBe(false); expect(plugin.shouldProcessFile(pdfFile)).toBe(false); expect(plugin.shouldProcessFile(imgFile)).toBe(false); }); test.each(FILE_PROCESSING_SCENARIOS)('$name', scenario => { const file = new TFile(scenario.filePath); if (scenario.parentPath) { file.parent = { path: scenario.parentPath }; } if (scenario.settings) { plugin.settings = { ...plugin.settings, ...scenario.settings, }; } expect(plugin.shouldProcessFile(file)).toBe(scenario.shouldProcess); }); test('should handle files in root directory', () => { const rootFile = new TFile('root-note.md'); rootFile.parent = null; expect(plugin.shouldProcessFile(rootFile)).toBe(true); }); }); describe('processFile', () => { beforeEach(() => { // Setup mocks with proper Jest mock functions mockApp.vault.read = jest.fn(); mockApp.fileManager.processFrontMatter = jest.fn(); }); test('should add "Pending tasks: true" for files with pending tasks', async () => { const file = new TFile('test.md'); (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_PENDING_TASKS); let capturedFrontmatter: any; (mockApp.fileManager.processFrontMatter as jest.Mock).mockImplementation( (file: TFile, callback: (fm: any) => void) => { const frontmatter = {}; callback(frontmatter); capturedFrontmatter = frontmatter; return Promise.resolve(); }, ); await plugin.processFile(file); expect(mockApp.vault.read).toHaveBeenCalledWith(file); expect(mockApp.fileManager.processFrontMatter).toHaveBeenCalledWith(file, expect.any(Function)); expect(capturedFrontmatter).toEqual({ 'Pending tasks': true }); }); test('should remove "Pending tasks" property when all tasks are completed', async () => { const file = new TFile('test.md'); (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_ALL_COMPLETED_TASKS); let capturedFrontmatter: any; (mockApp.fileManager.processFrontMatter as jest.Mock).mockImplementation( (file: TFile, callback: (fm: any) => void) => { const frontmatter = { 'Pending tasks': true, 'other-prop': 'value' }; callback(frontmatter); capturedFrontmatter = frontmatter; return Promise.resolve(); }, ); await plugin.processFile(file); expect(capturedFrontmatter).toEqual({ 'other-prop': 'value' }); expect(capturedFrontmatter).not.toHaveProperty('Pending tasks'); }); test('should remove "Pending tasks" property when no tasks exist', async () => { const file = new TFile('test.md'); (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_NO_TASKS); let capturedFrontmatter: any; (mockApp.fileManager.processFrontMatter as jest.Mock).mockImplementation( (file: TFile, callback: (fm: any) => void) => { const frontmatter = { 'Pending tasks': true }; callback(frontmatter); capturedFrontmatter = frontmatter; return Promise.resolve(); }, ); await plugin.processFile(file); expect(capturedFrontmatter).toEqual({}); expect(capturedFrontmatter).not.toHaveProperty('Pending tasks'); }); test('should use custom property name from settings', async () => { plugin.settings.propertyName = 'Custom Property'; const file = new TFile('test.md'); (mockApp.vault.read as jest.Mock).mockResolvedValue('- [ ] Task'); let capturedFrontmatter: any; (mockApp.fileManager.processFrontMatter as jest.Mock).mockImplementation( (file: TFile, callback: (fm: any) => void) => { const frontmatter = {}; callback(frontmatter); capturedFrontmatter = frontmatter; return Promise.resolve(); }, ); await plugin.processFile(file); expect(capturedFrontmatter).toEqual({ 'Custom Property': true }); }); test('should not process files that should be excluded', async () => { const file = new TFile('Templates/template.md'); file.parent = { path: 'Templates' }; plugin.settings.excludeFolders = ['Templates']; await plugin.processFile(file); expect(mockApp.vault.read).not.toHaveBeenCalled(); expect(mockApp.fileManager.processFrontMatter).not.toHaveBeenCalled(); }); test('should handle file read errors gracefully', async () => { const file = new TFile('test.md'); const error = new Error('File read error'); (mockApp.vault.read as jest.Mock).mockRejectedValue(error); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); await plugin.processFile(file); expect(consoleSpy).toHaveBeenCalledWith(`Auto Status Plugin: Error processing file ${file.path}:`, error); consoleSpy.mockRestore(); }); describe('task count properties', () => { const captureFrontmatter = (initial: Record = {}) => { const captured: { value: Record } = { value: {} }; (mockApp.fileManager.processFrontMatter as jest.Mock).mockImplementation( (_file: TFile, callback: (fm: any) => void) => { const frontmatter = { ...initial }; callback(frontmatter); captured.value = frontmatter; return Promise.resolve(); }, ); return captured; }; test('does not write count properties when disabled', async () => { plugin.settings.enableTaskCounts = false; (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_PENDING_TASKS); const captured = captureFrontmatter(); await plugin.processFile(new TFile('test.md')); expect(captured.value).not.toHaveProperty('Pending tasks count'); expect(captured.value).not.toHaveProperty('Total tasks count'); }); test('removes existing count properties when feature is disabled', async () => { plugin.settings.enableTaskCounts = false; (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_PENDING_TASKS); const captured = captureFrontmatter({ 'Pending tasks count': 99, 'Total tasks count': 99, 'other-prop': 'keep', }); await plugin.processFile(new TFile('test.md')); expect(captured.value).toEqual({ 'Pending tasks': true, 'other-prop': 'keep' }); }); test('writes count properties when enabled and tasks exist', async () => { plugin.settings.enableTaskCounts = true; (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_PENDING_TASKS); const captured = captureFrontmatter(); await plugin.processFile(new TFile('test.md')); expect(captured.value).toEqual({ 'Pending tasks': true, 'Pending tasks count': 3, 'Total tasks count': 6, }); }); test('writes zero pending when all tasks are completed', async () => { plugin.settings.enableTaskCounts = true; (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_ALL_COMPLETED_TASKS); const captured = captureFrontmatter(); await plugin.processFile(new TFile('test.md')); expect(captured.value).toEqual({ 'Pending tasks count': 0, 'Total tasks count': 5, }); }); test('deletes count properties when no tasks exist', async () => { plugin.settings.enableTaskCounts = true; (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_NO_TASKS); const captured = captureFrontmatter({ 'Pending tasks count': 5, 'Total tasks count': 10, }); await plugin.processFile(new TFile('test.md')); expect(captured.value).toEqual({}); }); test('respects countRootTasksOnly for counts and boolean', async () => { plugin.settings.enableTaskCounts = true; plugin.settings.countRootTasksOnly = true; (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_NESTED_TASKS); const captured = captureFrontmatter(); await plugin.processFile(new TFile('test.md')); expect(captured.value).toEqual({ 'Pending tasks': true, 'Pending tasks count': 1, 'Total tasks count': 1, }); }); test('uses custom count property names', async () => { plugin.settings.enableTaskCounts = true; plugin.settings.pendingTasksCountProperty = 'pending_n'; plugin.settings.totalTasksCountProperty = 'total_n'; (mockApp.vault.read as jest.Mock).mockResolvedValue(NOTES_WITH_PENDING_TASKS); const captured = captureFrontmatter(); await plugin.processFile(new TFile('test.md')); expect(captured.value).toEqual({ 'Pending tasks': true, pending_n: 3, total_n: 6, }); }); }); test('should handle frontmatter processing errors gracefully', async () => { const file = new TFile('test.md'); const error = new Error('Frontmatter processing error'); (mockApp.vault.read as jest.Mock).mockResolvedValue('- [ ] Task'); (mockApp.fileManager.processFrontMatter as jest.Mock).mockRejectedValue(error); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); await plugin.processFile(file); expect(consoleSpy).toHaveBeenCalledWith(`Auto Status Plugin: Error processing file ${file.path}:`, error); consoleSpy.mockRestore(); }); }); describe('processAllNotes', () => { beforeEach(() => { mockApp.vault.getMarkdownFiles = jest.fn(); plugin.processFile = jest.fn(); }); test('should process all eligible markdown files', async () => { const files = [new TFile('note1.md'), new TFile('note2.md'), new TFile('Templates/template.md')]; files[2].parent = { path: 'Templates' }; plugin.settings.excludeFolders = ['Templates']; (mockApp.vault.getMarkdownFiles as jest.Mock).mockReturnValue(files); await plugin.processAllNotes(); expect(plugin.processFile).toHaveBeenCalledTimes(2); expect(plugin.processFile).toHaveBeenCalledWith(files[0]); expect(plugin.processFile).toHaveBeenCalledWith(files[1]); expect(plugin.processFile).not.toHaveBeenCalledWith(files[2]); }); test('should show completion notice', async () => { const files = [new TFile('note1.md'), new TFile('note2.md')]; (mockApp.vault.getMarkdownFiles as jest.Mock).mockReturnValue(files); await plugin.processAllNotes(); expect(Notice).toHaveBeenCalledWith('Auto Status: Processed 2 notes'); }); }); });