complete story 5.5 with qa tests
This commit is contained in:
parent
6eef462732
commit
e7c9284557
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Quality Gate Decision
|
||||||
|
schema: 1
|
||||||
|
story: "5.5"
|
||||||
|
story_title: "Post Search"
|
||||||
|
gate: PASS
|
||||||
|
status_reason: "All acceptance criteria met with comprehensive test coverage. Implementation follows coding standards with proper security measures (XSS protection, regex injection prevention)."
|
||||||
|
reviewer: "Quinn (Test Architect)"
|
||||||
|
updated: "2025-12-27T00:00:00Z"
|
||||||
|
|
||||||
|
waiver: { active: false }
|
||||||
|
|
||||||
|
top_issues: []
|
||||||
|
|
||||||
|
risk_summary:
|
||||||
|
totals: { critical: 0, high: 0, medium: 0, low: 0 }
|
||||||
|
recommendations:
|
||||||
|
must_fix: []
|
||||||
|
monitor:
|
||||||
|
- action: "Monitor search performance at scale - LIKE on JSON columns may need optimization for large datasets"
|
||||||
|
refs: ["resources/views/livewire/pages/posts/index.blade.php:47-49"]
|
||||||
|
|
||||||
|
quality_score: 100
|
||||||
|
expires: "2026-01-10T00:00:00Z"
|
||||||
|
|
||||||
|
evidence:
|
||||||
|
tests_reviewed: 9
|
||||||
|
risks_identified: 0
|
||||||
|
trace:
|
||||||
|
ac_covered: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
ac_gaps: []
|
||||||
|
|
||||||
|
nfr_validation:
|
||||||
|
security:
|
||||||
|
status: PASS
|
||||||
|
notes: "XSS protection via e() escaping, regex injection prevented via preg_quote(), published-only scope"
|
||||||
|
performance:
|
||||||
|
status: PASS
|
||||||
|
notes: "300ms debounce, pagination at 10 items. LIKE on JSON acceptable for blog scale"
|
||||||
|
reliability:
|
||||||
|
status: PASS
|
||||||
|
notes: "Proper error handling, graceful empty states"
|
||||||
|
maintainability:
|
||||||
|
status: PASS
|
||||||
|
notes: "Clean code following Volt class-based pattern, well-structured tests"
|
||||||
|
|
||||||
|
recommendations:
|
||||||
|
immediate: []
|
||||||
|
future:
|
||||||
|
- action: "Consider full-text search or JSON path operators if post volume grows significantly"
|
||||||
|
refs: ["resources/views/livewire/pages/posts/index.blade.php:47-49"]
|
||||||
|
- action: "Search suggestions feature (marked optional in story)"
|
||||||
|
refs: ["docs/stories/story-5.5-post-search.md:35"]
|
||||||
|
|
@ -19,25 +19,25 @@ So that **I can find relevant legal articles and information**.
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
### Search Functionality
|
### Search Functionality
|
||||||
- [ ] Search input on posts listing page
|
- [x] Search input on posts listing page
|
||||||
- [ ] Search by title (both languages)
|
- [x] Search by title (both languages)
|
||||||
- [ ] Search by body content (both languages)
|
- [x] Search by body content (both languages)
|
||||||
- [ ] Real-time search results (debounced)
|
- [x] Real-time search results (debounced)
|
||||||
|
|
||||||
### User Experience
|
### User Experience
|
||||||
- [ ] "No results found" message when empty
|
- [x] "No results found" message when empty
|
||||||
- [ ] Clear search button
|
- [x] Clear search button
|
||||||
- [ ] Search works in both Arabic and English
|
- [x] Search works in both Arabic and English
|
||||||
- [ ] Only searches published posts
|
- [x] Only searches published posts
|
||||||
|
|
||||||
### Optional Enhancements
|
### Optional Enhancements
|
||||||
- [ ] Search highlights in results
|
- [x] Search highlights in results
|
||||||
- [ ] Search suggestions
|
- [ ] Search suggestions
|
||||||
|
|
||||||
### Quality Requirements
|
### Quality Requirements
|
||||||
- [ ] Debounce to reduce server load (300ms)
|
- [x] Debounce to reduce server load (300ms)
|
||||||
- [ ] Case-insensitive search
|
- [x] Case-insensitive search
|
||||||
- [ ] Tests for search functionality
|
- [x] Tests for search functionality
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
|
|
||||||
|
|
@ -254,16 +254,48 @@ it('only searches published posts', function () {
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
|
|
||||||
- [ ] Search input on posts page
|
- [x] Search input on posts page
|
||||||
- [ ] Searches title in both languages
|
- [x] Searches title in both languages
|
||||||
- [ ] Searches body in both languages
|
- [x] Searches body in both languages
|
||||||
- [ ] Real-time results with debounce
|
- [x] Real-time results with debounce
|
||||||
- [ ] Clear search button works
|
- [x] Clear search button works
|
||||||
- [ ] "No results" message shows
|
- [x] "No results" message shows
|
||||||
- [ ] Only published posts searched
|
- [x] Only published posts searched
|
||||||
- [ ] Search highlighting works
|
- [x] Search highlighting works
|
||||||
- [ ] Tests pass
|
- [x] Tests pass
|
||||||
- [ ] Code formatted with Pint
|
- [x] Code formatted with Pint
|
||||||
|
|
||||||
|
## Dev Agent Record
|
||||||
|
|
||||||
|
### Agent Model Used
|
||||||
|
Claude Opus 4.5
|
||||||
|
|
||||||
|
### Completion Notes
|
||||||
|
- Implemented search functionality on the posts index page with real-time debounced search (300ms)
|
||||||
|
- Search queries both title and body fields as JSON columns (works with SQLite for tests and MySQL/MariaDB for production)
|
||||||
|
- Added search result highlighting with gold background on matched terms
|
||||||
|
- Clear search button with RTL/LTR position awareness
|
||||||
|
- Added bilingual translation strings for search UI (English and Arabic)
|
||||||
|
- 9 new search-specific tests added to PostsTest.php covering title search, body search, Arabic search, case-insensitivity, pagination reset, and clear functionality
|
||||||
|
- All 22 posts tests pass, full test suite passes
|
||||||
|
|
||||||
|
### File List
|
||||||
|
- `resources/views/livewire/pages/posts/index.blade.php` (modified) - Added search functionality and highlight method
|
||||||
|
- `lang/en/posts.php` (modified) - Added search translation strings
|
||||||
|
- `lang/ar/posts.php` (modified) - Added Arabic search translation strings
|
||||||
|
- `tests/Feature/Public/PostsTest.php` (modified) - Added 9 search tests
|
||||||
|
|
||||||
|
### Change Log
|
||||||
|
- Added `$search` property with debounced live binding
|
||||||
|
- Added `updatedSearch()` method to reset pagination on search change
|
||||||
|
- Added `clearSearch()` method to clear search and reset pagination
|
||||||
|
- Added `highlightSearch()` method for search term highlighting with HTML escaping
|
||||||
|
- Updated `with()` query to filter posts by title and body columns using LIKE
|
||||||
|
- Added search input with magnifying glass icon
|
||||||
|
- Added clear search button (X) with RTL-aware positioning
|
||||||
|
- Added search results info message showing count or "no results" message
|
||||||
|
- Added empty state with search icon and clear button when search yields no results
|
||||||
|
- Search highlighting applied to both title and excerpt in search results
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|
@ -273,3 +305,125 @@ it('only searches published posts', function () {
|
||||||
|
|
||||||
**Complexity:** Low-Medium
|
**Complexity:** Low-Medium
|
||||||
**Estimated Effort:** 2-3 hours
|
**Estimated Effort:** 2-3 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QA Results
|
||||||
|
|
||||||
|
### Review Date: 2025-12-27
|
||||||
|
|
||||||
|
### Reviewed By: Quinn (Test Architect)
|
||||||
|
|
||||||
|
### Code Quality Assessment
|
||||||
|
|
||||||
|
The implementation is solid and follows project conventions well. The search functionality is implemented correctly using Livewire Volt's class-based pattern with proper debouncing, pagination reset, and bilingual support. The `highlightSearch()` method is well-designed with proper XSS protection using `e()` for both input text and search term before applying regex highlighting.
|
||||||
|
|
||||||
|
**Notable positives:**
|
||||||
|
- Clean, readable code following coding standards
|
||||||
|
- Proper use of Flux UI components
|
||||||
|
- RTL-aware positioning for the clear search button
|
||||||
|
- Good separation between display logic (getTitle/getExcerpt) and search logic
|
||||||
|
|
||||||
|
### Refactoring Performed
|
||||||
|
|
||||||
|
None required - the implementation is clean and follows best practices.
|
||||||
|
|
||||||
|
### Compliance Check
|
||||||
|
|
||||||
|
- Coding Standards: ✓ Uses class-based Volt pattern, Flux UI components, proper testing
|
||||||
|
- Project Structure: ✓ Files in correct locations, naming follows conventions
|
||||||
|
- Testing Strategy: ✓ 9 comprehensive tests with 22 assertions covering all scenarios
|
||||||
|
- All ACs Met: ✓ All acceptance criteria verified with corresponding tests
|
||||||
|
|
||||||
|
### Improvements Checklist
|
||||||
|
|
||||||
|
- [x] Search functionality implemented with proper debouncing (300ms)
|
||||||
|
- [x] Bilingual search working for both title and body
|
||||||
|
- [x] XSS protection in highlight function
|
||||||
|
- [x] Case-insensitive search implemented
|
||||||
|
- [x] Clear search resets pagination
|
||||||
|
- [x] "No results" message displays correctly
|
||||||
|
- [x] Only published posts are searched
|
||||||
|
- [ ] Optional: Consider JSON path operators (`->`) for more precise JSON column searches in production MySQL (current LIKE on JSON works but is less precise)
|
||||||
|
- [ ] Optional enhancement: Search suggestions (marked as optional in story, not implemented)
|
||||||
|
|
||||||
|
### Security Review
|
||||||
|
|
||||||
|
✓ **XSS Protection**: The `highlightSearch()` method properly escapes HTML using `e()` before applying regex replacement, preventing XSS attacks.
|
||||||
|
✓ **Regex Injection**: `preg_quote()` is used to escape special regex characters in the search term.
|
||||||
|
✓ **Authorization**: Search is scoped to `published()` posts only - draft content is not accessible.
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
✓ **Debouncing**: 300ms debounce reduces unnecessary server requests
|
||||||
|
✓ **Pagination**: 10 items per page limits data transfer
|
||||||
|
- Note: LIKE queries on JSON columns may become slow with very large datasets. For a blog with hundreds/thousands of posts, this is acceptable. For larger scale, consider full-text search indexes.
|
||||||
|
|
||||||
|
### Files Modified During Review
|
||||||
|
|
||||||
|
None - no refactoring was required.
|
||||||
|
|
||||||
|
### Gate Status
|
||||||
|
|
||||||
|
Gate: PASS → docs/qa/gates/5.5-post-search.yml
|
||||||
|
|
||||||
|
### Recommended Status
|
||||||
|
|
||||||
|
✓ Ready for Done - All acceptance criteria met, tests pass, code follows standards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Story DoD Checklist
|
||||||
|
|
||||||
|
### 1. Requirements Met:
|
||||||
|
- [x] All functional requirements specified in the story are implemented.
|
||||||
|
- Search input on posts listing page ✓
|
||||||
|
- Search by title (both languages) ✓
|
||||||
|
- Search by body content (both languages) ✓
|
||||||
|
- Real-time search with 300ms debounce ✓
|
||||||
|
- [x] All acceptance criteria defined in the story are met.
|
||||||
|
- "No results found" message ✓
|
||||||
|
- Clear search button ✓
|
||||||
|
- Works in Arabic and English ✓
|
||||||
|
- Only searches published posts ✓
|
||||||
|
- Search highlights in results ✓
|
||||||
|
|
||||||
|
### 2. Coding Standards & Project Structure:
|
||||||
|
- [x] All new/modified code strictly adheres to `Operational Guidelines`.
|
||||||
|
- [x] All new/modified code aligns with `Project Structure` (file locations, naming, etc.).
|
||||||
|
- [x] Adherence to `Tech Stack` for technologies/versions used.
|
||||||
|
- [N/A] Adherence to `Api Reference` and `Data Models` - No API or data model changes.
|
||||||
|
- [x] Basic security best practices applied (HTML escaping in highlight function).
|
||||||
|
- [x] No new linter errors or warnings introduced (Pint passes).
|
||||||
|
- [x] Code is well-commented where necessary.
|
||||||
|
|
||||||
|
### 3. Testing:
|
||||||
|
- [x] All required unit tests implemented (9 new search tests).
|
||||||
|
- [N/A] Integration tests - Component tests cover functionality adequately.
|
||||||
|
- [x] All tests pass successfully (22 posts tests, full suite passes).
|
||||||
|
- [x] Test coverage meets project standards.
|
||||||
|
|
||||||
|
### 4. Functionality & Verification:
|
||||||
|
- [x] Functionality verified through comprehensive test suite.
|
||||||
|
- [x] Edge cases handled (empty search, no results, case-insensitivity, RTL support).
|
||||||
|
|
||||||
|
### 5. Story Administration:
|
||||||
|
- [x] All tasks within the story file are marked as complete.
|
||||||
|
- [x] Decisions documented (search on raw JSON columns vs JSON path, highlight implementation).
|
||||||
|
- [x] Story wrap up section completed with change log and file list.
|
||||||
|
|
||||||
|
### 6. Dependencies, Build & Configuration:
|
||||||
|
- [x] Project builds successfully without errors.
|
||||||
|
- [x] Project linting passes (Pint --dirty).
|
||||||
|
- [N/A] No new dependencies added.
|
||||||
|
- [N/A] No new environment variables or configurations.
|
||||||
|
|
||||||
|
### 7. Documentation:
|
||||||
|
- [x] Inline code documentation adequate (method names are self-documenting).
|
||||||
|
- [N/A] No user-facing documentation changes needed.
|
||||||
|
- [N/A] No architectural changes made.
|
||||||
|
|
||||||
|
### Final Confirmation
|
||||||
|
- [x] I, the Developer Agent, confirm that all applicable items above have been addressed.
|
||||||
|
|
||||||
|
**Status: Ready for Review**
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ return [
|
||||||
'create_post' => 'إنشاء مقال',
|
'create_post' => 'إنشاء مقال',
|
||||||
'no_posts' => 'لا توجد مقالات.',
|
'no_posts' => 'لا توجد مقالات.',
|
||||||
'search_placeholder' => 'البحث في المقالات...',
|
'search_placeholder' => 'البحث في المقالات...',
|
||||||
|
'search_results' => 'تم العثور على :count نتيجة لـ ":query"',
|
||||||
|
'no_results' => 'لم يتم العثور على نتائج لـ ":query"',
|
||||||
|
'clear_search' => 'مسح البحث',
|
||||||
'last_updated' => 'آخر تحديث',
|
'last_updated' => 'آخر تحديث',
|
||||||
'created' => 'تاريخ الإنشاء',
|
'created' => 'تاريخ الإنشاء',
|
||||||
'read_more' => 'اقرأ المزيد',
|
'read_more' => 'اقرأ المزيد',
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ return [
|
||||||
'create_post' => 'Create Post',
|
'create_post' => 'Create Post',
|
||||||
'no_posts' => 'No posts found.',
|
'no_posts' => 'No posts found.',
|
||||||
'search_placeholder' => 'Search posts...',
|
'search_placeholder' => 'Search posts...',
|
||||||
|
'search_results' => 'Found :count results for ":query"',
|
||||||
|
'no_results' => 'No results found for ":query"',
|
||||||
|
'clear_search' => 'Clear search',
|
||||||
'last_updated' => 'Last Updated',
|
'last_updated' => 'Last Updated',
|
||||||
'created' => 'Created',
|
'created' => 'Created',
|
||||||
'read_more' => 'Read More',
|
'read_more' => 'Read More',
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,46 @@ new #[Layout('components.layouts.public')] class extends Component
|
||||||
{
|
{
|
||||||
use WithPagination;
|
use WithPagination;
|
||||||
|
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
public function updatedSearch(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearSearch(): void
|
||||||
|
{
|
||||||
|
$this->search = '';
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function highlightSearch(string $text, string $search): string
|
||||||
|
{
|
||||||
|
if (empty($search)) {
|
||||||
|
return e($text);
|
||||||
|
}
|
||||||
|
|
||||||
|
$escapedText = e($text);
|
||||||
|
$escapedSearch = e($search);
|
||||||
|
|
||||||
|
return preg_replace(
|
||||||
|
'/('.preg_quote($escapedSearch, '/').')/iu',
|
||||||
|
'<mark class="bg-gold/30 rounded px-1">$1</mark>',
|
||||||
|
$escapedText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function with(): array
|
public function with(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'posts' => Post::published()
|
'posts' => Post::published()
|
||||||
|
->when($this->search, function ($query) {
|
||||||
|
$search = $this->search;
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('title', 'like', "%{$search}%")
|
||||||
|
->orWhere('body', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
})
|
||||||
->latest()
|
->latest()
|
||||||
->paginate(10),
|
->paginate(10),
|
||||||
];
|
];
|
||||||
|
|
@ -22,12 +58,50 @@ new #[Layout('components.layouts.public')] class extends Component
|
||||||
<div class="max-w-4xl mx-auto">
|
<div class="max-w-4xl mx-auto">
|
||||||
<flux:heading size="xl" class="text-navy">{{ __('posts.posts') }}</flux:heading>
|
<flux:heading size="xl" class="text-navy">{{ __('posts.posts') }}</flux:heading>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="mt-6 relative">
|
||||||
|
<flux:input
|
||||||
|
wire:model.live.debounce.300ms="search"
|
||||||
|
placeholder="{{ __('posts.search_placeholder') }}"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<x-slot:iconLeading>
|
||||||
|
<flux:icon name="magnifying-glass" class="w-5 h-5 text-charcoal/50" />
|
||||||
|
</x-slot:iconLeading>
|
||||||
|
</flux:input>
|
||||||
|
|
||||||
|
@if($search)
|
||||||
|
<button
|
||||||
|
wire:click="clearSearch"
|
||||||
|
class="absolute {{ app()->getLocale() === 'ar' ? 'left-3' : 'right-3' }} top-1/2 -translate-y-1/2 text-charcoal/50 hover:text-charcoal"
|
||||||
|
>
|
||||||
|
<flux:icon name="x-mark" class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results Info -->
|
||||||
|
@if($search)
|
||||||
|
<p class="mt-4 text-sm text-charcoal/70">
|
||||||
|
@if($posts->total() > 0)
|
||||||
|
{{ __('posts.search_results', ['count' => $posts->total(), 'query' => $search]) }}
|
||||||
|
@else
|
||||||
|
{{ __('posts.no_results', ['query' => $search]) }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Posts List -->
|
||||||
<div class="mt-8 space-y-6">
|
<div class="mt-8 space-y-6">
|
||||||
@forelse($posts as $post)
|
@forelse($posts as $post)
|
||||||
<article wire:key="post-{{ $post->id }}" class="bg-white p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
<article wire:key="post-{{ $post->id }}" class="bg-white p-6 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||||
<h2 class="text-xl font-semibold text-navy">
|
<h2 class="text-xl font-semibold text-navy">
|
||||||
<a href="{{ route('posts.show', $post) }}" class="hover:text-gold" wire:navigate>
|
<a href="{{ route('posts.show', $post) }}" class="hover:text-gold" wire:navigate>
|
||||||
|
@if($search)
|
||||||
|
{!! $this->highlightSearch($post->getTitle(), $search) !!}
|
||||||
|
@else
|
||||||
{{ $post->getTitle() }}
|
{{ $post->getTitle() }}
|
||||||
|
@endif
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
@ -36,7 +110,11 @@ new #[Layout('components.layouts.public')] class extends Component
|
||||||
</time>
|
</time>
|
||||||
|
|
||||||
<p class="mt-3 text-charcoal">
|
<p class="mt-3 text-charcoal">
|
||||||
|
@if($search)
|
||||||
|
{!! $this->highlightSearch($post->getExcerpt(), $search) !!}
|
||||||
|
@else
|
||||||
{{ $post->getExcerpt() }}
|
{{ $post->getExcerpt() }}
|
||||||
|
@endif
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="{{ route('posts.show', $post) }}" class="text-gold hover:underline mt-4 inline-block" wire:navigate>
|
<a href="{{ route('posts.show', $post) }}" class="text-gold hover:underline mt-4 inline-block" wire:navigate>
|
||||||
|
|
@ -44,9 +122,17 @@ new #[Layout('components.layouts.public')] class extends Component
|
||||||
</a>
|
</a>
|
||||||
</article>
|
</article>
|
||||||
@empty
|
@empty
|
||||||
<div class="text-center text-charcoal/70 py-12 bg-white rounded-lg">
|
<div class="text-center py-12 bg-white rounded-lg">
|
||||||
|
@if($search)
|
||||||
|
<flux:icon name="magnifying-glass" class="w-12 h-12 mx-auto mb-4 text-charcoal/30" />
|
||||||
|
<p class="text-charcoal/70">{{ __('posts.no_results', ['query' => $search]) }}</p>
|
||||||
|
<flux:button wire:click="clearSearch" class="mt-4">
|
||||||
|
{{ __('posts.clear_search') }}
|
||||||
|
</flux:button>
|
||||||
|
@else
|
||||||
<flux:icon name="document-text" class="w-12 h-12 mx-auto mb-4 text-charcoal/40" />
|
<flux:icon name="document-text" class="w-12 h-12 mx-auto mb-4 text-charcoal/40" />
|
||||||
<p>{{ __('posts.no_posts') }}</p>
|
<p class="text-charcoal/70">{{ __('posts.no_posts') }}</p>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -153,3 +153,155 @@ test('individual post page displays content in current locale', function () {
|
||||||
->assertSee('عنوان عربي')
|
->assertSee('عنوان عربي')
|
||||||
->assertSee('محتوى عربي');
|
->assertSee('محتوى عربي');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Search functionality tests
|
||||||
|
|
||||||
|
test('search input is displayed on posts listing page', function () {
|
||||||
|
$this->withSession(['locale' => 'en'])
|
||||||
|
->get(route('posts.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Search posts...');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search filters posts by English title', function () {
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Legal Rights Guide', 'ar' => 'دليل الحقوق القانونية'],
|
||||||
|
]);
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Tax Information', 'ar' => 'معلومات ضريبية'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
|
||||||
|
// Title is highlighted, so we check for parts that won't be in the mark tag
|
||||||
|
// "Legal" will be in <mark>, "Rights Guide" will be after
|
||||||
|
Livewire\Volt\Volt::test('pages.posts.index')
|
||||||
|
->set('search', 'Legal')
|
||||||
|
->assertSee('Rights Guide') // Part after the highlighted word
|
||||||
|
->assertSee('Legal') // The highlighted word (inside <mark>)
|
||||||
|
->assertDontSee('Tax Information');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search filters posts by Arabic title', function () {
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Legal Rights Guide', 'ar' => 'دليل الحقوق القانونية'],
|
||||||
|
]);
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Tax Information', 'ar' => 'معلومات ضريبية'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('ar');
|
||||||
|
|
||||||
|
// Search for "الحقوق" (rights) - should find first post only
|
||||||
|
$component = Livewire\Volt\Volt::test('pages.posts.index')
|
||||||
|
->set('search', 'الحقوق');
|
||||||
|
|
||||||
|
// Check we have results (not "no results")
|
||||||
|
$html = $component->html();
|
||||||
|
expect(str_contains($html, 'تم العثور'))->toBeTrue('Should show found results message');
|
||||||
|
|
||||||
|
// Check the highlighted word appears
|
||||||
|
$component->assertSee('الحقوق') // The highlighted word
|
||||||
|
->assertDontSee('ضريبية'); // Part of second post
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search filters posts by body content', function () {
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'First Article', 'ar' => 'المقال الأول'],
|
||||||
|
'body' => ['en' => 'This discusses property law topics.', 'ar' => 'هذا يناقش مواضيع قانون الملكية.'],
|
||||||
|
]);
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Second Article', 'ar' => 'المقال الثاني'],
|
||||||
|
'body' => ['en' => 'This is about family matters.', 'ar' => 'هذا يتعلق بشؤون الأسرة.'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
|
||||||
|
// Searching body content - title doesn't contain 'property' so won't be highlighted
|
||||||
|
Livewire\Volt\Volt::test('pages.posts.index')
|
||||||
|
->set('search', 'property')
|
||||||
|
->assertSee('First Article') // This title has no highlight since match is in body
|
||||||
|
->assertDontSee('Second Article');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search shows no results message when no matches found', function () {
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Test Post', 'ar' => 'مقال اختبار'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
|
||||||
|
Livewire\Volt\Volt::test('pages.posts.index')
|
||||||
|
->set('search', 'nonexistent')
|
||||||
|
->assertSee('No results found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search only searches published posts', function () {
|
||||||
|
Post::factory()->draft()->create([
|
||||||
|
'title' => ['en' => 'Draft Article', 'ar' => 'مقال مسودة'],
|
||||||
|
]);
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Published Article', 'ar' => 'مقال منشور'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
|
||||||
|
// Search for "Article" - both have it in title, but only published should appear
|
||||||
|
// "Article" will be highlighted in "Published Article"
|
||||||
|
Livewire\Volt\Volt::test('pages.posts.index')
|
||||||
|
->set('search', 'Article')
|
||||||
|
->assertSee('Published') // Part of the title without highlight
|
||||||
|
->assertSee('Article') // The highlighted word
|
||||||
|
->assertDontSee('Draft');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clear search button resets search and shows all posts', function () {
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Alpha Article', 'ar' => 'المقال الأول'],
|
||||||
|
]);
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Beta Article', 'ar' => 'المقال الثاني'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
|
||||||
|
Livewire\Volt\Volt::test('pages.posts.index')
|
||||||
|
->set('search', 'Alpha')
|
||||||
|
->assertSee('Alpha') // Highlighted in "Alpha Article"
|
||||||
|
->assertDontSee('Beta')
|
||||||
|
->call('clearSearch')
|
||||||
|
->assertSet('search', '')
|
||||||
|
->assertSee('Alpha Article') // No highlighting after clear
|
||||||
|
->assertSee('Beta Article');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search is case insensitive', function () {
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'UPPERCASE TITLE', 'ar' => 'عنوان كبير'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
|
||||||
|
// Search lowercase "uppercase" should find "UPPERCASE TITLE"
|
||||||
|
// Note: "uppercase" will be highlighted (case preserved in original)
|
||||||
|
Livewire\Volt\Volt::test('pages.posts.index')
|
||||||
|
->set('search', 'uppercase')
|
||||||
|
->assertSee('UPPERCASE') // The highlighted word
|
||||||
|
->assertSee('TITLE'); // Rest of title
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search resets pagination', function () {
|
||||||
|
Post::factory()->count(15)->published()->create();
|
||||||
|
Post::factory()->published()->create([
|
||||||
|
'title' => ['en' => 'Special Searchable Post', 'ar' => 'مقال قابل للبحث خاص'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire\Volt\Volt::test('pages.posts.index');
|
||||||
|
|
||||||
|
// Navigate to page 2
|
||||||
|
$component->call('gotoPage', 2);
|
||||||
|
|
||||||
|
// Set search - should reset to page 1
|
||||||
|
$component->set('search', 'Special')
|
||||||
|
->assertSet('paginators.page', 1);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue