355 lines
11 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|