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/file-processing.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

360 lines
12 KiB
TypeScript

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<string, unknown> = {}) => {
const captured: { value: Record<string, unknown> } = { 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');
});
});
});