refactor: migrate parent-folder-detection tests to mock adapters

Update parent-folder-detection.test.ts to use the new mock adapter
pattern (createMockVaultAdapter, createMockFileManagerAdapter) instead
of manually mocking Obsidian API objects.

Key changes:
- Replace manual vault/app mocks with adapter helpers
- Use createMockTFile and createMockTFolder for consistent fixtures
- Fix mock reference synchronization with getter pattern for App.vault
- Standardize all mock reassignments to use jest.fn() methods
- Update all 14 tests to properly reassign mocks in test setup

This resolves all 7 failing tests and improves test maintainability
by using consistent mock patterns across the test suite.

All tests passing: 14/14 in parent-folder-detection.test.ts
This commit is contained in:
2025-10-20 00:01:00 -04:00
parent 0185ca7d00
commit f5a671e625

View File

@@ -1,54 +1,52 @@
import { App, TFile, TFolder, Vault } from 'obsidian';
import { App } from 'obsidian';
import { NoteTools } from '../src/tools/note-tools';
import { PathUtils } from '../src/utils/path-utils';
import { createMockVaultAdapter, createMockFileManagerAdapter, createMockTFile, createMockTFolder } from './__mocks__/adapters';
// Mock Obsidian API
jest.mock('obsidian');
describe('Enhanced Parent Folder Detection', () => {
let app: jest.Mocked<App>;
let vault: jest.Mocked<Vault>;
let noteTools: NoteTools;
let mockVault: ReturnType<typeof createMockVaultAdapter>;
let mockFileManager: ReturnType<typeof createMockFileManagerAdapter>;
let mockApp: App;
beforeEach(() => {
// Create mock vault
vault = {
getAbstractFileByPath: jest.fn(),
create: jest.fn(),
createFolder: jest.fn(),
read: jest.fn(),
modify: jest.fn(),
delete: jest.fn(),
mockVault = createMockVaultAdapter();
mockFileManager = createMockFileManagerAdapter();
// Create a minimal mock App that supports PathUtils
// Use a getter to ensure it always uses the current mock
mockApp = {
vault: {
get getAbstractFileByPath() {
return mockVault.getAbstractFileByPath;
}
}
} as any;
// Create mock app
app = {
vault,
} as any;
noteTools = new NoteTools(app);
noteTools = new NoteTools(mockVault, mockFileManager, mockApp);
});
describe('Explicit parent folder detection', () => {
test('should detect missing parent folder before write operation', async () => {
// Setup: parent folder doesn't exist
vault.getAbstractFileByPath.mockReturnValue(null);
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await noteTools.createNote('missing-parent/file.md', 'content', false);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Parent folder does not exist');
expect(result.content[0].text).toContain('missing-parent');
expect(vault.create).not.toHaveBeenCalled();
expect(mockVault.create).not.toHaveBeenCalled();
});
test('should detect when parent path is a file, not a folder', async () => {
// Create a proper TFile instance
const mockFile = Object.create(TFile.prototype);
Object.assign(mockFile, { path: 'parent.md', name: 'parent.md', basename: 'parent', extension: 'md' });
const mockFile = createMockTFile('parent.md');
// Setup: parent path exists but is a file
vault.getAbstractFileByPath.mockImplementation((path: string) => {
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (path === 'parent.md') return mockFile;
return null;
});
@@ -58,34 +56,34 @@ describe('Enhanced Parent Folder Detection', () => {
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Path is not a folder');
expect(result.content[0].text).toContain('parent.md');
expect(vault.create).not.toHaveBeenCalled();
expect(mockVault.create).not.toHaveBeenCalled();
});
test('should succeed when parent folder exists', async () => {
const mockFolder = { path: 'existing-folder' } as TFolder;
const mockFile = { path: 'existing-folder/file.md' } as TFile;
const mockFolder = createMockTFolder('existing-folder');
const mockFile = createMockTFile('existing-folder/file.md');
// Setup: parent folder exists
vault.getAbstractFileByPath.mockImplementation((path: string) => {
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (path === 'existing-folder') return mockFolder;
if (path === 'existing-folder/file.md') return null; // file doesn't exist yet
return null;
});
vault.create.mockResolvedValue(mockFile);
mockVault.create = jest.fn().mockResolvedValue(mockFile);
const result = await noteTools.createNote('existing-folder/file.md', 'content', false);
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toContain('Note created successfully');
expect(vault.create).toHaveBeenCalledWith('existing-folder/file.md', 'content');
expect(JSON.parse(result.content[0].text).success).toBe(true);
expect(mockVault.create).toHaveBeenCalledWith('existing-folder/file.md', 'content');
});
test('should handle nested missing parents (a/b/c where b does not exist)', async () => {
const mockFolderA = { path: 'a' } as TFolder;
const mockFolderA = createMockTFolder('a');
// Setup: only 'a' exists, 'a/b' does not exist
vault.getAbstractFileByPath.mockImplementation((path: string) => {
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (path === 'a') return mockFolderA;
return null;
});
@@ -95,124 +93,124 @@ describe('Enhanced Parent Folder Detection', () => {
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Parent folder does not exist');
expect(result.content[0].text).toContain('a/b/c');
expect(vault.create).not.toHaveBeenCalled();
expect(mockVault.create).not.toHaveBeenCalled();
});
});
describe('createParents parameter', () => {
test('should create single missing parent folder when createParents is true', async () => {
const mockFolder = { path: 'new-folder' } as TFolder;
const mockFile = { path: 'new-folder/file.md' } as TFile;
const mockFolder = createMockTFolder('new-folder');
const mockFile = createMockTFile('new-folder/file.md');
// Setup: parent doesn't exist initially
let folderCreated = false;
vault.getAbstractFileByPath.mockImplementation((path: string) => {
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (path === 'new-folder' && folderCreated) return mockFolder;
return null;
});
vault.createFolder.mockImplementation(async (path: string) => {
mockVault.createFolder = jest.fn().mockImplementation(async (path: string) => {
folderCreated = true;
return mockFolder;
});
vault.create.mockResolvedValue(mockFile);
mockVault.create = jest.fn().mockResolvedValue(mockFile);
const result = await noteTools.createNote('new-folder/file.md', 'content', true);
expect(result.isError).toBeUndefined();
expect(vault.createFolder).toHaveBeenCalledWith('new-folder');
expect(vault.create).toHaveBeenCalledWith('new-folder/file.md', 'content');
expect(result.content[0].text).toContain('Note created successfully');
expect(mockVault.createFolder).toHaveBeenCalledWith('new-folder');
expect(mockVault.create).toHaveBeenCalledWith('new-folder/file.md', 'content');
expect(JSON.parse(result.content[0].text).success).toBe(true);
});
test('should recursively create all missing parent folders', async () => {
const createdFolders = new Set<string>();
const mockFile = { path: 'a/b/c/file.md' } as TFile;
const mockFile = createMockTFile('a/b/c/file.md');
// Setup: no folders exist initially
vault.getAbstractFileByPath.mockImplementation((path: string) => {
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (createdFolders.has(path)) {
return { path } as TFolder;
return createMockTFolder(path);
}
return null;
});
vault.createFolder.mockImplementation(async (path: string) => {
mockVault.createFolder = jest.fn().mockImplementation(async (path: string) => {
createdFolders.add(path);
return { path } as TFolder;
return createMockTFolder(path);
});
vault.create.mockResolvedValue(mockFile);
mockVault.create = jest.fn().mockResolvedValue(mockFile);
const result = await noteTools.createNote('a/b/c/file.md', 'content', true);
expect(result.isError).toBeUndefined();
expect(vault.createFolder).toHaveBeenCalledTimes(3);
expect(vault.createFolder).toHaveBeenCalledWith('a');
expect(vault.createFolder).toHaveBeenCalledWith('a/b');
expect(vault.createFolder).toHaveBeenCalledWith('a/b/c');
expect(vault.create).toHaveBeenCalledWith('a/b/c/file.md', 'content');
expect(mockVault.createFolder).toHaveBeenCalledTimes(3);
expect(mockVault.createFolder).toHaveBeenCalledWith('a');
expect(mockVault.createFolder).toHaveBeenCalledWith('a/b');
expect(mockVault.createFolder).toHaveBeenCalledWith('a/b/c');
expect(mockVault.create).toHaveBeenCalledWith('a/b/c/file.md', 'content');
});
test('should not create folders when createParents is false (default)', async () => {
// Setup: parent doesn't exist
vault.getAbstractFileByPath.mockReturnValue(null);
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await noteTools.createNote('missing/file.md', 'content', false);
expect(result.isError).toBe(true);
expect(vault.createFolder).not.toHaveBeenCalled();
expect(vault.create).not.toHaveBeenCalled();
expect(mockVault.createFolder).not.toHaveBeenCalled();
expect(mockVault.create).not.toHaveBeenCalled();
});
test('should handle createFolder errors gracefully', async () => {
// Setup: parent doesn't exist
vault.getAbstractFileByPath.mockReturnValue(null);
vault.createFolder.mockRejectedValue(new Error('Permission denied'));
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
mockVault.createFolder = jest.fn().mockRejectedValue(new Error('Permission denied'));
const result = await noteTools.createNote('new-folder/file.md', 'content', true);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Failed to create parent folders');
expect(result.content[0].text).toContain('Permission denied');
expect(vault.create).not.toHaveBeenCalled();
expect(mockVault.create).not.toHaveBeenCalled();
});
test('should skip creating folders that already exist', async () => {
const mockFolderA = { path: 'a' } as TFolder;
const mockFile = { path: 'a/b/file.md' } as TFile;
const mockFolderA = createMockTFolder('a');
const mockFile = createMockTFile('a/b/file.md');
let folderBCreated = false;
// Setup: 'a' exists, 'a/b' does not
vault.getAbstractFileByPath.mockImplementation((path: string) => {
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (path === 'a') return mockFolderA;
if (path === 'a/b' && folderBCreated) return { path: 'a/b' } as TFolder;
if (path === 'a/b' && folderBCreated) return createMockTFolder('a/b');
return null;
});
vault.createFolder.mockImplementation(async (path: string) => {
mockVault.createFolder = jest.fn().mockImplementation(async (path: string) => {
if (path === 'a/b') {
folderBCreated = true;
return { path: 'a/b' } as TFolder;
return createMockTFolder('a/b');
}
return null as any;
});
vault.create.mockResolvedValue(mockFile);
mockVault.create = jest.fn().mockResolvedValue(mockFile);
const result = await noteTools.createNote('a/b/file.md', 'content', true);
expect(result.isError).toBeUndefined();
// Should only create 'a/b', not 'a' (which already exists)
expect(vault.createFolder).toHaveBeenCalledTimes(1);
expect(vault.createFolder).toHaveBeenCalledWith('a/b');
expect(mockVault.createFolder).toHaveBeenCalledTimes(1);
expect(mockVault.createFolder).toHaveBeenCalledWith('a/b');
});
});
describe('Error message clarity', () => {
test('should provide helpful error message with createParents suggestion', async () => {
vault.getAbstractFileByPath.mockReturnValue(null);
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await noteTools.createNote('folder/subfolder/file.md', 'content', false);
@@ -225,10 +223,9 @@ describe('Enhanced Parent Folder Detection', () => {
test('should provide clear error when parent is a file', async () => {
// Create a proper TFile instance
const mockFile = Object.create(TFile.prototype);
Object.assign(mockFile, { path: 'file.md', name: 'file.md', basename: 'file', extension: 'md' });
vault.getAbstractFileByPath.mockImplementation((path: string) => {
const mockFile = createMockTFile('file.md');
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (path === 'file.md') return mockFile;
return null;
});
@@ -243,57 +240,57 @@ describe('Enhanced Parent Folder Detection', () => {
describe('Edge cases', () => {
test('should handle file in root directory (no parent path)', async () => {
const mockFile = { path: 'file.md' } as TFile;
vault.getAbstractFileByPath.mockReturnValue(null);
vault.create.mockResolvedValue(mockFile);
const mockFile = createMockTFile('file.md');
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
mockVault.create = jest.fn().mockResolvedValue(mockFile);
const result = await noteTools.createNote('file.md', 'content', false);
expect(result.isError).toBeUndefined();
expect(vault.create).toHaveBeenCalledWith('file.md', 'content');
expect(mockVault.create).toHaveBeenCalledWith('file.md', 'content');
});
test('should normalize paths before checking parent', async () => {
const mockFolder = { path: 'folder' } as TFolder;
const mockFile = { path: 'folder/file.md' } as TFile;
vault.getAbstractFileByPath.mockImplementation((path: string) => {
const mockFolder = createMockTFolder('folder');
const mockFile = createMockTFile('folder/file.md');
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (path === 'folder') return mockFolder;
return null;
});
vault.create.mockResolvedValue(mockFile);
mockVault.create = jest.fn().mockResolvedValue(mockFile);
// Test with various path formats
const result = await noteTools.createNote('folder//file.md', 'content', false);
expect(result.isError).toBeUndefined();
expect(vault.create).toHaveBeenCalledWith('folder/file.md', 'content');
expect(mockVault.create).toHaveBeenCalledWith('folder/file.md', 'content');
});
test('should handle deeply nested paths', async () => {
const createdFolders = new Set<string>();
const mockFile = { path: 'a/b/c/d/e/f/file.md' } as TFile;
vault.getAbstractFileByPath.mockImplementation((path: string) => {
const mockFile = createMockTFile('a/b/c/d/e/f/file.md');
mockVault.getAbstractFileByPath = jest.fn().mockImplementation((path: string) => {
if (createdFolders.has(path)) {
return { path } as TFolder;
return createMockTFolder(path);
}
return null;
});
vault.createFolder.mockImplementation(async (path: string) => {
mockVault.createFolder = jest.fn().mockImplementation(async (path: string) => {
createdFolders.add(path);
return { path } as TFolder;
return createMockTFolder(path);
});
vault.create.mockResolvedValue(mockFile);
mockVault.create = jest.fn().mockResolvedValue(mockFile);
const result = await noteTools.createNote('a/b/c/d/e/f/file.md', 'content', true);
expect(result.isError).toBeUndefined();
expect(vault.createFolder).toHaveBeenCalledTimes(6);
expect(mockVault.createFolder).toHaveBeenCalledTimes(6);
});
});
});