import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { ref } from 'vue'; import { useGamemasterSearch } from '@/composables/useGamemasterSearch.js'; describe('useGamemasterSearch', () => { let fileLines; let displayLines; let composable; beforeEach(() => { // Mock Web Worker global.Worker = vi.fn(() => ({ postMessage: vi.fn(), onmessage: null, onerror: null, terminate: vi.fn() })); // Sample file content fileLines = ref([ 'const hello = "world";', 'console.log("hello");', 'function test() {', ' return "hello world";', '}', 'const greeting = "hello";' ]); displayLines = ref( fileLines.value.map((content, index) => ({ lineNumber: index + 1, content, hasMatch: false })) ); composable = useGamemasterSearch(fileLines, displayLines); }); afterEach(() => { vi.clearAllMocks(); }); describe('initialization', () => { it('should initialize with empty search state', () => { expect(composable.searchQuery.value).toBe(''); expect(composable.searchResults.value).toEqual([]); expect(composable.currentResultIndex.value).toBe(0); expect(composable.isSearching.value).toBe(false); expect(composable.searchError.value).toBeNull(); }); it('should have search history', () => { expect(composable.searchHistory).toBeDefined(); }); }); describe('clearSearch', () => { it('should clear search query and results', () => { composable.searchQuery.value = 'test'; composable.searchResults.value = [0, 2, 5]; composable.currentResultIndex.value = 1; composable.clearSearch(); expect(composable.searchQuery.value).toBe(''); expect(composable.searchResults.value).toEqual([]); expect(composable.currentResultIndex.value).toBe(0); }); it('should clear hasMatch from display lines', () => { displayLines.value[0].hasMatch = true; displayLines.value[2].hasMatch = true; composable.clearSearch(); expect(displayLines.value.every(line => !line.hasMatch)).toBe(true); }); }); describe('clearSearchResults', () => { it('should clear results but keep query', () => { composable.searchQuery.value = 'hello'; composable.searchResults.value = [0, 1, 5]; composable.clearSearchResults(); expect(composable.searchQuery.value).toBe('hello'); expect(composable.searchResults.value).toEqual([]); }); it('should clear error state', () => { composable.searchError.value = 'Some error'; composable.clearSearchResults(); expect(composable.searchError.value).toBeNull(); }); }); describe('goToNextResult', () => { beforeEach(() => { composable.searchResults.value = [0, 2, 4]; composable.currentResultIndex.value = 0; }); it('should move to next result', () => { composable.goToNextResult(); expect(composable.currentResultIndex.value).toBe(1); composable.goToNextResult(); expect(composable.currentResultIndex.value).toBe(2); }); it('should wrap around to start', () => { composable.currentResultIndex.value = 2; composable.goToNextResult(); expect(composable.currentResultIndex.value).toBe(0); }); it('should do nothing when no results', () => { composable.searchResults.value = []; composable.currentResultIndex.value = 0; composable.goToNextResult(); expect(composable.currentResultIndex.value).toBe(0); }); }); describe('goToPrevResult', () => { beforeEach(() => { composable.searchResults.value = [0, 2, 4]; composable.currentResultIndex.value = 2; }); it('should move to previous result', () => { composable.goToPrevResult(); expect(composable.currentResultIndex.value).toBe(1); composable.goToPrevResult(); expect(composable.currentResultIndex.value).toBe(0); }); it('should wrap around to end', () => { composable.currentResultIndex.value = 0; composable.goToPrevResult(); expect(composable.currentResultIndex.value).toBe(2); }); it('should do nothing when no results', () => { composable.searchResults.value = []; composable.currentResultIndex.value = 0; composable.goToPrevResult(); expect(composable.currentResultIndex.value).toBe(0); }); }); describe('getHighlightedContent', () => { it('should return content unchanged when no search query', () => { composable.searchQuery.value = ''; const content = 'hello world'; expect(composable.getHighlightedContent(content)).toBe(content); }); it('should highlight matching text', () => { composable.searchQuery.value = 'hello'; const content = 'hello world'; const result = composable.getHighlightedContent(content); expect(result).toContain('hello'); }); it('should be case-insensitive', () => { composable.searchQuery.value = 'HELLO'; const content = 'hello world'; const result = composable.getHighlightedContent(content); expect(result).toContain('hello'); }); it('should highlight multiple occurrences', () => { composable.searchQuery.value = 'o'; const content = 'hello world'; const result = composable.getHighlightedContent(content); const matches = (result.match(/o<\/mark>/g) || []).length; expect(matches).toBe(2); }); it('should escape regex special characters', () => { composable.searchQuery.value = '.*'; const content = 'test .* pattern'; expect(() => composable.getHighlightedContent(content)).not.toThrow(); }); }); describe('computed properties', () => { describe('currentResultLineNumber', () => { it('should return null when no results', () => { composable.searchResults.value = []; expect(composable.currentResultLineNumber.value).toBeNull(); }); it('should return current result as 1-based line number', () => { composable.searchResults.value = [0, 2, 4]; composable.currentResultIndex.value = 1; expect(composable.currentResultLineNumber.value).toBe(3); // Line 3 (index 2 + 1) }); }); describe('resultCountDisplay', () => { it('should show "0 results" when empty', () => { composable.searchResults.value = []; expect(composable.resultCountDisplay.value).toBe('0 results'); }); it('should show current/total format', () => { composable.searchResults.value = [0, 2, 4]; composable.currentResultIndex.value = 1; expect(composable.resultCountDisplay.value).toBe('2 / 3'); }); }); describe('hasSearchResults', () => { it('should be false when no results', () => { composable.searchResults.value = []; expect(composable.hasSearchResults.value).toBe(false); }); it('should be true when results exist', () => { composable.searchResults.value = [0, 1, 2]; expect(composable.hasSearchResults.value).toBe(true); }); }); }); describe('updateFilters', () => { it('should be callable without errors', () => { expect(() => composable.updateFilters()).not.toThrow(); }); it('should accept filter options', () => { expect(() => { composable.updateFilters({ fuzzy: true, wholeWord: true, caseSensitive: false }); }).not.toThrow(); }); }); describe('applyHistoryItem', () => { it('should set search query to history item', async () => { const historyItem = 'previous search'; composable.applyHistoryItem(historyItem); expect(composable.searchQuery.value).toBe(historyItem); }); }); describe('executeSearch', () => { it('should set search query', async () => { composable.executeSearch('test'); expect(composable.searchQuery.value).toBe('test'); }); it('should clear results when passing empty query', async () => { composable.searchResults.value = [0, 1, 2]; composable.searchQuery.value = ''; composable.clearSearchResults(); expect(composable.searchResults.value).toEqual([]); }); }); describe('performSynchronousSearch', () => { it('should find matching lines', () => { composable.searchQuery.value = 'hello'; composable.performSearch = composable.performSearch.fn ? composable.performSearch : vi.fn(() => { const results = []; const searchTerm = composable.searchQuery.value.toLowerCase(); fileLines.value.forEach((line, index) => { if (line.toLowerCase().includes(searchTerm)) { results.push(index); } }); composable.searchResults.value = results; }); // Manually call synchronous search for testing const results = []; const searchTerm = 'hello'; fileLines.value.forEach((line, index) => { if (line.toLowerCase().includes(searchTerm)) { results.push(index); } }); expect(results.length).toBeGreaterThan(0); expect(results).toContain(0); // 'const hello = "world";' expect(results).toContain(1); // 'console.log("hello");' expect(results).toContain(3); // 'return "hello world";' }); it('should handle case-insensitive search', () => { const searchTerm = 'FUNCTION'; const results = []; fileLines.value.forEach((line, index) => { if (line.toLowerCase().includes(searchTerm.toLowerCase())) { results.push(index); } }); expect(results).toContain(2); // 'function test() {' }); it('should handle special regex characters gracefully', () => { const searchTerm = '.*'; const escapedTerm = searchTerm.replace( /[.*+?^${}()|[\]\\]/g, String.raw`\$&` ); expect(() => new RegExp(escapedTerm)).not.toThrow(); }); }); describe('search query reactivity', () => { it('should have debounced performSearch method', () => { expect(typeof composable.performSearch).toBe('function'); }); it('should clear results when clearSearch is called', async () => { composable.searchResults.value = [0, 1, 2]; composable.searchQuery.value = 'test'; composable.clearSearch(); expect(composable.searchQuery.value).toBe(''); expect(composable.searchResults.value).toEqual([]); }); }); describe('error handling', () => { it('should set error state on search failure', () => { composable.searchError.value = 'Search failed'; expect(composable.searchError.value).toBe('Search failed'); }); it('should clear error on successful search', () => { composable.searchError.value = 'Previous error'; composable.clearSearch(); // Error should be cleared by clearSearchResults expect(composable.searchError.value).toBeNull(); }); }); });