feat: add tests for searches with the tag operator

This commit is contained in:
Jamie Cockrill 2025-08-16 15:46:55 +01:00
parent 82423fcb6a
commit 0cc683431d
2 changed files with 134 additions and 0 deletions

View file

@ -91,4 +91,35 @@ export class Setting {
export function normalizePath(path: string): string {
// Simple normalization for testing
return path.replace(/\\/g, '/');
}
// Minimal implementation of Obsidian's getAllTags utility for tests
// Accepts a metadata cache-like object and returns a flat list of tags with leading '#'
export function getAllTags(cache: any): string[] {
if (!cache) return [];
const out = new Set<string>();
// From cache.tags: [{ tag: '#foo' }, { tag: '#foo/bar' }]
if (Array.isArray(cache.tags)) {
for (const t of cache.tags) {
const raw = typeof t === 'string' ? t : t?.tag;
if (!raw) continue;
const norm = raw.startsWith('#') ? raw : `#${raw}`;
out.add(norm);
}
}
// From frontmatter.tags: 'foo', ['foo', 'bar'], or ['#foo/bar']
const fmTags = cache.frontmatter?.tags;
if (fmTags) {
const list = Array.isArray(fmTags) ? fmTags : [fmTags];
for (const rawItem of list) {
if (!rawItem) continue;
const raw = String(rawItem).trim();
const norm = raw.startsWith('#') ? raw : `#${raw}`;
out.add(norm);
}
}
return Array.from(out);
}

View file

@ -0,0 +1,103 @@
import { ObsidianAPI } from '../src/utils/obsidian-api';
import { App, TFile } from 'obsidian';
// Helper to create a mock TFile with required fields
function makeFile(path: string): TFile {
const f = Object.create(TFile.prototype);
Object.assign(f, {
path,
name: path.split('/').pop()!,
basename: path.split('/').pop()!.replace(/\.[^/.]+$/, ''),
extension: path.split('.').pop() || 'md',
stat: { size: 123, mtime: Date.now() }
});
return f as TFile;
}
// Create a minimal mock app for tag-based search
function makeMockApp(filesWithCaches: Array<{ file: TFile; cache: any }>): App {
const files = filesWithCaches.map((x) => x.file);
const cacheMap = new Map<string, any>(filesWithCaches.map(({ file, cache }) => [file.path, cache]));
const mockApp: any = {
vault: {
getFiles: jest.fn(() => files),
},
metadataCache: {
getFileCache: jest.fn((file: TFile) => cacheMap.get(file.path)),
},
};
return mockApp as App;
}
describe('search tag: operator', () => {
test('matches files with exact tag and hierarchical children (no leading #)', async () => {
const f1 = makeFile('notes/alpha.md');
const f2 = makeFile('notes/beta.md');
const f3 = makeFile('notes/gamma.md');
const app = makeMockApp([
{
file: f1,
cache: {
// content tags form
tags: [{ tag: '#foo' }],
},
},
{
file: f2,
cache: {
// hierarchical child should match tag:foo
tags: [{ tag: '#foo/bar' }],
},
},
{
file: f3,
cache: {
// different tag
tags: [{ tag: '#bar' }],
},
},
]);
const api = new ObsidianAPI(app);
const res = await api.searchPaginated('tag:foo');
const paths = res.results.map((r) => r.path).sort();
expect(paths).toEqual(['notes/alpha.md', 'notes/beta.md']);
});
test('matches with leading # and frontmatter tags', async () => {
const f1 = makeFile('notes/frontmatter.md');
const f2 = makeFile('notes/other.md');
const app = makeMockApp([
{
file: f1,
cache: {
frontmatter: {
tags: ['foo', 'x/y'],
},
},
},
{
file: f2,
cache: {
frontmatter: {
tags: ['bar'],
},
},
},
]);
const api = new ObsidianAPI(app);
const res1 = await api.searchPaginated('tag:#foo');
expect(res1.results.map((r) => r.path)).toEqual(['notes/frontmatter.md']);
const res2 = await api.searchPaginated('tag:x');
expect(res2.results.map((r) => r.path)).toEqual(['notes/frontmatter.md']);
});
});