test: add comprehensive waypoint-utils tests

- Test extractWaypointBlock() with valid/invalid waypoints, unclosed blocks, multiple links
- Test hasWaypointMarker() with all marker combinations
- Test isFolderNote() with basename match, waypoint marker, both, neither, file read errors
- Test wouldAffectWaypoint() detecting removal, content changes, acceptable moves
- Test getParentFolderPath() and getBasename() helper methods
- Achieve 100% coverage on waypoint-utils.ts (52 tests)
This commit is contained in:
2025-10-20 08:08:25 -04:00
parent 3360790149
commit f9634a7b2a

View File

@@ -0,0 +1,539 @@
import { WaypointUtils, WaypointBlock, FolderNoteInfo } from '../src/utils/waypoint-utils';
import { createMockVaultAdapter, createMockTFile, createMockTFolder } from './__mocks__/adapters';
import { IVaultAdapter } from '../src/adapters/interfaces';
import { TFile } from 'obsidian';
describe('WaypointUtils', () => {
describe('extractWaypointBlock()', () => {
test('extracts valid waypoint with links', () => {
const content = `# Folder Index
%% Begin Waypoint %%
- [[Note 1]]
- [[Note 2]]
- [[Subfolder/Note 3]]
%% End Waypoint %%
More content`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.waypointRange).toEqual({ start: 2, end: 6 });
expect(result.links).toEqual(['Note 1', 'Note 2', 'Subfolder/Note 3']);
expect(result.rawContent).toBe('- [[Note 1]]\n- [[Note 2]]\n- [[Subfolder/Note 3]]');
});
test('extracts waypoint with no links', () => {
const content = `%% Begin Waypoint %%
Empty waypoint
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.waypointRange).toEqual({ start: 1, end: 3 });
expect(result.links).toEqual([]);
expect(result.rawContent).toBe('Empty waypoint');
});
test('extracts waypoint with links with aliases', () => {
const content = `%% Begin Waypoint %%
- [[Note|Alias]]
- [[Another Note#Section|Custom Text]]
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.links).toEqual(['Note|Alias', 'Another Note#Section|Custom Text']);
});
test('extracts empty waypoint', () => {
const content = `%% Begin Waypoint %%
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.waypointRange).toEqual({ start: 1, end: 2 });
expect(result.links).toEqual([]);
expect(result.rawContent).toBe('');
});
test('returns false for content without waypoint', () => {
const content = `# Regular Note
Just some content
- No waypoint here`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(false);
expect(result.waypointRange).toBeUndefined();
expect(result.links).toBeUndefined();
expect(result.rawContent).toBeUndefined();
});
test('returns false for unclosed waypoint', () => {
const content = `%% Begin Waypoint %%
- [[Note 1]]
- [[Note 2]]
Missing end marker`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(false);
});
test('handles waypoint with multiple links on same line', () => {
const content = `%% Begin Waypoint %%
- [[Note 1]], [[Note 2]], [[Note 3]]
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.links).toEqual(['Note 1', 'Note 2', 'Note 3']);
});
test('handles waypoint at start of file', () => {
const content = `%% Begin Waypoint %%
- [[Link]]
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.waypointRange).toEqual({ start: 1, end: 3 });
});
test('handles waypoint at end of file', () => {
const content = `Some content
%% Begin Waypoint %%
- [[Link]]
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.waypointRange).toEqual({ start: 2, end: 4 });
});
test('only extracts first waypoint if multiple exist', () => {
const content = `%% Begin Waypoint %%
- [[First]]
%% End Waypoint %%
%% Begin Waypoint %%
- [[Second]]
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(true);
expect(result.links).toEqual(['First']);
});
test('handles content with only start marker', () => {
const content = `%% Begin Waypoint %%
Content without end`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(false);
});
test('handles content with only end marker', () => {
const content = `Content without start
%% End Waypoint %%`;
const result = WaypointUtils.extractWaypointBlock(content);
expect(result.hasWaypoint).toBe(false);
});
test('handles empty string', () => {
const result = WaypointUtils.extractWaypointBlock('');
expect(result.hasWaypoint).toBe(false);
});
});
describe('hasWaypointMarker()', () => {
test('returns true when both markers present', () => {
const content = `%% Begin Waypoint %%
Content
%% End Waypoint %%`;
expect(WaypointUtils.hasWaypointMarker(content)).toBe(true);
});
test('returns false when only start marker present', () => {
const content = `%% Begin Waypoint %%
Content without end`;
expect(WaypointUtils.hasWaypointMarker(content)).toBe(false);
});
test('returns false when only end marker present', () => {
const content = `Content without start
%% End Waypoint %%`;
expect(WaypointUtils.hasWaypointMarker(content)).toBe(false);
});
test('returns false when no markers present', () => {
const content = 'Regular content with no markers';
expect(WaypointUtils.hasWaypointMarker(content)).toBe(false);
});
test('returns true even if markers are reversed', () => {
const content = `%% End Waypoint %%
%% Begin Waypoint %%`;
// This tests the regex logic - both patterns exist somewhere
expect(WaypointUtils.hasWaypointMarker(content)).toBe(true);
});
test('handles empty string', () => {
expect(WaypointUtils.hasWaypointMarker('')).toBe(false);
});
});
describe('isFolderNote()', () => {
let mockVault: IVaultAdapter;
beforeEach(() => {
mockVault = createMockVaultAdapter();
});
test('detects folder note by basename match', async () => {
const folder = createMockTFolder('Projects');
const file = createMockTFile('Projects/Projects.md');
file.basename = 'Projects';
file.parent = folder;
(mockVault.read as jest.Mock).mockResolvedValue('Regular content without waypoint');
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(true);
expect(result.reason).toBe('basename_match');
expect(result.folderPath).toBe('Projects');
});
test('detects folder note by waypoint marker', async () => {
const folder = createMockTFolder('Projects');
const file = createMockTFile('Projects/Index.md');
file.basename = 'Index';
file.parent = folder;
const content = `# Project Index
%% Begin Waypoint %%
- [[Project 1]]
%% End Waypoint %%`;
(mockVault.read as jest.Mock).mockResolvedValue(content);
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(true);
expect(result.reason).toBe('waypoint_marker');
expect(result.folderPath).toBe('Projects');
});
test('detects folder note by both basename and waypoint', async () => {
const folder = createMockTFolder('Projects');
const file = createMockTFile('Projects/Projects.md');
file.basename = 'Projects';
file.parent = folder;
const content = `%% Begin Waypoint %%
- [[Project 1]]
%% End Waypoint %%`;
(mockVault.read as jest.Mock).mockResolvedValue(content);
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(true);
expect(result.reason).toBe('both');
expect(result.folderPath).toBe('Projects');
});
test('detects non-folder note', async () => {
const folder = createMockTFolder('Projects');
const file = createMockTFile('Projects/Regular Note.md');
file.basename = 'Regular Note';
file.parent = folder;
(mockVault.read as jest.Mock).mockResolvedValue('Regular content without waypoint');
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(false);
expect(result.reason).toBe('none');
expect(result.folderPath).toBe('Projects');
});
test('handles file in root directory', async () => {
const file = createMockTFile('RootNote.md');
file.basename = 'RootNote';
file.parent = null;
(mockVault.read as jest.Mock).mockResolvedValue('Content');
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(false);
expect(result.reason).toBe('none');
expect(result.folderPath).toBeUndefined();
});
test('handles file read error - basename match still works', async () => {
const folder = createMockTFolder('Projects');
const file = createMockTFile('Projects/Projects.md');
file.basename = 'Projects';
file.parent = folder;
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
(mockVault.read as jest.Mock).mockRejectedValue(new Error('Read failed'));
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(true);
expect(result.reason).toBe('basename_match');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to read file Projects/Projects.md'),
expect.any(Error)
);
consoleErrorSpy.mockRestore();
});
test('handles file read error - waypoint cannot be detected', async () => {
const folder = createMockTFolder('Projects');
const file = createMockTFile('Projects/Index.md');
file.basename = 'Index';
file.parent = folder;
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
(mockVault.read as jest.Mock).mockRejectedValue(new Error('Read failed'));
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(false);
expect(result.reason).toBe('none');
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
test('handles unclosed waypoint as no waypoint', async () => {
const folder = createMockTFolder('Projects');
const file = createMockTFile('Projects/Index.md');
file.basename = 'Index';
file.parent = folder;
const content = `%% Begin Waypoint %%
Missing end marker`;
(mockVault.read as jest.Mock).mockResolvedValue(content);
const result = await WaypointUtils.isFolderNote(mockVault, file);
expect(result.isFolderNote).toBe(false);
expect(result.reason).toBe('none');
});
});
describe('wouldAffectWaypoint()', () => {
test('returns false when no waypoint in original content', () => {
const content = 'Regular content';
const newContent = 'Updated content';
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(false);
expect(result.waypointRange).toBeUndefined();
});
test('detects waypoint removal', () => {
const content = `%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%`;
const newContent = 'Waypoint removed';
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(true);
expect(result.waypointRange).toEqual({ start: 1, end: 3 });
});
test('detects waypoint content change', () => {
const content = `%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%`;
const newContent = `%% Begin Waypoint %%
- [[Note 2]]
%% End Waypoint %%`;
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(true);
expect(result.waypointRange).toEqual({ start: 1, end: 3 });
});
test('allows waypoint to be moved (content unchanged)', () => {
const content = `%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%`;
const newContent = `# Added heading
%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%`;
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(false);
});
test('detects waypoint content change with added link', () => {
const content = `%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%`;
const newContent = `%% Begin Waypoint %%
- [[Note 1]]
- [[Note 2]]
%% End Waypoint %%`;
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(true);
});
test('allows waypoint when only surrounding content changes', () => {
const content = `# Heading
%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%
Footer`;
const newContent = `# Different Heading
%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%
Different Footer`;
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(false);
});
test('detects waypoint content change with whitespace differences', () => {
const content = `%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%`;
const newContent = `%% Begin Waypoint %%
- [[Note 1]]
%% End Waypoint %%`;
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(true);
});
test('returns false when waypoint stays identical', () => {
const content = `# Heading
%% Begin Waypoint %%
- [[Note 1]]
- [[Note 2]]
%% End Waypoint %%
Content`;
const newContent = content;
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(false);
});
test('handles empty waypoint blocks', () => {
const content = `%% Begin Waypoint %%
%% End Waypoint %%`;
const newContent = `%% Begin Waypoint %%
%% End Waypoint %%`;
const result = WaypointUtils.wouldAffectWaypoint(content, newContent);
expect(result.affected).toBe(false);
});
});
describe('getParentFolderPath()', () => {
test('extracts parent folder from nested path', () => {
expect(WaypointUtils.getParentFolderPath('folder/subfolder/file.md')).toBe('folder/subfolder');
});
test('extracts parent folder from single level path', () => {
expect(WaypointUtils.getParentFolderPath('folder/file.md')).toBe('folder');
});
test('returns null for root level file', () => {
expect(WaypointUtils.getParentFolderPath('file.md')).toBe(null);
});
test('handles path with multiple slashes', () => {
expect(WaypointUtils.getParentFolderPath('a/b/c/d/file.md')).toBe('a/b/c/d');
});
test('handles empty string', () => {
expect(WaypointUtils.getParentFolderPath('')).toBe(null);
});
test('handles path ending with slash', () => {
expect(WaypointUtils.getParentFolderPath('folder/subfolder/')).toBe('folder/subfolder');
});
});
describe('getBasename()', () => {
test('extracts basename from file with extension', () => {
expect(WaypointUtils.getBasename('file.md')).toBe('file');
});
test('extracts basename from nested path', () => {
expect(WaypointUtils.getBasename('folder/subfolder/file.md')).toBe('file');
});
test('handles file with multiple dots', () => {
expect(WaypointUtils.getBasename('file.test.md')).toBe('file.test');
});
test('handles file without extension', () => {
expect(WaypointUtils.getBasename('folder/file')).toBe('file');
});
test('returns entire name when no extension or path', () => {
expect(WaypointUtils.getBasename('filename')).toBe('filename');
});
test('handles empty string', () => {
expect(WaypointUtils.getBasename('')).toBe('');
});
test('handles path with only extension', () => {
expect(WaypointUtils.getBasename('.md')).toBe('');
});
test('handles deeply nested path', () => {
expect(WaypointUtils.getBasename('a/b/c/d/e/file.md')).toBe('file');
});
test('handles hidden file (starts with dot)', () => {
expect(WaypointUtils.getBasename('.gitignore')).toBe('');
});
test('handles hidden file with extension', () => {
expect(WaypointUtils.getBasename('.config.json')).toBe('.config');
});
});
});