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('places the state-aware no-cache action immediately after deploy or redeploy', function () {
$heading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
$actions = str($heading)->after('id="application-desktop-actions"')->before('@endteleport')->toString();
expect($actions)
->toContain("str(\$application->status)->startsWith('running') ? 'Redeploy (without cache)' : 'Deploy (without cache)'")
->toContain("str(\$application->status)->startsWith('running') ? 'force_deploy_without_cache' : 'deploy(true)'");
expect(strpos($actions, 'wire:click="deploy"'))
->toBeLessThan(strpos($actions, 'Redeploy (without cache)'))
->and(strpos($actions, 'Redeploy (without cache)'))
->toBeLessThan(strpos($actions, 'Restart'));
});
it('uses the shared Coollabs gradient for primary resource actions in the desktop header', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->toContain('.application-heading-actions .button-highlighted')
->toContain('@apply button-highlighted;');
});
it('uses iconless highlighted Actions dropdowns for service and proxy lifecycle controls', function () {
foreach ([
resource_path('views/livewire/project/service/heading.blade.php') => 'service-desktop-actions',
resource_path('views/livewire/server/navbar.blade.php') => 'server-desktop-actions',
] as $path => $id) {
$heading = file_get_contents($path);
$trigger = str($heading)->after("id=\"{$id}\"")->before('
toString();
expect($trigger)
->toContain('button button-highlighted')
->toContain('Actions')
->not->toContain('play-circle');
}
});
it('places the Traefik dashboard last in the server Actions menu', function () {
$navbar = file_get_contents(resource_path('views/livewire/server/navbar.blade.php'));
$actions = str($navbar)->after('id="server-desktop-actions"')->before('@endcan')->toString();
expect(strpos($actions, 'Refresh Proxy Status'))
->toBeLessThan(strpos($actions, 'Traefik Dashboard'));
});
it('keeps database lifecycle controls direct and highlights the primary action', function () {
$heading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
$desktop = str($heading)->after('resource-heading-actions flex')->before('@endteleport')->toString();
expect($desktop)
->toContain('Restart')
->toContain('Stop')
->toContain('button button-highlighted')
->not->toContain('
not->toContain('toContain("@teleport('#configuration-warning-hud-slot')")
->toContain("@teleport('#configuration-warning-hud-slot-mobile')")
->toContain('')
->not->toContain('and($warning)
->toContain('aria-label="Configuration changes not applied"')
->toContain('Changes pending')
->toContain('The latest configuration has not been applied')
->toContain('fixed top-14 left-1/2')
->toContain('-translate-x-1/2')
->toContain('lg:absolute lg:top-full lg:right-0 lg:left-auto')
->toContain('lg:translate-x-0')
->toContain('@click.outside="open = false"')
->toContain('@keydown.escape.window="open = false"')
->toContain("\$dispatch('open-configuration-diff')");
});
it('renders only one configuration warning on database runtime logs', function () {
$logs = file_get_contents(resource_path('views/livewire/project/shared/logs.blade.php'));
$databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
expect($logs)
->not->toContain(" \n\n @if (\$type === 'application')")
->and($databaseHeading)
->toContain('');
});
it('renders only one configuration warning on database and service terminal pages', function () {
$terminal = file_get_contents(resource_path('views/livewire/project/shared/execute-container-command.blade.php'));
expect($terminal)
->not->toContain("@elseif (\$type === 'database')\n ")
->not->toContain("@elseif (\$type === 'service')\n ");
});
it('renders only one configuration warning on the service volume backups page', function () {
$volumeBackups = file_get_contents(resource_path('views/livewire/project/service/volume-backup/index.blade.php'));
expect($volumeBackups)
->not->toContain('')
->toContain('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 Logs'")
->toContain("'label' => 'Runtime Logs'")
->toContain("'Observe & troubleshoot' => ['Runtime Logs', 'Deployment Logs', 'Terminal', 'Metrics']")
->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback'");
});
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("'Settings' => ['General', 'Domains', 'Environment Variables', 'Persistent Storage', 'Advanced', 'Swarm', 'Healthcheck']")
->toContain("'Deploy' => ['Git Source', 'Servers', 'Preview Deployments']")
->toContain("'Automation' => ['Scheduled Tasks', 'Webhooks', 'Backups']")
->toContain("'Operations' => ['Resource Operations', 'Resource Limits', 'Rollback'");
});
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 only on the action button', function () {
$previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
expect($previews)
->toContain('wire:click="load_prs"')
->not->toContain('wire:loading.remove wire:target="load_prs"')
->not->toContain('wire:loading wire:target="load_prs"')
->not->toContain('Loading pull requests…');
});
it('shows loading feedback while a service deployment starts', function () {
$service = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
expect($service)
->toContain('deploying: false')
->toContain('@service-deploy-finished.window="deploying = false"')
->toContain('toContain("window.dispatchEvent(new CustomEvent('service-deploy-finished'))");
});
it('uses neutral icons for non-destructive resource actions', function () {
foreach (['application', 'service', 'database'] as $resource) {
$heading = file_get_contents(resource_path("views/livewire/project/{$resource}/heading.blade.php"));
expect($heading)
->not->toContain('class="size-3.5 text-orange-500')
->not->toContain('class="size-3.5 text-warning"')
->not->toContain('class="size-4 text-warning"')
->toContain('name="stop-circle"');
}
});
it('uses a circular stop icon in application and service action menus', function () {
$icons = file_get_contents(resource_path('views/components/reicon.blade.php'));
expect($icons)
->toContain("'stop-circle' =>")
->toContain('toContain('toContain('name="stop-circle" class="size-3.5 text-error"')
->not->toContain('name="stop" class="size-3.5 text-error"');
}
});
it('allows the shared empty state to control the storage backups background', function () {
$backups = file_get_contents(resource_path('views/livewire/project/application/backup/index.blade.php'));
expect($backups)
->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('uses the same mobile heading gap on deployment pages as application settings', function () {
$configuration = file_get_contents(resource_path('views/livewire/project/application/configuration.blade.php'));
$deploymentIndex = file_get_contents(resource_path('views/livewire/project/application/deployment/index.blade.php'));
$deploymentShow = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
expect($configuration)->toContain('application-settings-workspace mt-4')
->and($deploymentIndex)->toContain("'mt-4 max-w-[1180px] lg:mt-0' => ! \$embedded")
->and($deploymentShow)->toContain('application-settings-workspace mt-4')
->toContain('lg:mt-0');
});
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 Logs' => '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'));
$serviceLinksComponent = file_get_contents(app_path('View/Components/Services/Links.php'));
expect($applicationLinks)->toContain("@props(['application', 'fullWidth' => false, 'compact' => false])")
->toContain('and($serviceLinksComponent)->toContain('public bool $fullWidth = false')
->and($serviceLinks)
->toContain('and($applicationLinks)->not->toContain('and($serviceLinks)->not->toContain('toContain('Production')
->toContain('PR #{{ data_get($preview, \'pull_request_id\') }}')
->not->toContain("PR{{ data_get(\$preview, 'pull_request_id') }} |");
});
it('shows application Links as a compact badge beside the mobile status', function () {
$application = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
$applicationLinks = file_get_contents(resource_path('views/components/applications/links.blade.php'));
$mobileApplicationTitle = str($application)->between('class="mb-3 w-full xl:hidden"', '')->toString();
$mobileApplicationActions = str($application)->between('
', '
')->toString();
expect($mobileApplicationTitle)
->toContain('toContain('toContain('relative flex w-full min-w-0 items-center gap-2')
->toContain('compact')
->not->toContain('full-width');
expect($mobileApplicationActions)
->not->toContain('toContain("'compact' => false")
->toContain("'static' => \$compact")
->toContain("'inline-flex h-6 shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2 text-xs font-medium leading-none text-neutral-700 dark:border-white/[0.12] dark:bg-white/[0.07] dark:text-white' => \$compact")
->toContain('@unless ($compact)')
->toContain("'left-1/2! right-auto! w-[calc(100vw-2rem)]! max-w-md! min-w-0! -translate-x-1/2' => \$compact")
->toContain('between('class="w-full md:hidden"', 'class="hidden w-full items-center md:flex')->toString();
expect($mobileServiceSection)
->toContain('toContain('full-width')
->toContain('resource-heading-menus')
->not->toContain('toContain("'button w-full justify-between' => \$fullWidth")
->toContain("'left-0! right-0! w-full! min-w-0! max-w-none!' => \$fullWidth")
->toContain('toContain('No links available');
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'),
];
foreach ($files as $path) {
expect(file_get_contents($path))->toContain('