This repository has been archived on 2026-05-02. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
obsidian-plugin-auto-task-s.../test/task-detection.test.ts
Felipe M. d113c1c027
All checks were successful
CI / build (push) Successful in 16s
Add optional task count properties with structural root-task detection
Introduces opt-in pending/total task count frontmatter fields and a
"count root tasks only" mode. Root tasks are detected structurally by
walking the list tree, so tasks nested under non-task bullets (e.g.
"- **Action items**:") are correctly classified as root.

Also updates the manifest author metadata.
2026-04-14 09:50:45 +02:00

207 lines
6.5 KiB
TypeScript

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