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>
450 lines
15 KiB
PHP
450 lines
15 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Core PHP Framework
|
|
*
|
|
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
|
* See LICENSE file for details.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Livewire\Attributes\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');
|
|
});
|
|
});
|