toContain('resource-heading-navbar'); } $application = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); expect($application) ->toContain('justify-start') ->toContain('xl:justify-end') ->toContain('xl:w-auto') ->toContain("@teleport('#resource-action-hud-slot')") ->not->toContain('xl:fixed') ->not->toContain('xl:top-14') ->toContain('xl:hidden') ->not->toContain('application-primary-tabs') ->not->toContain("typeof collapsed !== 'undefined'") ->not->toContain('Spacer: in-flow stand-in') ->not->toContain('hidden lg:block lg:h-12') ->not->toContain('border-l border-neutral-200 pl-1'); }); it('uses interactive status summaries in mobile resource headings', function () { $headings = [ resource_path('views/livewire/project/application/heading.blade.php'), resource_path('views/livewire/project/database/heading.blade.php'), resource_path('views/livewire/project/service/heading.blade.php'), ]; foreach ($headings as $path) { $mobileHeading = str(file_get_contents($path)) ->after('
') ->before('
') ->toString(); expect($mobileHeading) ->toContain('flex min-w-0 flex-col items-start gap-2') ->toContain('min-w-0 max-w-full truncate') ->toContain('not->toContain('toContain('id="resource-action-hud-slot"'); foreach ($files as $path) { $contents = file_get_contents($path); expect($contents) ->toContain("@teleport('#resource-action-hud-slot')") ->not->toContain('xl:fixed xl:top-14 xl:right-4'); $desktopHud = str($contents)->after("@teleport('#resource-action-hud-slot')")->before('@endteleport')->toString(); expect($desktopHud) ->not->toContain('rounded-[10px]') ->not->toContain('border-neutral-200') ->not->toContain('bg-neutral-100') ->not->toContain('dark:bg-white/[0.035]'); } }); it('shows disabled deploy actions when service variables are missing', function () { $heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); $mobileActions = str($heading) ->after('
') ->before("@teleport('#resource-action-hud-slot')") ->toString(); $desktopActions = str($heading) ->after("@teleport('#resource-action-hud-slot')") ->before('@endteleport') ->toString(); expect($mobileActions) ->toContain('id="service-mobile-actions"') ->toContain('aria-disabled="true"') ->toContain('Deploy') ->toContain('missing required env vars') ->toContain('href="{{ $environmentVariablesUrl }}"') ->toContain('underline') ->and($desktopActions) ->toContain('id="service-desktop-actions"') ->toContain('aria-disabled="true"') ->toContain('Deploy') ->toContain('missing required env vars') ->toContain('href="{{ $environmentVariablesUrl }}"') ->toContain('underline'); }); it('places the account menu beside the desktop sidebar toggle while retaining it on mobile', function () { $layout = file_get_contents(resource_path('views/layouts/app.blade.php')); $navbar = file_get_contents(resource_path('views/components/navbar.blade.php')); $accountMenu = file_get_contents(resource_path('views/components/top-user-menu.blade.php')); expect($layout) ->not->toContain("{{-- Right cluster --}}\n ") ->toContain('') ->toContain('flex grow min-w-0 flex-col overflow-visible'); expect(substr_count($layout, ''))->toBe(1); expect($navbar) ->toContain('') ->toContain('Toggle sidebar') ->toContain("collapsed ? 'flex-col-reverse justify-center'"); expect($accountMenu) ->toContain("'sidebar' => false") ->toContain("'bottom-full! left-0! right-auto! top-auto! mb-1!' => \$sidebar"); }); it('keeps advanced operations in a dedicated Advanced dropdown', function () { $application = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); $service = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); $applicationAdvanced = file_get_contents(resource_path('views/components/applications/advanced.blade.php')); $serviceAdvanced = file_get_contents(resource_path('views/components/services/advanced.blade.php')); $links = file_get_contents(resource_path('views/components/applications/links.blade.php')); $applicationDesktop = str($application)->after('resource-heading-actions flex')->toString(); $serviceDesktop = str($service)->after('resource-heading-actions flex')->toString(); expect($applicationDesktop) ->toContain('application-desktop-actions') ->toContain('Deploy (without cache)') ->not->toContain('not->toContain('resource-heading-overflow-separator') ->not->toContain('Force deploy without cache') ->and($serviceDesktop) ->toContain('service-desktop-actions') ->toContain('Force Deploy') ->toContain('Force Cleanup Containers') ->toContain('Restart (pull latest)') ->not->toContain('resource-heading-overflow-separator') ->not->toContain('Pull Latest Images & Restart'); expect(strpos($applicationDesktop, 'toBeLessThan(strpos($applicationDesktop, 'application-desktop-actions')); expect(strpos($serviceDesktop, 'toBeLessThan(strpos($serviceDesktop, 'service-desktop-actions')); expect($applicationAdvanced) ->toContain('Advanced') ->toContain('name="grid"') ->not->toContain('Force deploy without cache') ->and($serviceAdvanced) ->toContain('Advanced') ->toContain('name="grid"') ->not->toContain('Pull Latest Images & Restart') ->not->toContain('Pull latest and restart') ->toContain('Force Restart') ->toContain('Force Deploy') ->toContain('Force Cleanup Containers') ->and($links)->toContain("'right-0! left-auto! min-w-60! max-w-96!' => !\$fullWidth") ->and($links)->toContain('listbox-option justify-start! gap-2.5!') ->and($links)->not->toContain('md:left-0 md:right-auto'); // Desktop layer uses exactly one unified pill bar (mobile section may still use its own pill). expect(substr_count($application, 'resource-heading-navbar'))->toBe(1); }); it('groups desktop application lifecycle controls in one Actions dropdown', function () { $heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); $desktop = str($heading)->after('resource-heading-actions flex')->toString(); expect($desktop) ->toContain('application-desktop-actions') ->toContain('Actions') ->toContain('listbox-panel top-full! right-0! left-auto!') ->toContain('listbox-option') ->toContain('Deploy') ->toContain('Restart') ->toContain('Stop') ->not->toContain('not->toContain('toContain('resourceActionsOpen: false') ->toContain("'z-[1000]': resourceActionsOpen") ->toContain('@resource-actions-toggled.window="resourceActionsOpen = $event.detail.open"'); foreach ($dropdowns as $dropdown) { expect(file_get_contents($dropdown)) ->toContain("\$dispatch('resource-actions-toggled', { open })"); } }); it('groups service restart options in the Actions dropdown', function () { $heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); $desktop = str($heading)->after('resource-heading-actions flex')->toString(); expect($desktop) ->toContain('service-desktop-actions') ->toContain('Restart') ->toContain('Restart (pull latest)') ->toContain("\$wire.dispatch('pullAndRestartEvent')") ->not->toContain('Pull Latest Images & Restart') ->not->toContain('before("@teleport('#resource-action-hud-slot')")->toString(); expect($mobile) ->toContain('Restart current version') ->toContain('Pull latest and restart') ->not->toContain('Pull Latest Images & Restart'); }); it('orders service restart actions before stop and advanced operations', function () { $heading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); $actions = str($heading)->after('id="service-desktop-actions"')->before('@else\n toString(); expect(strpos($actions, 'Restart\n')) ->toBeLessThan(strpos($actions, 'Restart (pull latest)')) ->and(strpos($actions, 'Restart (pull latest)')) ->toBeLessThan(strpos($actions, 'Stop')); }); it('groups application lifecycle options in an Actions dropdown', function () { $heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); $desktop = str($heading)->after('resource-heading-actions flex')->toString(); $trigger = str($desktop)->after('id="application-desktop-actions"')->before('
toString(); expect($desktop) ->toContain('id="application-desktop-actions"') ->toContain('Actions') ->toContain('Deploy') ->toContain('Deploy (without cache)') ->toContain('force_deploy_without_cache') ->toContain('deploy(true)') ->and($trigger) ->toContain('button button-highlighted') ->not->toContain('play-circle'); $mobile = str($heading)->before("@teleport('#resource-action-hud-slot')")->toString(); expect($mobile) ->toContain('Deploy (without cache)') ->not->toContain('Force deploy without cache'); }); it('shows stop in application action menus when the application is exited', function () { $heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); $desktopExitedActions = str($heading) ->after("@if (str(\$application->status)->startsWith('exited'))") ->before('@else') ->toString(); $mobileActions = str($heading) ->after('id="application-mobile-actions"') ->before('