708 lines
24 KiB
PHP
708 lines
24 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\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');
|
||
|
|
});
|
||
|
|
});
|