import AutoStatusPlugin from '../src/main'; import { App, TFile } from 'obsidian'; import { TASK_DETECTION_SCENARIOS } from './test-scenarios'; import { NOTES_WITH_MIXED_FORMATS, NOTES_WITH_MALFORMED_CHECKBOXES, EMPTY_NOTE, WHITESPACE_ONLY_NOTE, } from './sample-notes'; describe('Task Detection Logic', () => { let plugin: AutoStatusPlugin; let mockApp: App; beforeEach(() => { mockApp = new App(); plugin = new AutoStatusPlugin(mockApp, {} as any); }); describe('detectTasks', () => { test.each(TASK_DETECTION_SCENARIOS)('$name', scenario => { const result = plugin.detectTasks(scenario.content); expect(result.totalTasks).toBe(scenario.expected.totalTasks); expect(result.completedTasks).toBe(scenario.expected.completedTasks); expect(result.pendingTasks).toBe(scenario.expected.pendingTasks); expect(result.hasPendingTasks).toBe(scenario.expected.hasPendingTasks); }); test('should handle complex markdown with mixed content', () => { const result = plugin.detectTasks(NOTES_WITH_MIXED_FORMATS); expect(result.totalTasks).toBe(6); // 5 actual tasks + 1 in paragraph expect(result.completedTasks).toBe(2); expect(result.pendingTasks).toBe(4); expect(result.hasPendingTasks).toBe(true); }); test('should ignore malformed checkboxes', () => { const result = plugin.detectTasks(NOTES_WITH_MALFORMED_CHECKBOXES); expect(result.totalTasks).toBe(2); expect(result.completedTasks).toBe(1); expect(result.pendingTasks).toBe(1); expect(result.hasPendingTasks).toBe(true); }); test('should handle empty or whitespace-only content', () => { const emptyResult = plugin.detectTasks(EMPTY_NOTE); expect(emptyResult.totalTasks).toBe(0); expect(emptyResult.hasPendingTasks).toBe(false); const whitespaceResult = plugin.detectTasks(WHITESPACE_ONLY_NOTE); expect(whitespaceResult.totalTasks).toBe(0); expect(whitespaceResult.hasPendingTasks).toBe(false); }); describe('root-only counting', () => { const nestedContent = ` - [ ] Main task 1 - [ ] Subtask 1 - [x] Subtask 2 - [ ] Sub-subtask - [x] Main task 2 `; test('counts all tasks by default', () => { const result = plugin.detectTasks(nestedContent); expect(result.totalTasks).toBe(5); expect(result.completedTasks).toBe(2); expect(result.pendingTasks).toBe(3); expect(result.hasPendingTasks).toBe(true); }); test('counts only root tasks when rootOnly is true', () => { const result = plugin.detectTasks(nestedContent, true); expect(result.totalTasks).toBe(2); expect(result.completedTasks).toBe(1); expect(result.pendingTasks).toBe(1); expect(result.hasPendingTasks).toBe(true); }); test('rootOnly reports no pending when only subtasks are pending', () => { const content = ` - [x] Main task - [ ] Subtask pending `; const all = plugin.detectTasks(content); expect(all.pendingTasks).toBe(1); expect(all.hasPendingTasks).toBe(true); const rootOnly = plugin.detectTasks(content, true); expect(rootOnly.totalTasks).toBe(1); expect(rootOnly.pendingTasks).toBe(0); expect(rootOnly.hasPendingTasks).toBe(false); }); test('rootOnly treats tab-indented tasks as nested', () => { const content = ` - [ ] Root task \t- [ ] Tab-indented subtask `; const result = plugin.detectTasks(content, true); expect(result.totalTasks).toBe(1); expect(result.pendingTasks).toBe(1); }); test('task under non-task bullet is still a root task', () => { const content = ` - **Action items**: - [ ] My tasks `; const all = plugin.detectTasks(content); expect(all.totalTasks).toBe(1); const rootOnly = plugin.detectTasks(content, true); expect(rootOnly.totalTasks).toBe(1); expect(rootOnly.pendingTasks).toBe(1); }); test('classifies each task by its nearest list parent', () => { const content = ` - [ ] A - Notes section - [ ] N - [ ] N.1 - [x] M - [ ] B `; const rootOnly = plugin.detectTasks(content, true); // Roots: A, N (parent is non-task), M (parent is non-task), B expect(rootOnly.totalTasks).toBe(4); expect(rootOnly.completedTasks).toBe(1); expect(rootOnly.pendingTasks).toBe(3); const all = plugin.detectTasks(content); // All: A, N, N.1, M, B expect(all.totalTasks).toBe(5); expect(all.completedTasks).toBe(1); expect(all.pendingTasks).toBe(4); }); test('deeply nested non-task bullets still promote their direct children to root', () => { const content = ` - Project - Subproject - [ ] Real task - [ ] Detail `; const rootOnly = plugin.detectTasks(content, true); expect(rootOnly.totalTasks).toBe(1); expect(rootOnly.pendingTasks).toBe(1); }); test('less-indented task after a deeper task is correctly re-rooted', () => { const content = ` - [ ] A - [ ] A.1 - [ ] A.1.1 - [ ] B `; const rootOnly = plugin.detectTasks(content, true); expect(rootOnly.totalTasks).toBe(2); expect(rootOnly.pendingTasks).toBe(2); }); test('sibling tasks at the same indent are all root when parent is non-task', () => { const content = ` Heading line - Outline: - [ ] First - [ ] Second - [x] Third `; const rootOnly = plugin.detectTasks(content, true); expect(rootOnly.totalTasks).toBe(3); expect(rootOnly.completedTasks).toBe(1); expect(rootOnly.pendingTasks).toBe(2); }); test('non-task bullet between two tasks does not merge them', () => { const content = ` - [ ] A - Section B - [ ] B.1 - [ ] C `; const rootOnly = plugin.detectTasks(content, true); // A is root, B.1 root (parent is non-task "Section B"), C is root expect(rootOnly.totalTasks).toBe(3); expect(rootOnly.pendingTasks).toBe(3); }); test('task with only subtasks pending is completed at root level', () => { const content = ` - [x] Parent done - [ ] Child pending `; const rootOnly = plugin.detectTasks(content, true); expect(rootOnly.totalTasks).toBe(1); expect(rootOnly.completedTasks).toBe(1); expect(rootOnly.pendingTasks).toBe(0); expect(rootOnly.hasPendingTasks).toBe(false); }); }); }); });