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('xl:bg-transparent') ->toContain('xl:fixed') ->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('keeps application links next to advanced actions on the right', function () { $application = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); $links = file_get_contents(resource_path('views/components/applications/links.blade.php')); $desktop = str($application)->after('resource-heading-actions flex')->toString(); $advancedPosition = strpos($desktop, 'not->toBeFalse() ->and($linksPosition)->not->toBeFalse() ->and($linksPosition)->toBeGreaterThan($advancedPosition) ->and($links)->toContain('listbox-panel top-full! right-0! left-auto!') ->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 application lifecycle controls 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(); expect($desktop) ->toContain('application-desktop-actions') ->toContain('Actions') ->toContain('listbox-panel top-full! right-0! left-auto!') ->toContain('Redeploy') ->toContain('Restart') ->toContain('Stop') ->toContain('Deploy'); }); it('shows deploy directly when it is the only available lifecycle action', 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("@if (str(\$application->status)->startsWith('exited'))") ->toContain('id="application-desktop-deploy"') ->toContain('@else') ->toContain('id="application-desktop-actions"'); }); it('moves application backups from the top tabs into the settings sidebar', function () { $heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); $configuration = file_get_contents(resource_path('views/livewire/project/application/configuration.blade.php')); $backup = file_get_contents(resource_path('views/livewire/project/application/backup/index.blade.php')); expect($heading)->not->toContain("'label' => 'Backups'") ->and($configuration)->toContain('and($backup)->toContain('not->toContain("'label' => 'Terminal'") ->not->toContain("'label' => 'Deployment'") ->not->toContain("'label' => 'Runtime'") ->and($sidebar) ->toContain("'label' => 'Terminal'") ->toContain("'label' => 'Deployment'") ->toContain("'label' => 'Runtime'") ->toContain("'Logs' => ['Deployment', 'Runtime']") ->toContain("'Operations' => ['Terminal', 'Rollback', 'Resource Limits'"); }); it('groups application automation pages separately from build and deploy', function () { $sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php')); expect($sidebar) ->toContain("'Build & deploy' => ['Git Source', 'Servers', 'Healthcheck']") ->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Preview Deployments']") ->toContain("'Operations' => ['Terminal', 'Rollback', 'Resource Limits'"); }); it('centers the rollback image loading state across the card', function () { $rollback = file_get_contents(resource_path('views/livewire/project/application/rollback.blade.php')); expect($rollback) ->toContain('class="w-full" wire:target="loadImages" wire:loading') ->toContain('flex items-center justify-center'); }); it('shows pull request loading feedback inside its settings card', function () { $previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php')); expect($previews) ->toContain('class="w-full" wire:loading wire:target="load_prs"') ->toContain('min-h-32 items-center justify-center') ->toContain('toContain("'application-settings-section-body w-full'") ->toContain("'is-flush' => \$backups->isNotEmpty()") ->not->toContain('application-settings-section-body is-flush w-full bg-transparent!'); }); it('keeps the application settings sidebar below the fixed header while scrolling', function () { $sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php')); $css = file_get_contents(resource_path('css/app.css')); expect($sidebar)->toContain('application-settings-navigation') ->and($css)->toContain('.application-settings-workspace > .application-settings-navigation') ->and($css)->toContain('top: 4rem;') ->and($css)->toContain('max-height: calc(100dvh - 5rem);'); }); it('builds application sidebar routes independently of the current request route', function () { $sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php')); expect($sidebar) ->not->toContain('get_route_parameters()') ->toContain("'project_uuid' => \$application->environment->project->uuid") ->toContain("'environment_uuid' => \$application->environment->uuid") ->toContain("'application_uuid' => \$application->uuid"); }); it('keeps the deployment log sidebar fixed in the layout without a top gap', function () { $deployment = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php')); $css = file_get_contents(resource_path('css/app.css')); expect($deployment)->toContain(':flush="true"') ->and($css)->toContain('.application-settings-navigation.is-flush') ->and($css)->toContain('position: static;') ->and($css)->toContain('overflow: visible;'); }); it('removes desktop top spacing from the deployment log viewer', function () { $deployment = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php')); expect($deployment)->toContain("'mt-2 flex flex-1 min-h-0 flex-col overflow-hidden lg:mt-0'"); }); it('uses a distinct runtime log icon in the sidebar', function () { $sidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php')); expect($sidebar)->toContain("'Runtime' => 'unordered-list'"); }); it('shows deployment history above the selected deployment logs', function () { $indexClass = file_get_contents(app_path('Livewire/Project/Application/Deployment/Index.php')); $indexView = file_get_contents(resource_path('views/livewire/project/application/deployment/index.blade.php')); $showView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php')); expect($indexClass) ->toContain('public bool $embedded = false;') ->toContain('public ?string $selectedDeploymentUuid = null;') ->and($indexView) ->toContain("'data-table-row-active' => \$selectedDeploymentUuid") ->and($showView) ->toContain('toContain(':selected-deployment-uuid="$deployment_uuid"'); }); it('fits the combined deployment history and logs within the desktop viewport', function () { $indexClass = file_get_contents(app_path('Livewire/Project/Application/Deployment/Index.php')); $showView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php')); expect($indexClass)->toContain('$this->defaultTake = 3;') ->and($showView) ->toContain('xl:h-[calc(100dvh-7.5rem)]') ->toContain('xl:overflow-hidden') ->toContain('xl:flex-1'); }); it('uses only the healthcheck toggle action to communicate enabled state', function () { $healthcheck = file_get_contents(resource_path('views/livewire/project/shared/health-checks.blade.php')); expect($healthcheck) ->not->toContain('toContain('buttonTitle="Enable"') ->toContain('Disable'); }); it('keeps Links dropdowns outside the scrollable tabs strip', 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')); $css = file_get_contents(resource_path('css/app.css')); $tabsComponent = file_get_contents(resource_path('views/components/resource-heading-tabs.blade.php')); // Application Links are standalone on mobile and next to actions on desktop. expect($application) ->not->toContain('toContain('resource-heading-menus') ->toContain('not->toContain('toContain('resource-heading-menus') ->toContain('toContain('.resource-heading-tabs::-webkit-scrollbar') ->toContain('scrollbar-width: none') ->toContain('.resource-heading-tabs-control') ->toContain('.resource-heading-tabs-control-icon'); // Kumo-style overflow chevrons live on the shared tabs component. expect($tabsComponent) ->toContain('Scroll tabs left') ->toContain('Scroll tabs right') ->toContain('scrollByDir') ->toContain('canStart') ->toContain('canEnd') ->toContain('resource-heading-tabs-control is-start') ->toContain('resource-heading-tabs-control is-end') ->toContain('scrollActiveIntoView') ->toContain('findActiveTab') ->toContain('livewire:navigated'); }); it('shows an icon in application and service Links controls', function () { $applicationLinks = file_get_contents(resource_path('views/components/applications/links.blade.php')); $serviceLinks = file_get_contents(resource_path('views/components/services/links.blade.php')); expect($applicationLinks)->toContain("@props(['application', 'fullWidth' => false])") ->toContain('and($serviceLinks)->toContain('and($applicationLinks)->not->toContain('and($serviceLinks)->not->toContain('between('class="w-full xl:hidden"', 'class="hidden w-full items-center xl:fixed')->toString(); $mobileServiceSection = str($service)->between('class="w-full md:hidden"', 'class="hidden w-full items-center md:flex')->toString(); expect($mobileApplicationSection) ->toContain('toContain('full-width') ->toContain('resource-heading-menus') ->not->toContain('not->toContain("'label' => 'Settings'"); $applicationLinks = file_get_contents(resource_path('views/components/applications/links.blade.php')); expect($applicationLinks) ->toContain("'button w-full justify-between' => \$fullWidth") ->toContain('toContain('toContain('resource-heading-menus') ->not->toContain('toBe(2); expect(substr_count($service, 'toBe(2); }); it('uses overflow scroll arrows on resource heading navbars', function () { $files = [ resource_path('views/livewire/server/navbar.blade.php'), resource_path('views/components/project/navbar.blade.php'), ]; foreach ($files as $path) { expect(file_get_contents($path))->toContain('