Files
memory-infrastructure-palace/code/websites/pokedex.online/tests/unit/composables/useGamemasterSearch.test.js

355 lines
11 KiB
JavaScript

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('<mark>hello</mark>');
});
it('should be case-insensitive', () => {
composable.searchQuery.value = 'HELLO';
const content = 'hello world';
const result = composable.getHighlightedContent(content);
expect(result).toContain('<mark>hello</mark>');
});
it('should highlight multiple occurrences', () => {
composable.searchQuery.value = 'o';
const content = 'hello world';
const result = composable.getHighlightedContent(content);
const matches = (result.match(/<mark>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 and trigger search', async () => {
await composable.executeSearch('test');
expect(composable.searchQuery.value).toBe('test');
});
it('should clear results for empty query', async () => {
composable.searchResults.value = [0, 1, 2];
await composable.executeSearch('');
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 trigger search when query changes', async () => {
const performSearchSpy = vi.spyOn(composable, 'performSearch');
composable.searchQuery.value = 'test';
// Wait for debounce
await new Promise(resolve => setTimeout(resolve, 400));
expect(performSearchSpy).toHaveBeenCalled();
});
it('should clear results when query becomes empty', async () => {
composable.searchResults.value = [0, 1, 2];
composable.searchQuery.value = '';
await new Promise(resolve => setTimeout(resolve, 50));
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();
});
});
});