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>
This commit is contained in:
parent
f90cd2c3ec
commit
d1afc5592a
4 changed files with 2242 additions and 0 deletions
483
tests/Feature/Modal/ActivityLogModalTest.php
Normal file
483
tests/Feature/Modal/ActivityLogModalTest.php
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
<?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');
|
||||
});
|
||||
});
|
||||
602
tests/Feature/Modal/PlatformUserModalTest.php
Normal file
602
tests/Feature/Modal/PlatformUserModalTest.php
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
<?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\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* Tests for PlatformUser modal component patterns.
|
||||
*
|
||||
* These tests verify the PlatformUser admin panel behaviour using test doubles:
|
||||
* tab navigation, tier management, verification toggling, delete confirmation
|
||||
* flows, package/entitlement modals, and GDPR data export.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Test Double Components
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* PlatformUser-style component for admin user management.
|
||||
*/
|
||||
class PlatformUserModalDouble extends Component
|
||||
{
|
||||
// User data
|
||||
public int $userId = 0;
|
||||
|
||||
public string $userName = '';
|
||||
|
||||
public string $userEmail = '';
|
||||
|
||||
// Editable fields
|
||||
public string $editingTier = 'free';
|
||||
|
||||
public bool $editingVerified = false;
|
||||
|
||||
// Action state
|
||||
public string $actionMessage = '';
|
||||
|
||||
public string $actionType = '';
|
||||
|
||||
// Tab navigation
|
||||
public string $activeTab = 'overview';
|
||||
|
||||
// Delete confirmation flow
|
||||
public bool $showDeleteConfirm = false;
|
||||
|
||||
public bool $immediateDelete = false;
|
||||
|
||||
public string $deleteReason = '';
|
||||
|
||||
// Package provisioning modal
|
||||
public bool $showPackageModal = false;
|
||||
|
||||
public ?int $selectedWorkspaceId = null;
|
||||
|
||||
public string $selectedPackageCode = '';
|
||||
|
||||
// Entitlement provisioning modal
|
||||
public bool $showEntitlementModal = false;
|
||||
|
||||
public ?int $entitlementWorkspaceId = null;
|
||||
|
||||
public string $entitlementFeatureCode = '';
|
||||
|
||||
public string $entitlementType = 'enable';
|
||||
|
||||
public ?int $entitlementLimit = null;
|
||||
|
||||
public string $entitlementDuration = 'permanent';
|
||||
|
||||
public ?string $entitlementExpiresAt = null;
|
||||
|
||||
// Export tracking
|
||||
public bool $dataExported = false;
|
||||
|
||||
public function mount(int $id = 1): void
|
||||
{
|
||||
$this->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'
|
||||
<div>
|
||||
<h1>User: {{ $userName }}</h1>
|
||||
<span>Tab: {{ $activeTab }}</span>
|
||||
<span>Tier: {{ $editingTier }}</span>
|
||||
@if($showDeleteConfirm)
|
||||
<div class="delete-confirm">Delete Confirm</div>
|
||||
@endif
|
||||
@if($showPackageModal)
|
||||
<div class="package-modal">Package Modal</div>
|
||||
@endif
|
||||
@if($showEntitlementModal)
|
||||
<div class="entitlement-modal">Entitlement Modal</div>
|
||||
@endif
|
||||
@if($actionMessage)
|
||||
<div class="action-message {{ $actionType }}">{{ $actionMessage }}</div>
|
||||
@endif
|
||||
</div>
|
||||
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.');
|
||||
});
|
||||
});
|
||||
707
tests/Feature/Modal/ServiceManagerModalTest.php
Normal file
707
tests/Feature/Modal/ServiceManagerModalTest.php
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
<?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\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* Tests for ServiceManager modal component patterns.
|
||||
*
|
||||
* These tests verify the ServiceManager behaviour using test doubles:
|
||||
* opening/closing the edit modal, form validation, field population
|
||||
* from data, form reset on close, service toggling, table rendering,
|
||||
* and sync operations.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Test Double Components
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* ServiceManager-style component with CRUD modal and table display.
|
||||
*/
|
||||
class ServiceManagerModalDouble extends Component
|
||||
{
|
||||
public bool $showModal = false;
|
||||
|
||||
public ?int $editingId = null;
|
||||
|
||||
// Editable form fields
|
||||
public string $name = '';
|
||||
|
||||
public string $tagline = '';
|
||||
|
||||
public string $description = '';
|
||||
|
||||
public string $icon = '';
|
||||
|
||||
public string $color = '';
|
||||
|
||||
public string $marketing_domain = '';
|
||||
|
||||
public string $marketing_url = '';
|
||||
|
||||
public string $docs_url = '';
|
||||
|
||||
public bool $is_enabled = true;
|
||||
|
||||
public bool $is_public = true;
|
||||
|
||||
public bool $is_featured = false;
|
||||
|
||||
public int $sort_order = 50;
|
||||
|
||||
// Read-only fields
|
||||
public string $code = '';
|
||||
|
||||
public string $module = '';
|
||||
|
||||
public string $entitlement_code = '';
|
||||
|
||||
// Simulated service data
|
||||
public array $serviceData = [];
|
||||
|
||||
// Track actions
|
||||
public array $flashMessages = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->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'
|
||||
<div>
|
||||
<span>Services: {{ count($this->services) }}</span>
|
||||
<span>Enabled: {{ $this->enabledCount }}</span>
|
||||
<span>Featured: {{ $this->featuredCount }}</span>
|
||||
@if($showModal)
|
||||
<div class="edit-modal">
|
||||
<h2>Edit: {{ $name }}</h2>
|
||||
<span>Code: {{ $code }}</span>
|
||||
<span>Module: {{ $module }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@foreach($flashMessages as $msg)
|
||||
<div class="flash">{{ $msg }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
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');
|
||||
});
|
||||
});
|
||||
450
tests/Feature/Modal/SettingsModalTest.php
Normal file
450
tests/Feature/Modal/SettingsModalTest.php
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
<?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\Url;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* Tests for Settings modal component patterns.
|
||||
*
|
||||
* These tests verify the Settings modal behaviour using test doubles:
|
||||
* tabbed navigation, profile updates, password changes, preference
|
||||
* updates, confirmation modals, and form state resets.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Test Double Components
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Settings-style component with tabbed sections and multiple forms.
|
||||
*/
|
||||
class SettingsModalDouble extends Component
|
||||
{
|
||||
#[Url(as: 'tab')]
|
||||
public string $activeSection = 'profile';
|
||||
|
||||
// Profile Info
|
||||
#[Validate('required|string|max:255')]
|
||||
public string $name = '';
|
||||
|
||||
#[Validate('required|email|max:255')]
|
||||
public string $email = '';
|
||||
|
||||
// Preferences
|
||||
public string $locale = 'en_GB';
|
||||
|
||||
public string $timezone = 'Europe/London';
|
||||
|
||||
public int $time_format = 12;
|
||||
|
||||
public int $week_starts_on = 1;
|
||||
|
||||
// Password Change
|
||||
public string $current_password = '';
|
||||
|
||||
public string $new_password = '';
|
||||
|
||||
public string $new_password_confirmation = '';
|
||||
|
||||
// Two-Factor Authentication
|
||||
public bool $showTwoFactorSetup = false;
|
||||
|
||||
public bool $isTwoFactorEnabled = false;
|
||||
|
||||
public bool $userHasTwoFactorEnabled = false;
|
||||
|
||||
// Account Deletion
|
||||
public bool $showDeleteConfirmation = false;
|
||||
|
||||
public string $deleteReason = '';
|
||||
|
||||
public bool $isDeleteAccountEnabled = true;
|
||||
|
||||
// Action tracking
|
||||
public array $dispatched = [];
|
||||
|
||||
public array $toasts = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->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'
|
||||
<div>
|
||||
<nav>
|
||||
<button wire:click="$set('activeSection', 'profile')">Profile</button>
|
||||
<button wire:click="$set('activeSection', 'preferences')">Preferences</button>
|
||||
<button wire:click="$set('activeSection', 'security')">Security</button>
|
||||
<button wire:click="$set('activeSection', 'danger')">Danger Zone</button>
|
||||
</nav>
|
||||
<span>Section: {{ $activeSection }}</span>
|
||||
@if($showDeleteConfirmation)
|
||||
<div class="confirm-modal">Delete Confirmation</div>
|
||||
@endif
|
||||
</div>
|
||||
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');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue