11import { describe , it , expect , beforeEach , vi } from 'vitest' ;
22import { FileIndex } from './FileIndex' ;
3+ import { normalizeForSearch } from './utils' ;
34import type { App , TFile , Vault , MetadataCache , Workspace } from 'obsidian' ;
45
56// Test-specific subclass that allows resetting the singleton
67class TestableFileIndex extends FileIndex {
78 static reset ( ) : void {
8- if ( TestableFileIndex . instance ) {
9+ if ( FileIndex . instance ) {
910 // Clear any pending timeouts
10- if ( ( TestableFileIndex . instance as any ) . reindexTimeout !== null ) {
11- clearTimeout ( ( TestableFileIndex . instance as any ) . reindexTimeout ) ;
11+ if ( ( FileIndex . instance as any ) . reindexTimeout !== null ) {
12+ clearTimeout ( ( FileIndex . instance as any ) . reindexTimeout ) ;
1213 }
13- if ( ( TestableFileIndex . instance as any ) . fuseUpdateTimeout !== null ) {
14- clearTimeout ( ( TestableFileIndex . instance as any ) . fuseUpdateTimeout ) ;
14+ if ( ( FileIndex . instance as any ) . fuseUpdateTimeout !== null ) {
15+ clearTimeout ( ( FileIndex . instance as any ) . fuseUpdateTimeout ) ;
1516 }
16- // Clear the instance
17- TestableFileIndex . instance = null as any ;
1817 }
18+ // Clear the instance
19+ FileIndex . instance = null as any ;
1920 }
2021}
2122
@@ -49,6 +50,32 @@ const createMockApp = (): App => {
4950 } as App ;
5051} ;
5152
53+ const createMockIndexedFile = ( overrides : {
54+ path : string ;
55+ basename : string ;
56+ aliases ?: string [ ] ;
57+ headings ?: string [ ] ;
58+ blockIds ?: string [ ] ;
59+ tags ?: string [ ] ;
60+ modified ?: number ;
61+ folder ?: string ;
62+ } ) => {
63+ const aliases = overrides . aliases ?? [ ] ;
64+ return {
65+ path : overrides . path ,
66+ pathNormalized : normalizeForSearch ( overrides . path ) ,
67+ basename : overrides . basename ,
68+ basenameNormalized : normalizeForSearch ( overrides . basename ) ,
69+ aliases,
70+ aliasesNormalized : aliases . map ( ( alias ) => normalizeForSearch ( alias ) ) ,
71+ headings : overrides . headings ?? [ ] ,
72+ blockIds : overrides . blockIds ?? [ ] ,
73+ tags : overrides . tags ?? [ ] ,
74+ modified : overrides . modified ?? Date . now ( ) ,
75+ folder : overrides . folder ?? ''
76+ } ;
77+ } ;
78+
5279describe ( 'FileIndex' , ( ) => {
5380 let mockApp : App ;
5481 let fileIndex : FileIndex ;
@@ -66,16 +93,11 @@ describe('FileIndex', () => {
6693
6794 describe ( 'scoring system' , ( ) => {
6895 it ( 'should boost same-folder files' , ( ) => {
69- const mockFile = {
96+ const mockFile = createMockIndexedFile ( {
7097 path : 'folder/test.md' ,
7198 basename : 'test' ,
72- aliases : [ ] ,
73- headings : [ ] ,
74- blockIds : [ ] ,
75- tags : [ ] ,
76- modified : Date . now ( ) ,
7799 folder : 'folder'
78- } ;
100+ } ) ;
79101
80102 const context = { currentFolder : 'folder' } ;
81103 const score = ( fileIndex as any ) . calculateScore ( mockFile , 'test' , context , 0.5 ) ;
@@ -84,33 +106,23 @@ describe('FileIndex', () => {
84106 } ) ;
85107
86108 it ( 'should penalize alias match types' , ( ) => {
87- const mockFile = {
109+ const mockFile = createMockIndexedFile ( {
88110 path : 'test.md' ,
89111 basename : 'test' ,
90- aliases : [ 'exact-match' ] ,
91- headings : [ ] ,
92- blockIds : [ ] ,
93- tags : [ ] ,
94- modified : Date . now ( ) ,
95- folder : ''
96- } ;
112+ aliases : [ 'exact-match' ]
113+ } ) ;
97114
98115 const score = ( fileIndex as any ) . calculateScore ( mockFile , 'exact-match' , { } , 0.5 , 'alias' ) ;
99116
100117 expect ( score ) . toBeGreaterThan ( 0.5 ) ; // Should be penalized (higher score = worse ranking)
101118 } ) ;
102119
103120 it ( 'should rank basename matches better than alias matches' , ( ) => {
104- const mockFile = {
121+ const mockFile = createMockIndexedFile ( {
105122 path : 'test.md' ,
106123 basename : 'test' ,
107- aliases : [ 'my-alias' ] ,
108- headings : [ ] ,
109- blockIds : [ ] ,
110- tags : [ ] ,
111- modified : Date . now ( ) ,
112- folder : ''
113- } ;
124+ aliases : [ 'my-alias' ]
125+ } ) ;
114126
115127 const aliasScore = ( fileIndex as any ) . calculateScore ( mockFile , 'my-alias' , { } , 0.5 , 'alias' ) ;
116128 const basenameScore = ( fileIndex as any ) . calculateScore ( mockFile , 'test' , { } , 0.5 , 'exact' ) ;
@@ -119,27 +131,17 @@ describe('FileIndex', () => {
119131 } ) ;
120132
121133 it ( 'should boost files with tag overlap' , ( ) => {
122- const currentFile = {
134+ const currentFile = createMockIndexedFile ( {
123135 path : 'current.md' ,
124136 basename : 'current' ,
125- aliases : [ ] ,
126- headings : [ ] ,
127- blockIds : [ ] ,
128- tags : [ '#shared-tag' ] ,
129- modified : Date . now ( ) ,
130- folder : ''
131- } ;
137+ tags : [ '#shared-tag' ]
138+ } ) ;
132139
133- const testFile = {
140+ const testFile = createMockIndexedFile ( {
134141 path : 'test.md' ,
135142 basename : 'test' ,
136- aliases : [ ] ,
137- headings : [ ] ,
138- blockIds : [ ] ,
139- tags : [ '#shared-tag' , '#other-tag' ] ,
140- modified : Date . now ( ) ,
141- folder : ''
142- } ;
143+ tags : [ '#shared-tag' , '#other-tag' ]
144+ } ) ;
143145
144146 // Simulate current file in map
145147 ( fileIndex as any ) . fileMap . set ( 'current.md' , currentFile ) ;
@@ -189,9 +191,10 @@ describe('FileIndex', () => {
189191 } ) ) ;
190192
191193 // Initial index – ensure all batched timers run so both files are indexed
192- await freshIndex . ensureIndexed ( ) ;
194+ const indexPromise = freshIndex . ensureIndexed ( ) ;
193195 // Flush any pending timers (including 0-ms ones) used inside performReindex()
194196 await vi . runAllTimersAsync ( ) ;
197+ await indexPromise ;
195198 expect ( freshIndex . getIndexedFileCount ( ) ) . toBeGreaterThanOrEqual ( 1 ) ;
196199
197200 // Spy on the methods - use proper type assertion
@@ -340,6 +343,28 @@ describe('FileIndex', () => {
340343 } ) ;
341344
342345 describe ( 'search functionality' , ( ) => {
346+ it ( 'should match normalized query against decomposed filenames' , async ( ) => {
347+ const nfcName = 'Rücken-Fit' ;
348+ const nfdName = nfcName . normalize ( 'NFD' ) ;
349+ const files = [
350+ {
351+ path : `${ nfdName } .md` ,
352+ basename : nfdName ,
353+ extension : 'md' ,
354+ parent : { path : '' } ,
355+ stat : { mtime : Date . now ( ) }
356+ }
357+ ] as TFile [ ] ;
358+
359+ ( mockApp . vault . getMarkdownFiles as any ) . mockReturnValue ( files ) ;
360+ mockApp . metadataCache . getFileCache = vi . fn ( ( ) => ( { } ) ) ;
361+
362+ await fileIndex . ensureIndexed ( ) ;
363+ const results = fileIndex . search ( 'Rü' , { } , 10 ) ;
364+
365+ expect ( results . some ( result => result . file . basename === nfdName ) ) . toBe ( true ) ;
366+ } ) ;
367+
343368 it . skip ( 'should return exact matches first' , async ( ) => {
344369 const files = [
345370 { path : 'test.md' , basename : 'test' } ,
0 commit comments