php-admin/tests/Feature/Modal/ActivityLogModalTest.php
Claude d1afc5592a
test: add tests for admin modal components (Settings, PlatformUser, ActivityLog, ServiceManager)
Adds Pest test suites for the four critical admin modals using test double
components, following the existing LivewireModalTest.php pattern. Covers
tab navigation, form validation, CRUD flows, filter combinations, and
state management. Fixes #7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:51:57 +00:00

483 lines
16 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
use Livewire\Attributes\Computed;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\Livewire;
/**
* Tests for ActivityLog modal component patterns.
*
* These tests verify the ActivityLog behaviour using test doubles:
* search filtering, log name filtering, event filtering, pagination
* reset on filter changes, clearing filters, and activity item mapping.
*/
// =============================================================================
// Test Double Components
// =============================================================================
/**
* ActivityLog-style component with search, filters, and computed data.
*/
class ActivityLogModalDouble extends Component
{
#[Url]
public string $search = '';
#[Url]
public string $logName = '';
#[Url]
public string $event = '';
public int $page = 1;
// Simulated activity data
public array $activityData = [];
public function mount(): void
{
// Seed with sample activity data
$this->activityData = [
[
'id' => 1,
'description' => 'Created new workspace',
'log_name' => 'workspace',
'event' => 'created',
'causer_name' => 'John Doe',
'subject_type' => 'Workspace',
'subject_name' => 'My Project',
'changes' => null,
'timestamp' => '2026-03-20 10:00:00',
],
[
'id' => 2,
'description' => 'Updated user profile',
'log_name' => 'user',
'event' => 'updated',
'causer_name' => 'Jane Smith',
'subject_type' => 'User',
'subject_name' => 'Jane Smith',
'changes' => ['old' => ['name' => 'Jane'], 'new' => ['name' => 'Jane Smith']],
'timestamp' => '2026-03-21 14:30:00',
],
[
'id' => 3,
'description' => 'Deleted old service',
'log_name' => 'service',
'event' => 'deleted',
'causer_name' => 'Admin',
'subject_type' => 'Service',
'subject_name' => 'Legacy API',
'changes' => null,
'timestamp' => '2026-03-22 09:15:00',
],
[
'id' => 4,
'description' => 'Updated workspace settings',
'log_name' => 'workspace',
'event' => 'updated',
'causer_name' => 'John Doe',
'subject_type' => 'Workspace',
'subject_name' => 'My Project',
'changes' => ['old' => ['public' => false], 'new' => ['public' => true]],
'timestamp' => '2026-03-23 11:45:00',
],
[
'id' => 5,
'description' => 'Created new user account',
'log_name' => 'user',
'event' => 'created',
'causer_name' => 'System',
'subject_type' => 'User',
'subject_name' => 'New User',
'changes' => null,
'timestamp' => '2026-03-24 08:00:00',
],
];
}
public function clearFilters(): void
{
$this->search = '';
$this->logName = '';
$this->event = '';
$this->page = 1;
}
#[Computed]
public function logNames(): array
{
return array_values(array_unique(array_column($this->activityData, 'log_name')));
}
#[Computed]
public function events(): array
{
return array_values(array_unique(array_column($this->activityData, 'event')));
}
#[Computed]
public function logNameOptions(): array
{
$options = ['' => 'All logs'];
foreach ($this->logNames as $name) {
$options[$name] = ucfirst($name);
}
return $options;
}
#[Computed]
public function eventOptions(): array
{
$options = ['' => 'All events'];
foreach ($this->events as $eventName) {
$options[$eventName] = ucfirst($eventName);
}
return $options;
}
#[Computed]
public function filteredActivities(): array
{
$result = $this->activityData;
if ($this->logName) {
$result = array_filter($result, fn ($a) => $a['log_name'] === $this->logName);
}
if ($this->event) {
$result = array_filter($result, fn ($a) => $a['event'] === $this->event);
}
if ($this->search) {
$search = strtolower($this->search);
$result = array_filter($result, fn ($a) => str_contains(strtolower($a['description']), $search));
}
return array_values($result);
}
#[Computed]
public function activityItems(): array
{
return array_map(function ($activity) {
$item = [
'description' => $activity['description'],
'event' => $activity['event'],
'timestamp' => $activity['timestamp'],
];
if ($activity['causer_name']) {
$item['actor'] = [
'name' => $activity['causer_name'],
'initials' => substr($activity['causer_name'], 0, 1),
];
}
if ($activity['subject_type']) {
$item['subject'] = [
'type' => $activity['subject_type'],
'name' => $activity['subject_name'],
];
}
if ($activity['changes']) {
$item['changes'] = $activity['changes'];
}
return $item;
}, $this->filteredActivities);
}
public function render(): string
{
return <<<'HTML'
<div>
<input wire:model="search" placeholder="Search..." />
<span>Results: {{ count($this->filteredActivities) }}</span>
<span>Log: {{ $logName ?: 'all' }}</span>
<span>Event: {{ $event ?: 'all' }}</span>
@foreach($this->activityItems as $item)
<div class="activity-item">{{ $item['description'] }}</div>
@endforeach
</div>
HTML;
}
}
// =============================================================================
// Initial State Tests
// =============================================================================
describe('ActivityLog initial state', function () {
it('starts with empty filters', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet('search', '')
->assertSet('logName', '')
->assertSet('event', '');
});
it('loads activity data on mount', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet('activityData', fn ($data) => count($data) === 5);
});
it('shows all activities initially', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSee('Results: 5');
});
});
// =============================================================================
// Search Filter Tests
// =============================================================================
describe('ActivityLog search filtering', function () {
it('filters by search term in description', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'workspace')
->assertSee('Results: 2')
->assertSee('Created new workspace')
->assertSee('Updated workspace settings');
});
it('search is case-insensitive', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'WORKSPACE')
->assertSee('Results: 2');
});
it('returns no results for non-matching search', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'nonexistent-term')
->assertSee('Results: 0');
});
it('shows all results when search is cleared', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'workspace')
->assertSee('Results: 2')
->set('search', '')
->assertSee('Results: 5');
});
it('filters by partial description match', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'user')
->assertSee('Results: 2');
});
});
// =============================================================================
// Log Name Filter Tests
// =============================================================================
describe('ActivityLog log name filtering', function () {
it('filters by workspace log', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('logName', 'workspace')
->assertSee('Results: 2')
->assertSee('Log: workspace');
});
it('filters by user log', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('logName', 'user')
->assertSee('Results: 2');
});
it('filters by service log', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('logName', 'service')
->assertSee('Results: 1');
});
it('shows all logs when filter is empty', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('logName', '')
->assertSee('Results: 5')
->assertSee('Log: all');
});
it('provides log name options with All logs default', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => $component->logNameOptions[''] === 'All logs'
&& array_key_exists('workspace', $component->logNameOptions)
&& array_key_exists('user', $component->logNameOptions)
&& array_key_exists('service', $component->logNameOptions));
});
it('returns distinct log names', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => count($component->logNames) === 3);
});
});
// =============================================================================
// Event Filter Tests
// =============================================================================
describe('ActivityLog event filtering', function () {
it('filters by created events', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('event', 'created')
->assertSee('Results: 2')
->assertSee('Event: created');
});
it('filters by updated events', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('event', 'updated')
->assertSee('Results: 2');
});
it('filters by deleted events', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('event', 'deleted')
->assertSee('Results: 1');
});
it('shows all events when filter is empty', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('event', '')
->assertSee('Results: 5')
->assertSee('Event: all');
});
it('provides event options with All events default', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => $component->eventOptions[''] === 'All events'
&& array_key_exists('created', $component->eventOptions)
&& array_key_exists('updated', $component->eventOptions)
&& array_key_exists('deleted', $component->eventOptions));
});
it('returns distinct event types', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => count($component->events) === 3);
});
});
// =============================================================================
// Combined Filter Tests
// =============================================================================
describe('ActivityLog combined filters', function () {
it('combines log name and event filters', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('logName', 'workspace')
->set('event', 'created')
->assertSee('Results: 1')
->assertSee('Created new workspace');
});
it('combines search with log name filter', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'settings')
->set('logName', 'workspace')
->assertSee('Results: 1')
->assertSee('Updated workspace settings');
});
it('combines all three filters', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'workspace')
->set('logName', 'workspace')
->set('event', 'updated')
->assertSee('Results: 1');
});
it('returns empty when combined filters exclude everything', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('logName', 'service')
->set('event', 'created')
->assertSee('Results: 0');
});
});
// =============================================================================
// Clear Filters Tests
// =============================================================================
describe('ActivityLog clear filters', function () {
it('clears all filters at once', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('search', 'workspace')
->set('logName', 'workspace')
->set('event', 'created')
->call('clearFilters')
->assertSet('search', '')
->assertSet('logName', '')
->assertSet('event', '')
->assertSee('Results: 5');
});
it('resets page number when clearing filters', function () {
Livewire::test(ActivityLogModalDouble::class)
->set('page', 3)
->call('clearFilters')
->assertSet('page', 1);
});
});
// =============================================================================
// Activity Item Mapping Tests
// =============================================================================
describe('ActivityLog item mapping', function () {
it('maps activity items with actor information', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => $component->activityItems[0]['actor']['name'] === 'John Doe'
&& $component->activityItems[0]['actor']['initials'] === 'J');
});
it('maps activity items with subject information', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => $component->activityItems[0]['subject']['type'] === 'Workspace'
&& $component->activityItems[0]['subject']['name'] === 'My Project');
});
it('includes changes diff when present', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => isset($component->activityItems[1]['changes'])
&& $component->activityItems[1]['changes']['old']['name'] === 'Jane'
&& $component->activityItems[1]['changes']['new']['name'] === 'Jane Smith');
});
it('omits changes when not present', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => ! isset($component->activityItems[0]['changes']));
});
it('includes event type in each item', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => $component->activityItems[0]['event'] === 'created'
&& $component->activityItems[1]['event'] === 'updated'
&& $component->activityItems[2]['event'] === 'deleted');
});
it('includes timestamp in each item', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSet(fn ($component) => $component->activityItems[0]['timestamp'] === '2026-03-20 10:00:00');
});
it('renders activity descriptions in view', function () {
Livewire::test(ActivityLogModalDouble::class)
->assertSee('Created new workspace')
->assertSee('Updated user profile')
->assertSee('Deleted old service');
});
});