libra/tests/Feature/Admin/TimelineDashboardTest.php

437 lines
13 KiB
PHP

<?php
use App\Enums\TimelineStatus;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\User;
use Livewire\Volt\Volt;
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
});
// ===========================================
// Route Access Tests
// ===========================================
test('admin can view timeline dashboard', function () {
Timeline::factory()->count(5)->create();
$this->actingAs($this->admin)
->get(route('admin.timelines.index'))
->assertOk()
->assertSeeLivewire('admin.timelines.index');
});
test('non-admin cannot access timeline dashboard', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client)
->get(route('admin.timelines.index'))
->assertForbidden();
});
test('guest cannot access timeline dashboard', function () {
$this->get(route('admin.timelines.index'))
->assertRedirect(route('login'));
});
// ===========================================
// List View Tests
// ===========================================
test('timeline dashboard displays all timelines with correct data', function () {
$client = User::factory()->individual()->create(['full_name' => 'Test Client']);
$timeline = Timeline::factory()->create([
'user_id' => $client->id,
'case_name' => 'Test Case Name',
]);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->assertSee('Test Case Name')
->assertSee('Test Client');
});
test('dashboard shows empty state when no timelines exist', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->assertSee(__('timelines.no_timelines'));
});
test('dashboard displays timeline status correctly', function () {
$activeTimeline = Timeline::factory()->active()->create(['case_name' => 'Active Case']);
$archivedTimeline = Timeline::factory()->archived()->create(['case_name' => 'Archived Case']);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->assertSee('Active Case')
->assertSee('Archived Case')
->assertSee(__('enums.timeline_status.active'))
->assertSee(__('enums.timeline_status.archived'));
});
test('dashboard displays update count', function () {
$timeline = Timeline::factory()->create();
TimelineUpdate::factory()->count(3)->create(['timeline_id' => $timeline->id]);
$this->actingAs($this->admin);
$component = Volt::test('admin.timelines.index');
// The component should have the timeline with 3 updates
$timelines = $component->viewData('timelines');
expect($timelines->first()->updates_count)->toBe(3);
});
// ===========================================
// Filtering Tests
// ===========================================
test('can filter timelines by status', function () {
Timeline::factory()->active()->create(['case_name' => 'Active Case']);
Timeline::factory()->archived()->create(['case_name' => 'Archived Case']);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('statusFilter', 'active')
->assertSee('Active Case')
->assertDontSee('Archived Case');
});
test('can filter timelines by client', function () {
$client1 = User::factory()->individual()->create(['full_name' => 'Client One']);
$client2 = User::factory()->individual()->create(['full_name' => 'Client Two']);
Timeline::factory()->create(['user_id' => $client1->id, 'case_name' => 'Client1 Case']);
Timeline::factory()->create(['user_id' => $client2->id, 'case_name' => 'Client2 Case']);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('clientFilter', $client1->id)
->assertSee('Client1 Case')
->assertDontSee('Client2 Case');
});
test('can search timelines by case name', function () {
Timeline::factory()->create(['case_name' => 'Contract Dispute']);
Timeline::factory()->create(['case_name' => 'Property Issue']);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('search', 'Contract')
->assertSee('Contract Dispute')
->assertDontSee('Property Issue');
});
test('can search timelines by case reference', function () {
Timeline::factory()->create(['case_reference' => 'REF-123', 'case_name' => 'Case A']);
Timeline::factory()->create(['case_reference' => 'REF-456', 'case_name' => 'Case B']);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('search', 'REF-123')
->assertSee('Case A')
->assertDontSee('Case B');
});
test('can filter timelines by date range', function () {
$oldTimeline = Timeline::factory()->create([
'case_name' => 'Old Case',
'created_at' => now()->subDays(30),
]);
$newTimeline = Timeline::factory()->create([
'case_name' => 'New Case',
'created_at' => now(),
]);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('dateFrom', now()->subDays(5)->format('Y-m-d'))
->assertSee('New Case')
->assertDontSee('Old Case');
});
test('clear filters resets all filter values', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('search', 'test')
->set('statusFilter', 'active')
->set('clientFilter', '1')
->set('dateFrom', '2024-01-01')
->set('dateTo', '2024-12-31')
->call('clearFilters')
->assertSet('search', '')
->assertSet('statusFilter', '')
->assertSet('clientFilter', '')
->assertSet('dateFrom', '')
->assertSet('dateTo', '');
});
test('shows no results message when filters return empty', function () {
Timeline::factory()->create(['case_name' => 'Existing Case']);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('search', 'NonexistentCase')
->assertSee(__('timelines.no_timelines'));
});
// ===========================================
// Sorting Tests
// ===========================================
test('can sort timelines by case name', function () {
Timeline::factory()->create(['case_name' => 'Zebra Case']);
Timeline::factory()->create(['case_name' => 'Alpha Case']);
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('sort', 'case_name')
->assertSet('sortBy', 'case_name')
->assertSet('sortDir', 'asc');
});
test('clicking same sort column toggles direction', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('sort', 'case_name')
->assertSet('sortDir', 'asc')
->call('sort', 'case_name')
->assertSet('sortDir', 'desc');
});
test('can sort timelines by updated_at', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('sort', 'updated_at')
->assertSet('sortBy', 'updated_at')
->assertSet('sortDir', 'asc');
});
test('can sort timelines by created_at', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('sort', 'created_at')
->assertSet('sortBy', 'created_at')
->assertSet('sortDir', 'asc');
});
test('can sort timelines by client', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('sort', 'user_id')
->assertSet('sortBy', 'user_id')
->assertSet('sortDir', 'asc');
});
test('default sort is by updated_at desc', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->assertSet('sortBy', 'updated_at')
->assertSet('sortDir', 'desc');
});
// ===========================================
// Quick Actions Tests
// ===========================================
test('can archive timeline from dashboard', function () {
$timeline = Timeline::factory()->active()->create();
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('toggleArchive', $timeline->id);
expect($timeline->fresh()->status)->toBe(TimelineStatus::Archived);
});
test('can unarchive timeline from dashboard', function () {
$timeline = Timeline::factory()->archived()->create();
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('toggleArchive', $timeline->id);
expect($timeline->fresh()->status)->toBe(TimelineStatus::Active);
});
test('toggle archive creates admin log entry for archive', function () {
$timeline = Timeline::factory()->active()->create();
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('toggleArchive', $timeline->id);
$this->assertDatabaseHas('admin_logs', [
'admin_id' => $this->admin->id,
'action' => 'archive',
'target_type' => 'timeline',
'target_id' => $timeline->id,
]);
});
test('toggle archive creates admin log entry for unarchive', function () {
$timeline = Timeline::factory()->archived()->create();
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->call('toggleArchive', $timeline->id);
$this->assertDatabaseHas('admin_logs', [
'admin_id' => $this->admin->id,
'action' => 'unarchive',
'target_type' => 'timeline',
'target_id' => $timeline->id,
]);
});
// ===========================================
// Pagination Tests
// ===========================================
test('pagination displays correct number of items', function () {
Timeline::factory()->count(20)->create();
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->assertSet('perPage', 15);
});
test('can change items per page', function () {
$this->actingAs($this->admin);
Volt::test('admin.timelines.index')
->set('perPage', 25)
->assertSet('perPage', 25);
});
test('changing per page resets pagination', function () {
Timeline::factory()->count(30)->create();
$this->actingAs($this->admin);
// Navigate to page 2, then change perPage
$component = Volt::test('admin.timelines.index');
// Simulate being on page 2 then changing perPage
$component->set('perPage', 50);
// After changing perPage, page should reset
$timelines = $component->viewData('timelines');
expect($timelines->currentPage())->toBe(1);
});
// ===========================================
// Eager Loading Tests
// ===========================================
test('dashboard uses eager loading to prevent N+1 queries', function () {
// Create 10 timelines with updates
$timelines = Timeline::factory()->count(10)->create();
foreach ($timelines as $timeline) {
TimelineUpdate::factory()->count(3)->create(['timeline_id' => $timeline->id]);
}
$this->actingAs($this->admin);
// This test verifies the component loads without errors
// The eager loading is verified by checking the query includes relationships
Volt::test('admin.timelines.index')
->assertHasNoErrors();
// Verify that timelines have their relationships loaded
$component = Volt::test('admin.timelines.index');
$loadedTimelines = $component->viewData('timelines');
foreach ($loadedTimelines as $timeline) {
// These relationships should be already loaded (not lazy loaded)
expect($timeline->relationLoaded('user'))->toBeTrue();
expect($timeline->relationLoaded('updates'))->toBeTrue();
}
});
// ===========================================
// Filter Reset on Search Tests
// ===========================================
test('updating search resets pagination', function () {
Timeline::factory()->count(20)->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.timelines.index');
// Change search should reset page
$component->set('search', 'test');
$timelines = $component->viewData('timelines');
expect($timelines->currentPage())->toBe(1);
});
test('updating status filter resets pagination', function () {
Timeline::factory()->count(20)->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.timelines.index');
$component->set('statusFilter', 'active');
$timelines = $component->viewData('timelines');
expect($timelines->currentPage())->toBe(1);
});
test('updating client filter resets pagination', function () {
$client = User::factory()->individual()->create();
Timeline::factory()->count(20)->create(['user_id' => $client->id]);
$this->actingAs($this->admin);
$component = Volt::test('admin.timelines.index');
$component->set('clientFilter', $client->id);
$timelines = $component->viewData('timelines');
expect($timelines->currentPage())->toBe(1);
});
// ===========================================
// Client List Tests
// ===========================================
test('client filter only shows clients with timelines', function () {
$clientWithTimeline = User::factory()->individual()->create(['full_name' => 'Has Timeline']);
$clientWithoutTimeline = User::factory()->individual()->create(['full_name' => 'No Timeline']);
Timeline::factory()->create(['user_id' => $clientWithTimeline->id]);
$this->actingAs($this->admin);
$component = Volt::test('admin.timelines.index');
$clients = $component->viewData('clients');
expect($clients->pluck('id')->toArray())->toContain($clientWithTimeline->id);
expect($clients->pluck('id')->toArray())->not->toContain($clientWithoutTimeline->id);
});