not->toContain('and($databaseHeading)->not->toContain('and($serviceConfiguration) ->toContain("['label' => 'Backups'") ->toContain("['label' => 'Runtime Logs'") ->toContain("['label' => 'Terminal'") ->and($databaseSidebar) ->toContain("['label' => 'Backups'") ->toContain("['label' => 'Runtime Logs'") ->toContain("['label' => 'Terminal'"); }); it('uses a full page load for database backup imports', function () { $sidebar = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php')); expect($sidebar)->toContain("['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'icon' => 'upload', 'navigate' => false"); }); it('matches application action bar behavior for services and databases', function () { $service = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); $database = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php')); foreach ([$service, $database] as $heading) { expect($heading) ->toContain('@teleport(\'#resource-action-hud-slot\')') ->toContain('xl:w-auto') ->toContain('not->toContain('hidden lg:block lg:h-12'); } expect($service)->toContain('id="service-desktop-actions"') ->and($database)->toContain('id="database-desktop-actions"'); }); it('groups database and service navigation by user workflow', function () { $database = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php')); $serviceSidebars = [ file_get_contents(resource_path('views/components/service/configuration-sidebar.blade.php')), file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php')), ]; expect($database) ->toContain("'Settings' => ['General', 'Environment Variables', 'Persistent Storage', 'Healthcheck']") ->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Terminal', 'Metrics']") ->toContain("'Deploy' => ['Servers']") ->toContain("'Automation' => ['Webhooks', 'Backups', 'Import Backup']") ->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Tags', 'Danger Zone']"); foreach ($serviceSidebars as $serviceSidebar) { expect($serviceSidebar) ->toContain("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage']") ->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Terminal']") ->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups', 'Import Backup']") ->toContain("'Operations' => ['Resource Operations', 'Tags', 'Danger Zone']"); } }); it('groups application navigation by user workflow', function () { $application = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php')); expect($application) ->toContain("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Advanced', 'Swarm', 'Healthcheck']") ->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Deployment Logs', 'Terminal', 'Metrics']") ->toContain("'Deploy' => ['Git Source', 'Servers', 'Preview Deployments']") ->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups']") ->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback', 'Tags', 'Danger Zone']"); }); it('uses the same responsive settings grid for applications services and databases', function () { $sidebars = [ resource_path('views/components/application/configuration-sidebar.blade.php'), resource_path('views/components/service/configuration-sidebar.blade.php'), resource_path('views/components/database/configuration-sidebar.blade.php'), ]; foreach ($sidebars as $sidebar) { expect(file_get_contents($sidebar)) ->toContain('grid grid-cols-2 gap-0.5') ->toContain('sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1'); } expect(file_get_contents($sidebars[0])) ->not->toContain('aria-label="Configuration menu"') ->not->toContain('menuOpen'); }); it('shows the database sidebar on backup pages', function () { $configuration = file_get_contents(resource_path('views/livewire/project/database/configuration.blade.php')); $backups = file_get_contents(resource_path('views/livewire/project/database/backup/index.blade.php')); $sidebar = file_get_contents(resource_path('views/components/database/configuration-sidebar.blade.php')); expect($configuration)->toContain('and($backups) ->toContain('toContain('current-route="project.database.backup.index"') ->and($sidebar) ->toContain("str(\$currentRoute)->startsWith('project.database.backup')"); }); it('shows the service sidebar on the storage backups page', function () { $backups = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); expect($backups) ->toContain('toContain('current-route="project.service.volume-backups.index"') ->toContain('xl:grid-cols-[210px_minmax(0,1fr)]'); }); it('combines service database and storage backups in one section', function () { $backups = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); $styles = file_get_contents(resource_path('css/app.css')); expect(substr_count($backups, 'toBe(1) ->and(substr_count($backups, 'data-table-header backup-table-grid'))->toBe(1) ->and($backups)->toContain('Storage backup')->toContain('Database backup') ->not->toContain('data-table-header scheduled-backups-table-grid') ->not->toContain('>Database backups') ->not->toContain('>Storage backups') ->toContain("'application-settings-section-body w-full'") ->toContain('class="data-table w-full overflow-x-auto"') ->toContain('backup-table-grid service-backup-table-grid') ->not->toContain('Executions') ->toMatch('/class="(?=[^"]*\\bdata-table-row\\b)(?=[^"]*\\bbackup-table-grid\\b)(?=[^"]*text-\\[13px\\])[^\"]*"/') ->toContain('class="listbox-option justify-start! gap-2.5!"') ->toContain('x-data="{ dropdownOpen: false }"') ->toContain('class="listbox-panel left-0! right-auto! z-[90]! w-52! min-w-52! sm:left-auto! sm:right-0!"') ->not->toContain('toContain('.service-backup-table-grid'); }); it('keeps backup navigation out of compose database settings', function () { $sidebar = file_get_contents(resource_path('views/components/service-database/sidebar.blade.php')); expect($sidebar) ->not->toContain("'label' => 'Backups'") ->not->toContain("'label' => 'Import Backup'") ->not->toContain("'route' => 'project.service.database.backups'"); }); it('links service backup details back to the unified service backups page', function () { $sidebar = file_get_contents(resource_path('views/components/backup-sidebar.blade.php')); expect($sidebar)->toContain("'back' => 'project.service.volume-backups.index'"); }); it('declares the database backup mount return type', function () { $returnType = (new ReflectionMethod(DatabaseBackups::class, 'mount'))->getReturnType(); expect($returnType)->not->toBeNull() ->and($returnType->getName())->toBe('mixed'); }); it('keeps service navigation visible on backup detail pages and uses section tabs', function () { $databaseBackup = file_get_contents(resource_path('views/livewire/project/service/database-backups.blade.php')); $storageBackup = file_get_contents(resource_path('views/livewire/project/service/volume-backup/show.blade.php')); foreach ([$databaseBackup, $storageBackup] as $view) { expect($view) ->toContain('toContain('not->toContain('toContain('context="service"') ->and($storageBackup)->toContain('context="service-volume"'); }); it('opens service backup schedules in place without navigating', function () { $index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); expect($index) ->toContain('wire:click="openSchedule(') ->toContain('wireOpen="scheduleModalOpen"') ->toContain('not->toContain("route('project.service.backups.schedule.show'"); }); it('shows service backup executions below schedules without a separate view tab', function () { $index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); $executions = file_get_contents(resource_path('views/livewire/project/service/backup-executions.blade.php')); expect($index) ->toContain('not->toContain('aria-label="Backup views"') ->not->toContain("route('project.service.backups.executions'"); expect($executions) ->toContain('title="Executions"') ->toContain('flush>'); }); it('wraps backup modal tabs and shows schedule loading feedback', function () { $tabs = file_get_contents(resource_path('views/components/backup-tabs.blade.php')); $index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); expect($tabs) ->toContain('flex-wrap') ->not->toContain('overflow-x-auto'); expect($index) ->toContain('not->toContain('Loading schedule\n'); }); it('loads every backup editor section when the modal opens and switches tabs locally', function () { $tabs = file_get_contents(resource_path('views/components/backup-tabs.blade.php')); $index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); expect($index) ->toContain("x-data=\"{ activeSection: 'general' }\"") ->toContain("@foreach (['general', 's3', 'retention', 'danger'] as \$modalSection)") ->toContain("x-show=\"activeSection === '{{ \$modalSection }}'\""); expect($tabs) ->toContain("@click=\"activeSection = '{{ \$item['key'] }}'\"") ->not->toContain('wire:click="selectScheduleSection'); }); it('keeps the backup schedule modal height stable while switching sections', function () { $modal = file_get_contents(resource_path('views/components/modal-input.blade.php')); $index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); expect($index)->toContain('isLarge fixedHeight') ->and($modal) ->toContain("'sm:h-[40rem]' => \$fixedHeight"); }); it('shows S3 section descriptions from the title helper', function () { $volumeS3 = file_get_contents(resource_path('views/livewire/project/shared/storages/volume-backups/s3.blade.php')); $databaseS3 = file_get_contents(resource_path('views/livewire/project/database/backup-edit/s3.blade.php')); foreach ([$volumeS3, $databaseS3] as $view) { expect($view) ->toContain('not->toContain('
') ->not->toContain('

S3 storage

'); } }); it('adds a back up now action to every service backup schedule row', function () { $index = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php')); $styles = file_get_contents(resource_path('css/app.css')); expect($index) ->toContain('Actions') ->toContain('
') ->toContain("wire:click.stop=\"backupNow('database',") ->toContain("wire:click.stop=\"backupNow('storage',") ->toContain('toContain('Back up now') ->toContain('defaultClass="icon-button shrink-0"') ->toContain('aria-label="Edit backup schedule"') ->toContain('') ->not->toContain('>Settings') ->not->toContain('class="contents cursor-pointer"'); expect($styles) ->toContain('.service-backup-table-grid') ->toContain('width: 100%;') ->toContain('.data-table-row.service-backup-table-grid {') ->not->toContain('.data-table-header.service-backup-table-grid > :last-child') ->not->toContain('.data-table-row.service-backup-table-grid > :last-child'); }); it('refreshes backup executions from backup broadcasts on the current team channel', function () { $serviceExecutions = file_get_contents(app_path('Livewire/Project/Service/BackupExecutions.php')); $serviceBackups = file_get_contents(app_path('Livewire/Project/Service/VolumeBackup/Index.php')); $databaseExecutions = file_get_contents(app_path('Livewire/Project/Database/BackupExecutions.php')); $databaseBackupJob = file_get_contents(app_path('Jobs/DatabaseBackupJob.php')); expect($serviceExecutions) ->toContain('echo-private:team.{$teamId},BackupCreated') ->toContain("=> '\$refresh'") ->and($serviceBackups) ->toContain('echo-private:team.{$teamId},BackupCreated') ->and($databaseExecutions) ->toContain('$teamId = currentTeam()->id') ->not->toContain('$userId = Auth::id()') ->and(strpos($databaseBackupJob, "'finished_at' => Carbon::now()->toImmutable()")) ->toBeLessThan(strrpos($databaseBackupJob, 'BackupCreated::dispatch($this->team->id)')); }); it('offers downloads from the service backup executions list', function () { $component = file_get_contents(app_path('Livewire/Project/Service/BackupExecutions.php')); $view = file_get_contents(resource_path('views/livewire/project/service/backup-executions.blade.php')); expect($component) ->toContain("route('download.backup'") ->toContain("route('download.volume-backup'") ->and($view) ->toContain('Actions') ->toContain('aria-label="Download backup"') ->toContain('@click.stop'); }); it('uses a distinct backup icon across resource sidebars', function () { $sidebars = [ resource_path('views/components/application/configuration-sidebar.blade.php'), resource_path('views/components/database/configuration-sidebar.blade.php'), resource_path('views/livewire/project/service/configuration.blade.php'), ]; foreach ($sidebars as $sidebar) { $contents = file_get_contents($sidebar); expect($contents)->toMatch("/['\"]Backups['\"].*['\"]database['\"]/s"); } }); it('shows the database sidebar on runtime logs', function () { $logs = file_get_contents(resource_path('views/livewire/project/shared/logs.blade.php')); expect($logs) ->toContain("in_array(\$type, ['application', 'database', 'service'], true)") ->toContain('toContain('loading-state-card'); }); it('shows the database sidebar in the terminal', function () { $terminal = file_get_contents(resource_path('views/livewire/project/shared/execute-container-command.blade.php')); expect($terminal) ->toContain("in_array(\$type, ['application', 'database', 'service', 'server'], true)") ->toContain('toContain("in_array(\$type, ['application', 'database', 'service'], true)") ->toContain('and($terminal) ->toContain("in_array(\$type, ['application', 'database', 'service', 'server'], true)") ->toContain('and($sidebar) ->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Terminal']") ->toContain("'Operations' => ['Resource Operations', 'Tags', 'Danger Zone']"); });