Compare commits
No commits in common. "feat/test-admin-modals" and "dev" have entirely different histories.
feat/test-
...
dev
4 changed files with 0 additions and 2242 deletions
|
|
@ -1,483 +0,0 @@
|
|||
<?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');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,602 +0,0 @@
|
|||
<?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.');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,707 +0,0 @@
|
|||
<?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');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,450 +0,0 @@
|
|||
<?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