451 lines
15 KiB
PHP
451 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');
|
||
|
|
});
|
||
|
|
});
|