All checks were successful
CI / build (push) Successful in 16s
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.
207 lines
6.5 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|
|
});
|