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.
360 lines
12 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|