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:
539
tests/waypoint-utils.test.ts
Normal file
539
tests/waypoint-utils.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user