diff --git a/tests/Feature/Modal/ActivityLogModalTest.php b/tests/Feature/Modal/ActivityLogModalTest.php
new file mode 100644
index 0000000..3647dd0
--- /dev/null
+++ b/tests/Feature/Modal/ActivityLogModalTest.php
@@ -0,0 +1,483 @@
+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'
+
+
+
Results: {{ count($this->filteredActivities) }}
+
Log: {{ $logName ?: 'all' }}
+
Event: {{ $event ?: 'all' }}
+ @foreach($this->activityItems as $item)
+
{{ $item['description'] }}
+ @endforeach
+
+ 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');
+ });
+});
diff --git a/tests/Feature/Modal/PlatformUserModalTest.php b/tests/Feature/Modal/PlatformUserModalTest.php
new file mode 100644
index 0000000..c9c7ab3
--- /dev/null
+++ b/tests/Feature/Modal/PlatformUserModalTest.php
@@ -0,0 +1,602 @@
+userId = $id;
+ $this->userName = 'Test User';
+ $this->userEmail = 'test@example.com';
+ $this->editingTier = 'free';
+ $this->editingVerified = false;
+ }
+
+ public function setTab(string $tab): void
+ {
+ if (in_array($tab, ['overview', 'workspaces', 'entitlements', 'data', 'danger'])) {
+ $this->activeTab = $tab;
+ }
+ }
+
+ public function saveTier(): void
+ {
+ $this->actionMessage = "Tier updated to {$this->editingTier}.";
+ $this->actionType = 'success';
+ }
+
+ public function saveVerification(): void
+ {
+ $this->actionMessage = $this->editingVerified
+ ? 'Email marked as verified.'
+ : 'Email verification removed.';
+ $this->actionType = 'success';
+ }
+
+ public function resendVerification(): void
+ {
+ if ($this->editingVerified) {
+ $this->actionMessage = 'User email is already verified.';
+ $this->actionType = 'warning';
+
+ return;
+ }
+
+ $this->actionMessage = 'Verification email sent.';
+ $this->actionType = 'success';
+ }
+
+ public function confirmDelete(bool $immediate = false): void
+ {
+ $this->immediateDelete = $immediate;
+ $this->showDeleteConfirm = true;
+ $this->deleteReason = '';
+ }
+
+ public function cancelDelete(): void
+ {
+ $this->showDeleteConfirm = false;
+ $this->immediateDelete = false;
+ $this->deleteReason = '';
+ }
+
+ public function scheduleDelete(): void
+ {
+ if ($this->immediateDelete) {
+ $this->actionMessage = 'User has been permanently deleted.';
+ $this->actionType = 'success';
+ } else {
+ $this->actionMessage = 'Account deletion scheduled. Will be deleted in 7 days unless cancelled.';
+ $this->actionType = 'warning';
+ }
+
+ $this->showDeleteConfirm = false;
+ }
+
+ public function anonymizeUser(): void
+ {
+ $this->userName = 'Anonymized User';
+ $this->userEmail = "anon_{$this->userId}@anonymized.local";
+ $this->editingTier = 'free';
+ $this->editingVerified = false;
+
+ $this->actionMessage = 'User data has been anonymized.';
+ $this->actionType = 'success';
+ }
+
+ public function openPackageModal(int $workspaceId): void
+ {
+ $this->selectedWorkspaceId = $workspaceId;
+ $this->selectedPackageCode = '';
+ $this->showPackageModal = true;
+ }
+
+ public function closePackageModal(): void
+ {
+ $this->showPackageModal = false;
+ $this->selectedWorkspaceId = null;
+ $this->selectedPackageCode = '';
+ }
+
+ public function provisionPackage(): void
+ {
+ if (! $this->selectedWorkspaceId || ! $this->selectedPackageCode) {
+ $this->actionMessage = 'Please select a workspace and package.';
+ $this->actionType = 'warning';
+
+ return;
+ }
+
+ $this->actionMessage = "Package provisioned to workspace.";
+ $this->actionType = 'success';
+ $this->closePackageModal();
+ }
+
+ public function openEntitlementModal(int $workspaceId): void
+ {
+ $this->entitlementWorkspaceId = $workspaceId;
+ $this->entitlementFeatureCode = '';
+ $this->entitlementType = 'enable';
+ $this->entitlementLimit = null;
+ $this->entitlementDuration = 'permanent';
+ $this->entitlementExpiresAt = null;
+ $this->showEntitlementModal = true;
+ }
+
+ public function closeEntitlementModal(): void
+ {
+ $this->showEntitlementModal = false;
+ $this->entitlementWorkspaceId = null;
+ $this->entitlementFeatureCode = '';
+ }
+
+ public function provisionEntitlement(): void
+ {
+ if (! $this->entitlementWorkspaceId || ! $this->entitlementFeatureCode) {
+ $this->actionMessage = 'Please select a workspace and feature.';
+ $this->actionType = 'warning';
+
+ return;
+ }
+
+ $this->actionMessage = "Entitlement added to workspace.";
+ $this->actionType = 'success';
+ $this->closeEntitlementModal();
+ }
+
+ public function exportUserData(): void
+ {
+ $this->dataExported = true;
+ }
+
+ #[Computed]
+ public function collectUserData(): array
+ {
+ return [
+ 'export_info' => [
+ 'exported_at' => now()->toIso8601String(),
+ 'exported_by' => 'Platform Administrator',
+ ],
+ 'account' => [
+ 'id' => $this->userId,
+ 'name' => $this->userName,
+ 'email' => $this->userEmail,
+ 'tier' => $this->editingTier,
+ ],
+ ];
+ }
+
+ public function render(): string
+ {
+ return <<<'HTML'
+
+
User: {{ $userName }}
+
Tab: {{ $activeTab }}
+
Tier: {{ $editingTier }}
+ @if($showDeleteConfirm)
+
Delete Confirm
+ @endif
+ @if($showPackageModal)
+
Package Modal
+ @endif
+ @if($showEntitlementModal)
+
Entitlement Modal
+ @endif
+ @if($actionMessage)
+
{{ $actionMessage }}
+ @endif
+
+ HTML;
+ }
+}
+
+// =============================================================================
+// Tab Navigation Tests
+// =============================================================================
+
+describe('PlatformUser tab navigation', function () {
+ it('defaults to overview tab', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->assertSet('activeTab', 'overview');
+ });
+
+ it('switches to valid tabs', function () {
+ $validTabs = ['overview', 'workspaces', 'entitlements', 'data', 'danger'];
+
+ foreach ($validTabs as $tab) {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('setTab', $tab)
+ ->assertSet('activeTab', $tab);
+ }
+ });
+
+ it('ignores invalid tab names', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('setTab', 'nonexistent')
+ ->assertSet('activeTab', 'overview');
+ });
+
+ it('renders current tab label', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('setTab', 'workspaces')
+ ->assertSee('Tab: workspaces');
+ });
+});
+
+// =============================================================================
+// Tier Management Tests
+// =============================================================================
+
+describe('PlatformUser tier management', function () {
+ it('loads user tier on mount', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->assertSet('editingTier', 'free');
+ });
+
+ it('can change tier to apollo', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingTier', 'apollo')
+ ->call('saveTier')
+ ->assertSet('actionMessage', 'Tier updated to apollo.')
+ ->assertSet('actionType', 'success');
+ });
+
+ it('can change tier to hades', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingTier', 'hades')
+ ->call('saveTier')
+ ->assertSet('actionMessage', 'Tier updated to hades.')
+ ->assertSet('actionType', 'success');
+ });
+
+ it('renders tier value', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingTier', 'apollo')
+ ->assertSee('Tier: apollo');
+ });
+});
+
+// =============================================================================
+// Verification Management Tests
+// =============================================================================
+
+describe('PlatformUser verification management', function () {
+ it('can mark email as verified', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingVerified', true)
+ ->call('saveVerification')
+ ->assertSet('actionMessage', 'Email marked as verified.')
+ ->assertSet('actionType', 'success');
+ });
+
+ it('can remove email verification', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingVerified', false)
+ ->call('saveVerification')
+ ->assertSet('actionMessage', 'Email verification removed.')
+ ->assertSet('actionType', 'success');
+ });
+
+ it('warns when resending to already verified user', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingVerified', true)
+ ->call('resendVerification')
+ ->assertSet('actionMessage', 'User email is already verified.')
+ ->assertSet('actionType', 'warning');
+ });
+
+ it('sends verification email for unverified user', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingVerified', false)
+ ->call('resendVerification')
+ ->assertSet('actionMessage', 'Verification email sent.')
+ ->assertSet('actionType', 'success');
+ });
+});
+
+// =============================================================================
+// Delete Confirmation Flow Tests
+// =============================================================================
+
+describe('PlatformUser delete confirmation flow', function () {
+ it('opens delete confirmation with scheduled mode', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('confirmDelete', false)
+ ->assertSet('showDeleteConfirm', true)
+ ->assertSet('immediateDelete', false)
+ ->assertSet('deleteReason', '')
+ ->assertSee('Delete Confirm');
+ });
+
+ it('opens delete confirmation with immediate mode', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('confirmDelete', true)
+ ->assertSet('showDeleteConfirm', true)
+ ->assertSet('immediateDelete', true);
+ });
+
+ it('cancels delete confirmation', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('confirmDelete', true)
+ ->set('deleteReason', 'GDPR request')
+ ->call('cancelDelete')
+ ->assertSet('showDeleteConfirm', false)
+ ->assertSet('immediateDelete', false)
+ ->assertSet('deleteReason', '')
+ ->assertDontSee('Delete Confirm');
+ });
+
+ it('schedules deletion (non-immediate)', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('confirmDelete', false)
+ ->set('deleteReason', 'User request')
+ ->call('scheduleDelete')
+ ->assertSet('showDeleteConfirm', false)
+ ->assertSet('actionMessage', 'Account deletion scheduled. Will be deleted in 7 days unless cancelled.')
+ ->assertSet('actionType', 'warning');
+ });
+
+ it('executes immediate deletion', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('confirmDelete', true)
+ ->call('scheduleDelete')
+ ->assertSet('showDeleteConfirm', false)
+ ->assertSet('actionMessage', 'User has been permanently deleted.')
+ ->assertSet('actionType', 'success');
+ });
+});
+
+// =============================================================================
+// Anonymization Tests
+// =============================================================================
+
+describe('PlatformUser anonymization', function () {
+ it('anonymizes user data', function () {
+ Livewire::test(PlatformUserModalDouble::class, ['id' => 42])
+ ->call('anonymizeUser')
+ ->assertSet('userName', 'Anonymized User')
+ ->assertSet('userEmail', 'anon_42@anonymized.local')
+ ->assertSet('editingTier', 'free')
+ ->assertSet('editingVerified', false)
+ ->assertSet('actionType', 'success');
+ });
+
+ it('displays anonymized name after anonymization', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('anonymizeUser')
+ ->assertSee('Anonymized User');
+ });
+});
+
+// =============================================================================
+// Package Modal Tests
+// =============================================================================
+
+describe('PlatformUser package modal', function () {
+ it('opens package modal for a workspace', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openPackageModal', 5)
+ ->assertSet('showPackageModal', true)
+ ->assertSet('selectedWorkspaceId', 5)
+ ->assertSet('selectedPackageCode', '')
+ ->assertSee('Package Modal');
+ });
+
+ it('closes package modal and resets state', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openPackageModal', 5)
+ ->set('selectedPackageCode', 'pro-plan')
+ ->call('closePackageModal')
+ ->assertSet('showPackageModal', false)
+ ->assertSet('selectedWorkspaceId', null)
+ ->assertSet('selectedPackageCode', '')
+ ->assertDontSee('Package Modal');
+ });
+
+ it('warns when provisioning without workspace or package', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openPackageModal', 5)
+ ->call('provisionPackage')
+ ->assertSet('actionMessage', 'Please select a workspace and package.')
+ ->assertSet('actionType', 'warning');
+ });
+
+ it('provisions package and closes modal on success', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openPackageModal', 5)
+ ->set('selectedPackageCode', 'pro-plan')
+ ->call('provisionPackage')
+ ->assertSet('showPackageModal', false)
+ ->assertSet('actionType', 'success');
+ });
+});
+
+// =============================================================================
+// Entitlement Modal Tests
+// =============================================================================
+
+describe('PlatformUser entitlement modal', function () {
+ it('opens entitlement modal with defaults', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openEntitlementModal', 7)
+ ->assertSet('showEntitlementModal', true)
+ ->assertSet('entitlementWorkspaceId', 7)
+ ->assertSet('entitlementFeatureCode', '')
+ ->assertSet('entitlementType', 'enable')
+ ->assertSet('entitlementLimit', null)
+ ->assertSet('entitlementDuration', 'permanent')
+ ->assertSet('entitlementExpiresAt', null)
+ ->assertSee('Entitlement Modal');
+ });
+
+ it('closes entitlement modal and resets state', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openEntitlementModal', 7)
+ ->set('entitlementFeatureCode', 'core.srv.bio')
+ ->call('closeEntitlementModal')
+ ->assertSet('showEntitlementModal', false)
+ ->assertSet('entitlementWorkspaceId', null)
+ ->assertSet('entitlementFeatureCode', '')
+ ->assertDontSee('Entitlement Modal');
+ });
+
+ it('warns when provisioning without workspace or feature', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openEntitlementModal', 7)
+ ->call('provisionEntitlement')
+ ->assertSet('actionMessage', 'Please select a workspace and feature.')
+ ->assertSet('actionType', 'warning');
+ });
+
+ it('provisions entitlement and closes modal on success', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openEntitlementModal', 7)
+ ->set('entitlementFeatureCode', 'core.srv.bio')
+ ->call('provisionEntitlement')
+ ->assertSet('showEntitlementModal', false)
+ ->assertSet('actionType', 'success');
+ });
+
+ it('supports different entitlement types', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openEntitlementModal', 7)
+ ->set('entitlementType', 'add_limit')
+ ->set('entitlementLimit', 100)
+ ->assertSet('entitlementType', 'add_limit')
+ ->assertSet('entitlementLimit', 100);
+ });
+
+ it('supports duration-based entitlements', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->call('openEntitlementModal', 7)
+ ->set('entitlementDuration', 'duration')
+ ->set('entitlementExpiresAt', '2026-12-31')
+ ->assertSet('entitlementDuration', 'duration')
+ ->assertSet('entitlementExpiresAt', '2026-12-31');
+ });
+});
+
+// =============================================================================
+// Data Export Tests
+// =============================================================================
+
+describe('PlatformUser data export', function () {
+ it('collects user data for export', function () {
+ Livewire::test(PlatformUserModalDouble::class, ['id' => 42])
+ ->assertSet(fn ($component) => $component->collectUserData['account']['id'] === 42
+ && $component->collectUserData['account']['name'] === 'Test User'
+ && $component->collectUserData['account']['email'] === 'test@example.com');
+ });
+
+ it('triggers data export', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->assertSet('dataExported', false)
+ ->call('exportUserData')
+ ->assertSet('dataExported', true);
+ });
+});
+
+// =============================================================================
+// Action Message Tests
+// =============================================================================
+
+describe('PlatformUser action messages', function () {
+ it('clears action message between operations', function () {
+ $component = Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingTier', 'apollo')
+ ->call('saveTier')
+ ->assertSet('actionType', 'success');
+
+ // Next operation replaces the message
+ $component->set('editingVerified', true)
+ ->call('resendVerification')
+ ->assertSet('actionType', 'warning')
+ ->assertSet('actionMessage', 'User email is already verified.');
+ });
+
+ it('renders action message in view', function () {
+ Livewire::test(PlatformUserModalDouble::class)
+ ->set('editingTier', 'hades')
+ ->call('saveTier')
+ ->assertSee('Tier updated to hades.');
+ });
+});
diff --git a/tests/Feature/Modal/ServiceManagerModalTest.php b/tests/Feature/Modal/ServiceManagerModalTest.php
new file mode 100644
index 0000000..197873a
--- /dev/null
+++ b/tests/Feature/Modal/ServiceManagerModalTest.php
@@ -0,0 +1,707 @@
+serviceData = [
+ [
+ 'id' => 1,
+ 'code' => 'social',
+ 'module' => 'Core\\Mod\\Social',
+ 'name' => 'Social',
+ 'tagline' => 'Social media management',
+ 'description' => 'Full social media management platform.',
+ 'icon' => 'fa-share-nodes',
+ 'color' => 'blue',
+ 'marketing_domain' => 'social.lthn.sh',
+ 'marketing_url' => 'https://social.lthn.sh',
+ 'docs_url' => 'https://docs.lthn.sh/social',
+ 'is_enabled' => true,
+ 'is_public' => true,
+ 'is_featured' => true,
+ 'entitlement_code' => 'core.srv.social',
+ 'sort_order' => 10,
+ ],
+ [
+ 'id' => 2,
+ 'code' => 'analytics',
+ 'module' => 'Core\\Mod\\Analytics',
+ 'name' => 'Analytics',
+ 'tagline' => 'Privacy-focused analytics',
+ 'description' => 'Website analytics without cookies.',
+ 'icon' => 'fa-chart-line',
+ 'color' => 'cyan',
+ 'marketing_domain' => '',
+ 'marketing_url' => '',
+ 'docs_url' => '',
+ 'is_enabled' => true,
+ 'is_public' => true,
+ 'is_featured' => false,
+ 'entitlement_code' => 'core.srv.analytics',
+ 'sort_order' => 20,
+ ],
+ [
+ 'id' => 3,
+ 'code' => 'legacy-api',
+ 'module' => 'Core\\Mod\\Legacy',
+ 'name' => 'Legacy API',
+ 'tagline' => null,
+ 'description' => null,
+ 'icon' => '',
+ 'color' => '',
+ 'marketing_domain' => '',
+ 'marketing_url' => '',
+ 'docs_url' => '',
+ 'is_enabled' => false,
+ 'is_public' => false,
+ 'is_featured' => false,
+ 'entitlement_code' => '',
+ 'sort_order' => 99,
+ ],
+ ];
+ }
+
+ protected function rules(): array
+ {
+ return [
+ 'name' => ['required', 'string', 'max:100'],
+ 'tagline' => ['nullable', 'string', 'max:200'],
+ 'description' => ['nullable', 'string', 'max:2000'],
+ 'icon' => ['nullable', 'string', 'max:50'],
+ 'color' => ['nullable', 'string', 'max:20'],
+ 'marketing_domain' => ['nullable', 'string', 'max:100'],
+ 'marketing_url' => ['nullable', 'url', 'max:255'],
+ 'docs_url' => ['nullable', 'url', 'max:255'],
+ 'is_enabled' => ['boolean'],
+ 'is_public' => ['boolean'],
+ 'is_featured' => ['boolean'],
+ 'sort_order' => ['integer', 'min:0', 'max:999'],
+ ];
+ }
+
+ public function openEdit(int $id): void
+ {
+ $service = collect($this->serviceData)->firstWhere('id', $id);
+ if (! $service) {
+ return;
+ }
+
+ $this->editingId = $id;
+
+ // Read-only fields
+ $this->code = $service['code'];
+ $this->module = $service['module'];
+ $this->entitlement_code = $service['entitlement_code'] ?? '';
+
+ // Editable fields
+ $this->name = $service['name'];
+ $this->tagline = $service['tagline'] ?? '';
+ $this->description = $service['description'] ?? '';
+ $this->icon = $service['icon'] ?? '';
+ $this->color = $service['color'] ?? '';
+ $this->marketing_domain = $service['marketing_domain'] ?? '';
+ $this->marketing_url = $service['marketing_url'] ?? '';
+ $this->docs_url = $service['docs_url'] ?? '';
+ $this->is_enabled = $service['is_enabled'];
+ $this->is_public = $service['is_public'];
+ $this->is_featured = $service['is_featured'];
+ $this->sort_order = $service['sort_order'];
+
+ $this->showModal = true;
+ }
+
+ public function save(): void
+ {
+ $this->validate();
+
+ // Simulate updating the service in our data array
+ $this->serviceData = collect($this->serviceData)->map(function ($s) {
+ if ($s['id'] === $this->editingId) {
+ $s['name'] = $this->name;
+ $s['tagline'] = $this->tagline ?: null;
+ $s['description'] = $this->description ?: null;
+ $s['icon'] = $this->icon ?: null;
+ $s['color'] = $this->color ?: null;
+ $s['marketing_domain'] = $this->marketing_domain ?: null;
+ $s['marketing_url'] = $this->marketing_url ?: null;
+ $s['docs_url'] = $this->docs_url ?: null;
+ $s['is_enabled'] = $this->is_enabled;
+ $s['is_public'] = $this->is_public;
+ $s['is_featured'] = $this->is_featured;
+ $s['sort_order'] = $this->sort_order;
+ }
+
+ return $s;
+ })->all();
+
+ $this->flashMessages[] = 'Service updated successfully.';
+ $this->closeModal();
+ }
+
+ public function toggleEnabled(int $id): void
+ {
+ $this->serviceData = collect($this->serviceData)->map(function ($s) use ($id) {
+ if ($s['id'] === $id) {
+ $s['is_enabled'] = ! $s['is_enabled'];
+ $status = $s['is_enabled'] ? 'enabled' : 'disabled';
+ $this->flashMessages[] = "{$s['name']} has been {$status}.";
+ }
+
+ return $s;
+ })->all();
+ }
+
+ public function syncFromModules(): void
+ {
+ $this->flashMessages[] = 'Services synced from modules successfully.';
+ }
+
+ public function closeModal(): void
+ {
+ $this->showModal = false;
+ $this->resetForm();
+ }
+
+ protected function resetForm(): void
+ {
+ $this->editingId = null;
+ $this->code = '';
+ $this->module = '';
+ $this->entitlement_code = '';
+ $this->name = '';
+ $this->tagline = '';
+ $this->description = '';
+ $this->icon = '';
+ $this->color = '';
+ $this->marketing_domain = '';
+ $this->marketing_url = '';
+ $this->docs_url = '';
+ $this->is_enabled = true;
+ $this->is_public = true;
+ $this->is_featured = false;
+ $this->sort_order = 50;
+ }
+
+ #[Computed]
+ public function services(): array
+ {
+ return $this->serviceData;
+ }
+
+ #[Computed]
+ public function tableColumns(): array
+ {
+ return [
+ 'Service',
+ 'Code',
+ 'Domain',
+ ['label' => 'Entitlement', 'align' => 'center'],
+ ['label' => 'Status', 'align' => 'center'],
+ ['label' => 'Actions', 'align' => 'center'],
+ ];
+ }
+
+ #[Computed]
+ public function enabledCount(): int
+ {
+ return count(array_filter($this->serviceData, fn ($s) => $s['is_enabled']));
+ }
+
+ #[Computed]
+ public function featuredCount(): int
+ {
+ return count(array_filter($this->serviceData, fn ($s) => $s['is_featured']));
+ }
+
+ public function render(): string
+ {
+ return <<<'HTML'
+
+
Services: {{ count($this->services) }}
+
Enabled: {{ $this->enabledCount }}
+
Featured: {{ $this->featuredCount }}
+ @if($showModal)
+
+
Edit: {{ $name }}
+ Code: {{ $code }}
+ Module: {{ $module }}
+
+ @endif
+ @foreach($flashMessages as $msg)
+
{{ $msg }}
+ @endforeach
+
+ HTML;
+ }
+}
+
+// =============================================================================
+// Initial State Tests
+// =============================================================================
+
+describe('ServiceManager initial state', function () {
+ it('starts with modal closed', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSet('showModal', false)
+ ->assertSet('editingId', null);
+ });
+
+ it('loads service data on mount', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSet('serviceData', fn ($data) => count($data) === 3);
+ });
+
+ it('shows service count', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSee('Services: 3');
+ });
+
+ it('counts enabled services correctly', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSee('Enabled: 2');
+ });
+
+ it('counts featured services correctly', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSee('Featured: 1');
+ });
+
+ it('has correct default form values', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSet('is_enabled', true)
+ ->assertSet('is_public', true)
+ ->assertSet('is_featured', false)
+ ->assertSet('sort_order', 50);
+ });
+});
+
+// =============================================================================
+// Open Edit Modal Tests
+// =============================================================================
+
+describe('ServiceManager edit modal opening', function () {
+ it('opens modal and populates all fields', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->assertSet('showModal', true)
+ ->assertSet('editingId', 1)
+ ->assertSet('name', 'Social')
+ ->assertSet('tagline', 'Social media management')
+ ->assertSet('code', 'social')
+ ->assertSet('module', 'Core\\Mod\\Social')
+ ->assertSet('entitlement_code', 'core.srv.social')
+ ->assertSet('icon', 'fa-share-nodes')
+ ->assertSet('color', 'blue')
+ ->assertSet('marketing_domain', 'social.lthn.sh')
+ ->assertSet('marketing_url', 'https://social.lthn.sh')
+ ->assertSet('docs_url', 'https://docs.lthn.sh/social')
+ ->assertSet('is_enabled', true)
+ ->assertSet('is_public', true)
+ ->assertSet('is_featured', true)
+ ->assertSet('sort_order', 10);
+ });
+
+ it('renders edit modal with service name', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->assertSee('Edit: Social')
+ ->assertSee('Code: social')
+ ->assertSee('Module: Core\\Mod\\Social');
+ });
+
+ it('populates fields for service with empty optional fields', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 3)
+ ->assertSet('showModal', true)
+ ->assertSet('name', 'Legacy API')
+ ->assertSet('tagline', '')
+ ->assertSet('description', '')
+ ->assertSet('icon', '')
+ ->assertSet('marketing_domain', '')
+ ->assertSet('is_enabled', false)
+ ->assertSet('is_public', false)
+ ->assertSet('is_featured', false)
+ ->assertSet('sort_order', 99);
+ });
+
+ it('does nothing for non-existent service id', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 999)
+ ->assertSet('showModal', false)
+ ->assertSet('editingId', null);
+ });
+
+ it('can open different services sequentially', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->assertSet('name', 'Social')
+ ->call('closeModal')
+ ->call('openEdit', 2)
+ ->assertSet('name', 'Analytics')
+ ->assertSet('code', 'analytics');
+ });
+});
+
+// =============================================================================
+// Close Modal and Form Reset Tests
+// =============================================================================
+
+describe('ServiceManager close modal', function () {
+ it('closes modal and resets all form fields', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->call('closeModal')
+ ->assertSet('showModal', false)
+ ->assertSet('editingId', null)
+ ->assertSet('code', '')
+ ->assertSet('module', '')
+ ->assertSet('entitlement_code', '')
+ ->assertSet('name', '')
+ ->assertSet('tagline', '')
+ ->assertSet('description', '')
+ ->assertSet('icon', '')
+ ->assertSet('color', '')
+ ->assertSet('marketing_domain', '')
+ ->assertSet('marketing_url', '')
+ ->assertSet('docs_url', '')
+ ->assertSet('is_enabled', true)
+ ->assertSet('is_public', true)
+ ->assertSet('is_featured', false)
+ ->assertSet('sort_order', 50);
+ });
+
+ it('does not show modal content after closing', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->assertSee('Edit: Social')
+ ->call('closeModal')
+ ->assertDontSee('Edit: Social');
+ });
+});
+
+// =============================================================================
+// Form Validation Tests
+// =============================================================================
+
+describe('ServiceManager form validation', function () {
+ it('validates required name', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('name', '')
+ ->call('save')
+ ->assertHasErrors(['name']);
+ });
+
+ it('validates name max length', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('name', str_repeat('A', 101))
+ ->call('save')
+ ->assertHasErrors(['name']);
+ });
+
+ it('validates tagline max length', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('tagline', str_repeat('A', 201))
+ ->call('save')
+ ->assertHasErrors(['tagline']);
+ });
+
+ it('validates description max length', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('description', str_repeat('A', 2001))
+ ->call('save')
+ ->assertHasErrors(['description']);
+ });
+
+ it('validates marketing_url format', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('marketing_url', 'not-a-url')
+ ->call('save')
+ ->assertHasErrors(['marketing_url']);
+ });
+
+ it('validates docs_url format', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('docs_url', 'invalid')
+ ->call('save')
+ ->assertHasErrors(['docs_url']);
+ });
+
+ it('validates sort_order range', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('sort_order', 1000)
+ ->call('save')
+ ->assertHasErrors(['sort_order']);
+ });
+
+ it('validates sort_order minimum', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('sort_order', -1)
+ ->call('save')
+ ->assertHasErrors(['sort_order']);
+ });
+
+ it('accepts valid form data', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('name', 'Updated Social')
+ ->set('tagline', 'New tagline')
+ ->set('marketing_url', 'https://new.example.com')
+ ->set('docs_url', 'https://docs.example.com')
+ ->set('sort_order', 5)
+ ->call('save')
+ ->assertHasNoErrors();
+ });
+
+ it('accepts nullable optional fields', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('tagline', '')
+ ->set('description', '')
+ ->set('icon', '')
+ ->set('color', '')
+ ->set('marketing_domain', '')
+ ->set('marketing_url', '')
+ ->set('docs_url', '')
+ ->call('save')
+ ->assertHasNoErrors(['tagline', 'description', 'icon', 'color', 'marketing_domain', 'marketing_url', 'docs_url']);
+ });
+});
+
+// =============================================================================
+// Save and Update Tests
+// =============================================================================
+
+describe('ServiceManager save', function () {
+ it('updates service data on save', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('name', 'Social Pro')
+ ->set('is_featured', false)
+ ->call('save')
+ ->assertHasNoErrors()
+ ->assertSet('showModal', false)
+ ->assertSet('serviceData', fn ($data) => $data[0]['name'] === 'Social Pro'
+ && $data[0]['is_featured'] === false);
+ });
+
+ it('records success message on save', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->call('save')
+ ->assertSee('Service updated successfully.');
+ });
+
+ it('closes modal after successful save', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->call('save')
+ ->assertSet('showModal', false)
+ ->assertSet('editingId', null);
+ });
+
+ it('preserves other services when updating one', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('name', 'Updated Name')
+ ->call('save')
+ ->assertSet('serviceData', fn ($data) => $data[1]['name'] === 'Analytics'
+ && $data[2]['name'] === 'Legacy API');
+ });
+});
+
+// =============================================================================
+// Toggle Enabled Tests
+// =============================================================================
+
+describe('ServiceManager toggle enabled', function () {
+ it('disables an enabled service', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('toggleEnabled', 1)
+ ->assertSet('serviceData', fn ($data) => $data[0]['is_enabled'] === false)
+ ->assertSee('Social has been disabled.');
+ });
+
+ it('enables a disabled service', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('toggleEnabled', 3)
+ ->assertSet('serviceData', fn ($data) => $data[2]['is_enabled'] === true)
+ ->assertSee('Legacy API has been enabled.');
+ });
+
+ it('toggles without opening the modal', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('toggleEnabled', 1)
+ ->assertSet('showModal', false);
+ });
+
+ it('can toggle the same service twice', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('toggleEnabled', 1)
+ ->assertSet('serviceData', fn ($data) => $data[0]['is_enabled'] === false)
+ ->call('toggleEnabled', 1)
+ ->assertSet('serviceData', fn ($data) => $data[0]['is_enabled'] === true);
+ });
+
+ it('updates enabled count after toggling', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSee('Enabled: 2')
+ ->call('toggleEnabled', 1)
+ ->assertSee('Enabled: 1');
+ });
+});
+
+// =============================================================================
+// Sync From Modules Tests
+// =============================================================================
+
+describe('ServiceManager sync from modules', function () {
+ it('records sync success message', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('syncFromModules')
+ ->assertSee('Services synced from modules successfully.');
+ });
+});
+
+// =============================================================================
+// Table Structure Tests
+// =============================================================================
+
+describe('ServiceManager table structure', function () {
+ it('has correct table columns', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSet(fn ($component) => count($component->tableColumns) === 6
+ && $component->tableColumns[0] === 'Service'
+ && $component->tableColumns[1] === 'Code'
+ && $component->tableColumns[2] === 'Domain');
+ });
+
+ it('has alignment config for status and actions columns', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->assertSet(fn ($component) => $component->tableColumns[4]['label'] === 'Status'
+ && $component->tableColumns[4]['align'] === 'center'
+ && $component->tableColumns[5]['label'] === 'Actions'
+ && $component->tableColumns[5]['align'] === 'center');
+ });
+});
+
+// =============================================================================
+// Edge Cases
+// =============================================================================
+
+describe('ServiceManager edge cases', function () {
+ it('handles editing fields while modal is open', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('name', 'Changed')
+ ->assertSet('name', 'Changed')
+ ->assertSet('editingId', 1) // Still editing same service
+ ->assertSet('code', 'social'); // Read-only unchanged
+ });
+
+ it('handles rapid open/close without saving', function () {
+ $component = Livewire::test(ServiceManagerModalDouble::class);
+
+ for ($i = 0; $i < 5; $i++) {
+ $component->call('openEdit', 1)
+ ->assertSet('showModal', true)
+ ->call('closeModal')
+ ->assertSet('showModal', false);
+ }
+
+ // Original data should be unchanged
+ $component->assertSet('serviceData', fn ($data) => $data[0]['name'] === 'Social');
+ });
+
+ it('preserves service data across multiple edits', function () {
+ Livewire::test(ServiceManagerModalDouble::class)
+ ->call('openEdit', 1)
+ ->set('name', 'Social v2')
+ ->call('save')
+ ->call('openEdit', 2)
+ ->set('name', 'Analytics v2')
+ ->call('save')
+ ->assertSet('serviceData', fn ($data) => $data[0]['name'] === 'Social v2'
+ && $data[1]['name'] === 'Analytics v2'
+ && $data[2]['name'] === 'Legacy API');
+ });
+});
diff --git a/tests/Feature/Modal/SettingsModalTest.php b/tests/Feature/Modal/SettingsModalTest.php
new file mode 100644
index 0000000..af2c26b
--- /dev/null
+++ b/tests/Feature/Modal/SettingsModalTest.php
@@ -0,0 +1,450 @@
+name = 'Test User';
+ $this->email = 'test@example.com';
+ }
+
+ public function updateProfile(): void
+ {
+ $this->validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => ['required', 'email', 'max:255'],
+ ]);
+
+ $this->dispatched[] = 'profile-updated';
+ $this->toasts[] = ['text' => 'Profile updated successfully.', 'variant' => 'success'];
+ }
+
+ public function updatePreferences(): void
+ {
+ $this->validate([
+ 'locale' => ['required', 'string'],
+ 'timezone' => ['required', 'timezone'],
+ 'time_format' => ['required', 'in:12,24'],
+ 'week_starts_on' => ['required', 'in:0,1'],
+ ]);
+
+ $this->dispatched[] = 'preferences-updated';
+ $this->toasts[] = ['text' => 'Preferences updated.', 'variant' => 'success'];
+ }
+
+ public function updatePassword(): void
+ {
+ $this->validate([
+ 'current_password' => ['required'],
+ 'new_password' => ['required', 'confirmed', 'min:8'],
+ ]);
+
+ $this->current_password = '';
+ $this->new_password = '';
+ $this->new_password_confirmation = '';
+
+ $this->dispatched[] = 'password-updated';
+ $this->toasts[] = ['text' => 'Password updated.', 'variant' => 'success'];
+ }
+
+ public function enableTwoFactor(): void
+ {
+ $this->toasts[] = ['text' => 'Two-factor upgrading.', 'variant' => 'warning'];
+ }
+
+ public function requestAccountDeletion(): void
+ {
+ $this->showDeleteConfirmation = false;
+ $this->deleteReason = '';
+ $this->toasts[] = ['text' => 'Deletion scheduled.', 'variant' => 'warning'];
+ }
+
+ public function cancelAccountDeletion(): void
+ {
+ $this->toasts[] = ['text' => 'Deletion cancelled.', 'variant' => 'success'];
+ }
+
+ public function render(): string
+ {
+ return <<<'HTML'
+
+
+
Section: {{ $activeSection }}
+ @if($showDeleteConfirmation)
+
Delete Confirmation
+ @endif
+
+ HTML;
+ }
+}
+
+// =============================================================================
+// Tab Navigation Tests
+// =============================================================================
+
+describe('Settings tab navigation', function () {
+ it('defaults to profile section', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->assertSet('activeSection', 'profile');
+ });
+
+ it('switches to preferences section', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('activeSection', 'preferences')
+ ->assertSet('activeSection', 'preferences');
+ });
+
+ it('switches to security section', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('activeSection', 'security')
+ ->assertSet('activeSection', 'security');
+ });
+
+ it('switches to danger zone section', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('activeSection', 'danger')
+ ->assertSet('activeSection', 'danger');
+ });
+
+ it('renders active section label', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('activeSection', 'preferences')
+ ->assertSee('Section: preferences');
+ });
+
+ it('cycles through all sections', function () {
+ $component = Livewire::test(SettingsModalDouble::class);
+
+ foreach (['profile', 'preferences', 'security', 'danger'] as $section) {
+ $component->set('activeSection', $section)
+ ->assertSet('activeSection', $section);
+ }
+ });
+});
+
+// =============================================================================
+// Profile Update Tests
+// =============================================================================
+
+describe('Settings profile update', function () {
+ it('loads initial profile data on mount', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->assertSet('name', 'Test User')
+ ->assertSet('email', 'test@example.com');
+ });
+
+ it('validates required name', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('name', '')
+ ->call('updateProfile')
+ ->assertHasErrors(['name']);
+ });
+
+ it('validates required email', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('email', '')
+ ->call('updateProfile')
+ ->assertHasErrors(['email']);
+ });
+
+ it('validates email format', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('email', 'not-an-email')
+ ->call('updateProfile')
+ ->assertHasErrors(['email']);
+ });
+
+ it('validates name max length', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('name', str_repeat('A', 256))
+ ->call('updateProfile')
+ ->assertHasErrors(['name']);
+ });
+
+ it('succeeds with valid profile data', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('name', 'Jane Doe')
+ ->set('email', 'jane@example.com')
+ ->call('updateProfile')
+ ->assertHasNoErrors()
+ ->assertSet('dispatched', ['profile-updated']);
+ });
+
+ it('records success toast on profile update', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('name', 'Jane Doe')
+ ->set('email', 'jane@example.com')
+ ->call('updateProfile')
+ ->assertSet('toasts', fn ($toasts) => count($toasts) === 1
+ && $toasts[0]['variant'] === 'success');
+ });
+});
+
+// =============================================================================
+// Preferences Update Tests
+// =============================================================================
+
+describe('Settings preferences update', function () {
+ it('has correct default preferences', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->assertSet('locale', 'en_GB')
+ ->assertSet('timezone', 'Europe/London')
+ ->assertSet('time_format', 12)
+ ->assertSet('week_starts_on', 1);
+ });
+
+ it('validates timezone', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('timezone', 'Invalid/Zone')
+ ->call('updatePreferences')
+ ->assertHasErrors(['timezone']);
+ });
+
+ it('validates time_format values', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('time_format', 99)
+ ->call('updatePreferences')
+ ->assertHasErrors(['time_format']);
+ });
+
+ it('validates week_starts_on values', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('week_starts_on', 5)
+ ->call('updatePreferences')
+ ->assertHasErrors(['week_starts_on']);
+ });
+
+ it('accepts valid 24-hour format', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('time_format', 24)
+ ->call('updatePreferences')
+ ->assertHasNoErrors(['time_format']);
+ });
+
+ it('accepts Monday start (1) and Sunday start (0)', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('week_starts_on', 0)
+ ->call('updatePreferences')
+ ->assertHasNoErrors(['week_starts_on']);
+
+ Livewire::test(SettingsModalDouble::class)
+ ->set('week_starts_on', 1)
+ ->call('updatePreferences')
+ ->assertHasNoErrors(['week_starts_on']);
+ });
+
+ it('dispatches preferences-updated on success', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('locale', 'en_US')
+ ->set('timezone', 'America/New_York')
+ ->set('time_format', 24)
+ ->set('week_starts_on', 0)
+ ->call('updatePreferences')
+ ->assertHasNoErrors()
+ ->assertSet('dispatched', ['preferences-updated']);
+ });
+});
+
+// =============================================================================
+// Password Change Tests
+// =============================================================================
+
+describe('Settings password change', function () {
+ it('validates current password required', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('current_password', '')
+ ->set('new_password', 'newpass123')
+ ->set('new_password_confirmation', 'newpass123')
+ ->call('updatePassword')
+ ->assertHasErrors(['current_password']);
+ });
+
+ it('validates new password required', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('current_password', 'oldpass')
+ ->set('new_password', '')
+ ->call('updatePassword')
+ ->assertHasErrors(['new_password']);
+ });
+
+ it('validates password confirmation matches', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('current_password', 'oldpass')
+ ->set('new_password', 'newpass123')
+ ->set('new_password_confirmation', 'different')
+ ->call('updatePassword')
+ ->assertHasErrors(['new_password']);
+ });
+
+ it('validates minimum password length', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('current_password', 'oldpass')
+ ->set('new_password', 'short')
+ ->set('new_password_confirmation', 'short')
+ ->call('updatePassword')
+ ->assertHasErrors(['new_password']);
+ });
+
+ it('clears password fields on success', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('current_password', 'oldpassword')
+ ->set('new_password', 'newpassword123')
+ ->set('new_password_confirmation', 'newpassword123')
+ ->call('updatePassword')
+ ->assertHasNoErrors()
+ ->assertSet('current_password', '')
+ ->assertSet('new_password', '')
+ ->assertSet('new_password_confirmation', '');
+ });
+
+ it('dispatches password-updated event on success', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('current_password', 'oldpassword')
+ ->set('new_password', 'newpassword123')
+ ->set('new_password_confirmation', 'newpassword123')
+ ->call('updatePassword')
+ ->assertSet('dispatched', ['password-updated']);
+ });
+});
+
+// =============================================================================
+// Account Deletion Tests
+// =============================================================================
+
+describe('Settings account deletion', function () {
+ it('shows delete confirmation modal', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('showDeleteConfirmation', true)
+ ->assertSee('Delete Confirmation');
+ });
+
+ it('hides delete confirmation by default', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->assertSet('showDeleteConfirmation', false)
+ ->assertDontSee('Delete Confirmation');
+ });
+
+ it('resets state when deletion requested', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->set('showDeleteConfirmation', true)
+ ->set('deleteReason', 'No longer needed')
+ ->call('requestAccountDeletion')
+ ->assertSet('showDeleteConfirmation', false)
+ ->assertSet('deleteReason', '');
+ });
+
+ it('records warning toast for deletion', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->call('requestAccountDeletion')
+ ->assertSet('toasts', fn ($toasts) => $toasts[0]['variant'] === 'warning');
+ });
+
+ it('records success toast for deletion cancellation', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->call('cancelAccountDeletion')
+ ->assertSet('toasts', fn ($toasts) => $toasts[0]['variant'] === 'success');
+ });
+
+ it('has delete account feature enabled by default', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->assertSet('isDeleteAccountEnabled', true);
+ });
+});
+
+// =============================================================================
+// Two-Factor Authentication Tests
+// =============================================================================
+
+describe('Settings two-factor authentication', function () {
+ it('has two-factor disabled by default', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->assertSet('isTwoFactorEnabled', false)
+ ->assertSet('userHasTwoFactorEnabled', false)
+ ->assertSet('showTwoFactorSetup', false);
+ });
+
+ it('records warning toast when enabling two-factor (not yet implemented)', function () {
+ Livewire::test(SettingsModalDouble::class)
+ ->call('enableTwoFactor')
+ ->assertSet('toasts', fn ($toasts) => $toasts[0]['variant'] === 'warning');
+ });
+});