✅ Update progress documentation to reflect completed SearchBar component and additional passing tests
This commit is contained in:
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search-bar">
|
||||||
|
<div class="search-input-wrapper">
|
||||||
|
<input
|
||||||
|
ref="searchInput"
|
||||||
|
type="text"
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="Search in file... (Ctrl+F)"
|
||||||
|
class="search-input"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="searchQuery"
|
||||||
|
@click="clearSearch"
|
||||||
|
class="btn-clear"
|
||||||
|
title="Clear search"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasSearchResults" class="search-results">
|
||||||
|
<span :title="currentResultLineTitle">
|
||||||
|
{{ resultCountDisplay }}
|
||||||
|
<template v-if="currentResultLineNumber">
|
||||||
|
(Line {{ currentResultLineNumber }})
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
@click="goToPrevResult"
|
||||||
|
class="btn-nav"
|
||||||
|
:disabled="!hasSearchResults"
|
||||||
|
title="Previous result (Shift+Ctrl+G)"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="goToNextResult"
|
||||||
|
class="btn-nav"
|
||||||
|
:disabled="!hasSearchResults"
|
||||||
|
title="Next result (Ctrl+G)"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="searchHistory.history.value.length > 0" class="search-history">
|
||||||
|
<button
|
||||||
|
v-for="(item, index) in searchHistory.history.value"
|
||||||
|
:key="index"
|
||||||
|
@click="applyHistoryItem(item)"
|
||||||
|
class="history-item"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="searchError" class="search-error">
|
||||||
|
{{ searchError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isSearching" class="search-status">
|
||||||
|
Searching...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, toRef } from 'vue';
|
||||||
|
import { useGamemasterSearch } from '../../composables/useGamemasterSearch.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
fileLines: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
displayLines: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileLines = toRef(props, 'fileLines');
|
||||||
|
const displayLines = toRef(props, 'displayLines');
|
||||||
|
|
||||||
|
const {
|
||||||
|
searchQuery,
|
||||||
|
searchResults,
|
||||||
|
currentResultIndex,
|
||||||
|
isSearching,
|
||||||
|
searchError,
|
||||||
|
searchHistory,
|
||||||
|
clearSearch,
|
||||||
|
goToNextResult,
|
||||||
|
goToPrevResult,
|
||||||
|
applyHistoryItem,
|
||||||
|
currentResultLineNumber,
|
||||||
|
resultCountDisplay,
|
||||||
|
hasSearchResults
|
||||||
|
} = useGamemasterSearch(fileLines, displayLines);
|
||||||
|
|
||||||
|
const isDisabled = computed(() => props.disabled || fileLines.value.length === 0);
|
||||||
|
|
||||||
|
const currentResultLineTitle = computed(() => {
|
||||||
|
if (!currentResultLineNumber.value) return '';
|
||||||
|
return `Line ${currentResultLineNumber.value}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 35px 8px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-history {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-error {
|
||||||
|
color: #d9534f;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-status {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* SearchBar Component Tests
|
||||||
|
* Verifies search UI rendering and interactions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import SearchBar from '../../../../src/components/gamemaster/SearchBar.vue';
|
||||||
|
import { useGamemasterSearch } from '../../../../src/composables/useGamemasterSearch.js';
|
||||||
|
|
||||||
|
vi.mock('../../../../src/composables/useGamemasterSearch.js', () => ({
|
||||||
|
useGamemasterSearch: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createSearchMock = overrides => ({
|
||||||
|
searchQuery: ref(''),
|
||||||
|
searchResults: ref([]),
|
||||||
|
currentResultIndex: ref(0),
|
||||||
|
isSearching: ref(false),
|
||||||
|
searchError: ref(null),
|
||||||
|
searchHistory: { history: ref([]) },
|
||||||
|
clearSearch: vi.fn(),
|
||||||
|
goToNextResult: vi.fn(),
|
||||||
|
goToPrevResult: vi.fn(),
|
||||||
|
applyHistoryItem: vi.fn(),
|
||||||
|
currentResultLineNumber: ref(null),
|
||||||
|
resultCountDisplay: ref('0 results'),
|
||||||
|
hasSearchResults: ref(false),
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SearchBar Component', () => {
|
||||||
|
let mockSearch;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSearch = createSearchMock();
|
||||||
|
useGamemasterSearch.mockReturnValue(mockSearch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the search input', () => {
|
||||||
|
const wrapper = mount(SearchBar, {
|
||||||
|
props: {
|
||||||
|
fileLines: ['line one'],
|
||||||
|
displayLines: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = wrapper.find('input.search-input');
|
||||||
|
expect(input.exists()).toBe(true);
|
||||||
|
expect(input.attributes('placeholder')).toContain('Search in file');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables input when fileLines are empty', () => {
|
||||||
|
const wrapper = mount(SearchBar, {
|
||||||
|
props: {
|
||||||
|
fileLines: [],
|
||||||
|
displayLines: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = wrapper.find('input.search-input');
|
||||||
|
expect(input.attributes('disabled')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows clear button when searchQuery is not empty', async () => {
|
||||||
|
mockSearch.searchQuery.value = 'Pikachu';
|
||||||
|
const wrapper = mount(SearchBar, {
|
||||||
|
props: {
|
||||||
|
fileLines: ['line one'],
|
||||||
|
displayLines: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearButton = wrapper.find('button.btn-clear');
|
||||||
|
expect(clearButton.exists()).toBe(true);
|
||||||
|
|
||||||
|
await clearButton.trigger('click');
|
||||||
|
expect(mockSearch.clearSearch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders search results summary and line number', () => {
|
||||||
|
mockSearch.hasSearchResults.value = true;
|
||||||
|
mockSearch.resultCountDisplay.value = '2 / 5';
|
||||||
|
mockSearch.currentResultLineNumber.value = 42;
|
||||||
|
|
||||||
|
const wrapper = mount(SearchBar, {
|
||||||
|
props: {
|
||||||
|
fileLines: ['line one'],
|
||||||
|
displayLines: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = wrapper.find('.search-results');
|
||||||
|
expect(results.exists()).toBe(true);
|
||||||
|
expect(results.text()).toContain('2 / 5');
|
||||||
|
expect(results.text()).toContain('Line 42');
|
||||||
|
|
||||||
|
const titleSpan = results.find('span');
|
||||||
|
expect(titleSpan.attributes('title')).toBe('Line 42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers next/previous navigation', async () => {
|
||||||
|
mockSearch.hasSearchResults.value = true;
|
||||||
|
|
||||||
|
const wrapper = mount(SearchBar, {
|
||||||
|
props: {
|
||||||
|
fileLines: ['line one'],
|
||||||
|
displayLines: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = wrapper.findAll('button.btn-nav');
|
||||||
|
expect(buttons.length).toBe(2);
|
||||||
|
|
||||||
|
await buttons[0].trigger('click');
|
||||||
|
await buttons[1].trigger('click');
|
||||||
|
|
||||||
|
expect(mockSearch.goToPrevResult).toHaveBeenCalled();
|
||||||
|
expect(mockSearch.goToNextResult).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders search history items and applies selection', async () => {
|
||||||
|
mockSearch.searchHistory.history.value = ['Pikachu', 'Charizard'];
|
||||||
|
|
||||||
|
const wrapper = mount(SearchBar, {
|
||||||
|
props: {
|
||||||
|
fileLines: ['line one'],
|
||||||
|
displayLines: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const historyButtons = wrapper.findAll('button.history-item');
|
||||||
|
expect(historyButtons.length).toBe(2);
|
||||||
|
|
||||||
|
await historyButtons[0].trigger('click');
|
||||||
|
expect(mockSearch.applyHistoryItem).toHaveBeenCalledWith('Pikachu');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error and searching status', () => {
|
||||||
|
mockSearch.searchError.value = 'Worker failed';
|
||||||
|
mockSearch.isSearching.value = true;
|
||||||
|
|
||||||
|
const wrapper = mount(SearchBar, {
|
||||||
|
props: {
|
||||||
|
fileLines: ['line one'],
|
||||||
|
displayLines: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('.search-error').text()).toContain('Worker failed');
|
||||||
|
expect(wrapper.find('.search-status').text()).toContain('Searching');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,9 +7,9 @@ Last Updated: January 28, 2026
|
|||||||
**Total Phases:** 12
|
**Total Phases:** 12
|
||||||
**Completed Phases:** 4 ✅
|
**Completed Phases:** 4 ✅
|
||||||
**Total Steps:** 68
|
**Total Steps:** 68
|
||||||
**Completed Steps:** 22 / 68 (32.4%)
|
**Completed Steps:** 23 / 68 (33.8%)
|
||||||
|
|
||||||
**Test Suite:** 283 tests passing ✅
|
**Test Suite:** 290 tests passing ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ Last Updated: January 28, 2026
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 5: GamemasterExplorer Refactoring (4/9 complete)
|
## Phase 5: GamemasterExplorer Refactoring (5/9 complete)
|
||||||
**Duration**: 5-7 days
|
**Duration**: 5-7 days
|
||||||
**Status**: In Progress
|
**Status**: In Progress
|
||||||
**Target**: Reduce from 1627 lines → ~400 lines (76% reduction)
|
**Target**: Reduce from 1627 lines → ~400 lines (76% reduction)
|
||||||
@@ -168,13 +168,13 @@ Last Updated: January 28, 2026
|
|||||||
- Statistics calculation
|
- Statistics calculation
|
||||||
- Edge cases (circular refs, arrays, unicode)
|
- Edge cases (circular refs, arrays, unicode)
|
||||||
|
|
||||||
### ⏳ Step 17: Create SearchBar Component
|
### ✅ Step 17: Create SearchBar Component
|
||||||
- [ ] Create `src/components/gamemaster/SearchBar.vue`
|
- [x] Create `src/components/gamemaster/SearchBar.vue`
|
||||||
- [ ] Use `useGamemasterSearch` composable
|
- [x] Use `useGamemasterSearch` composable
|
||||||
- [ ] Implement search input with result counter
|
- [x] Implement search input with result counter
|
||||||
- [ ] Add next/previous result navigation
|
- [x] Add next/previous result navigation
|
||||||
- [ ] Add clear/reset functionality
|
- [x] Add clear/reset functionality
|
||||||
- [ ] Write component tests
|
- [x] Write component tests - 7 tests passing ✅
|
||||||
|
|
||||||
### ⏳ Step 18: Create FileSelector Component
|
### ⏳ Step 18: Create FileSelector Component
|
||||||
- [ ] Create `src/components/gamemaster/FileSelector.vue`
|
- [ ] Create `src/components/gamemaster/FileSelector.vue`
|
||||||
|
|||||||
Reference in New Issue
Block a user