mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	feat(tests): implement tests for updated fuzzy search operators, and text_utils used in search
This commit is contained in:
		@@ -1,5 +1,6 @@
 | 
			
		||||
import { describe, it, expect } from "vitest";
 | 
			
		||||
import { processMindmapContent } from "./note_content_fulltext.js";
 | 
			
		||||
import NoteContentFulltextExp from "./note_content_fulltext.js";
 | 
			
		||||
 | 
			
		||||
describe("processMindmapContent", () => {
 | 
			
		||||
    it("supports empty JSON", () => {
 | 
			
		||||
@@ -11,3 +12,19 @@ describe("processMindmapContent", () => {
 | 
			
		||||
        expect(processMindmapContent(`{ "node": " }`)).toEqual("");
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("Fuzzy Search Operators", () => {
 | 
			
		||||
    it("~= operator works with typos", () => {
 | 
			
		||||
        // Test that the ~= operator can handle common typos
 | 
			
		||||
        const expression = new NoteContentFulltextExp("~=", { tokens: ["hello"] });
 | 
			
		||||
        expect(expression.tokens).toEqual(["hello"]);
 | 
			
		||||
        expect(() => new NoteContentFulltextExp("~=", { tokens: ["he"] })).toThrow(); // Too short
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("~* operator works with fuzzy contains", () => {
 | 
			
		||||
        // Test that the ~* operator handles fuzzy substring matching
 | 
			
		||||
        const expression = new NoteContentFulltextExp("~*", { tokens: ["world"] });
 | 
			
		||||
        expect(expression.tokens).toEqual(["world"]);
 | 
			
		||||
        expect(() => new NoteContentFulltextExp("~*", { tokens: ["wo"] })).toThrow(); // Too short
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -553,6 +553,31 @@ describe("Search", () => {
 | 
			
		||||
        expect(becca.notes[searchResults[0].noteId].title).toEqual("Reddit is bad");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("search completes in reasonable time", () => {
 | 
			
		||||
        // Create a moderate-sized dataset to test performance
 | 
			
		||||
        const countries = ["Austria", "Belgium", "Croatia", "Denmark", "Estonia", "Finland", "Germany", "Hungary", "Ireland", "Japan"];
 | 
			
		||||
        const europeanCountries = note("Europe");
 | 
			
		||||
        
 | 
			
		||||
        countries.forEach(country => {
 | 
			
		||||
            europeanCountries.child(note(country).label("type", "country").label("continent", "Europe"));
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        rootNote.child(europeanCountries);
 | 
			
		||||
 | 
			
		||||
        const searchContext = new SearchContext();
 | 
			
		||||
        const startTime = Date.now();
 | 
			
		||||
        
 | 
			
		||||
        // Perform a search that exercises multiple features
 | 
			
		||||
        const searchResults = searchService.findResultsWithQuery("#type=country AND continent", searchContext);
 | 
			
		||||
        
 | 
			
		||||
        const endTime = Date.now();
 | 
			
		||||
        const duration = endTime - startTime;
 | 
			
		||||
        
 | 
			
		||||
        // Search should complete in under 1 second for reasonable dataset
 | 
			
		||||
        expect(duration).toBeLessThan(1000);
 | 
			
		||||
        expect(searchResults.length).toEqual(10);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // FIXME: test what happens when we order without any filter criteria
 | 
			
		||||
 | 
			
		||||
    // it("comparison between labels", () => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										65
									
								
								apps/server/src/services/search/utils/text_utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								apps/server/src/services/search/utils/text_utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
import { describe, it, expect } from "vitest";
 | 
			
		||||
import { calculateOptimizedEditDistance, validateFuzzySearchTokens, fuzzyMatchWord } from './text_utils.js';
 | 
			
		||||
 | 
			
		||||
describe('Fuzzy Search Core', () => {
 | 
			
		||||
    describe('calculateOptimizedEditDistance', () => {
 | 
			
		||||
        it('calculates edit distance for common typos', () => {
 | 
			
		||||
            expect(calculateOptimizedEditDistance('hello', 'helo')).toBe(1);
 | 
			
		||||
            expect(calculateOptimizedEditDistance('world', 'wrold')).toBe(2);
 | 
			
		||||
            expect(calculateOptimizedEditDistance('cafe', 'café')).toBe(1);
 | 
			
		||||
            expect(calculateOptimizedEditDistance('identical', 'identical')).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('handles performance safety with oversized input', () => {
 | 
			
		||||
            const longString = 'a'.repeat(2000);
 | 
			
		||||
            const result = calculateOptimizedEditDistance(longString, 'short');
 | 
			
		||||
            expect(result).toBeGreaterThan(2); // Should use fallback heuristic
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('validateFuzzySearchTokens', () => {
 | 
			
		||||
        it('validates minimum length requirements for fuzzy operators', () => {
 | 
			
		||||
            const result1 = validateFuzzySearchTokens(['ab'], '~=');
 | 
			
		||||
            expect(result1.isValid).toBe(false);
 | 
			
		||||
            expect(result1.error).toContain('at least 3 characters');
 | 
			
		||||
 | 
			
		||||
            const result2 = validateFuzzySearchTokens(['hello'], '~=');
 | 
			
		||||
            expect(result2.isValid).toBe(true);
 | 
			
		||||
 | 
			
		||||
            const result3 = validateFuzzySearchTokens(['ok'], '=');
 | 
			
		||||
            expect(result3.isValid).toBe(true); // Non-fuzzy operators allow short tokens
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('validates token types and empty arrays', () => {
 | 
			
		||||
            expect(validateFuzzySearchTokens([], '=')).toEqual({
 | 
			
		||||
                isValid: false,
 | 
			
		||||
                error: 'Invalid tokens: at least one token is required'
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(validateFuzzySearchTokens([''], '=')).toEqual({
 | 
			
		||||
                isValid: false,
 | 
			
		||||
                error: 'Invalid tokens: empty or whitespace-only tokens are not allowed'
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('fuzzyMatchWord', () => {
 | 
			
		||||
        it('matches words with diacritics normalization', () => {
 | 
			
		||||
            expect(fuzzyMatchWord('cafe', 'café')).toBe(true);
 | 
			
		||||
            expect(fuzzyMatchWord('naive', 'naïve')).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('matches with typos within distance threshold', () => {
 | 
			
		||||
            expect(fuzzyMatchWord('hello', 'helo')).toBe(true);
 | 
			
		||||
            expect(fuzzyMatchWord('world', 'wrold')).toBe(true);
 | 
			
		||||
            expect(fuzzyMatchWord('test', 'tset')).toBe(true);
 | 
			
		||||
            expect(fuzzyMatchWord('test', 'xyz')).toBe(false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('handles edge cases safely', () => {
 | 
			
		||||
            expect(fuzzyMatchWord('', 'test')).toBe(false);
 | 
			
		||||
            expect(fuzzyMatchWord('test', '')).toBe(false);
 | 
			
		||||
            expect(fuzzyMatchWord('a', 'b')).toBe(false); // Very short tokens
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user