.*?)\n \}/s', $componentSource, $matches);
-
- expect($matches['body'] ?? '')
- ->toContain("\$this->dispatch('success', 'Sentinel settings updated. Restarting Sentinel.');");
-});
-
-it('starts mandatory Sentinel after server validation succeeds', function () {
- $interactiveValidation = file_get_contents(app_path('Livewire/Server/ValidateAndInstall.php'));
- $queuedValidation = file_get_contents(app_path('Jobs/ValidateAndInstallServerJob.php'));
-
- expect($interactiveValidation)->toContain('CheckAndStartSentinelJob::dispatch($this->server);')
- ->and($queuedValidation)->toContain('CheckAndStartSentinelJob::dispatch($this->server);');
-});
diff --git a/tests/Feature/LivewireLoadingColorTest.php b/tests/Feature/LivewireLoadingColorTest.php
deleted file mode 100644
index c524463c91..0000000000
--- a/tests/Feature/LivewireLoadingColorTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-toContain('@utility loading-indicator')
- ->toContain('@apply text-coollabs dark:text-warning;')
- ->and($loading)->toContain('loading-indicator')
- ->and($pageLoading)->toContain('loading-indicator')
- ->and($appCss)->toContain('.dark .animate-spin')
- ->toContain('color: var(--color-warning) !important;')
- ->toContain('.dark #nprogress .bar')
- ->toContain('background: var(--color-warning) !important;');
-});
-
-test('livewire navigation progress bar uses coollabs purple', function () {
- expect(config('livewire.navigate.progress_bar_color'))->toBe('#6b16ed');
-});
diff --git a/tests/Feature/LoadingComponentTest.php b/tests/Feature/LoadingComponentTest.php
deleted file mode 100644
index 0b8abec8e1..0000000000
--- a/tests/Feature/LoadingComponentTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-toContain('gap-2 text-[13px] text-neutral-500 dark:text-fg-dim')
- ->toContain("'size-3' => \$compact")
- ->toContain("'size-4' => ! \$compact")
- ->toContain('stroke="currentColor" stroke-width="2"')
- ->not->toContain('text-coollabs')
- ->not->toContain('dark:text-warning');
-});
-
-it('shows runtime container loading inside a settings card', function () {
- $logs = file_get_contents(resource_path('views/livewire/project/shared/logs.blade.php'));
-
- expect($logs)
- ->toContain('application-settings-section-body flex min-h-40 w-full items-center justify-center')
- ->toContain(' ');
-});
diff --git a/tests/Feature/LogClipboardHandlerTest.php b/tests/Feature/LogClipboardHandlerTest.php
deleted file mode 100644
index 1b02733532..0000000000
--- a/tests/Feature/LogClipboardHandlerTest.php
+++ /dev/null
@@ -1,39 +0,0 @@
-toContain('copyLogs()')
- ->toContain('navigator.clipboard?.writeText')
- ->toContain("Livewire.dispatch('error', ['Clipboard is not available. Please use HTTPS or localhost.']);")
- ->toContain("Livewire.dispatch('error', ['Failed to copy logs to clipboard.']);")
- ->toContain("Livewire.dispatch('success', ['Logs copied to clipboard.']);");
-
- expect(Str::between($view, 'copyLogs() {', 'downloadLogs()'))
- ->toContain('navigator.clipboard?.writeText(content).then(() =>')
- ->not->toContain("navigator.clipboard.writeText(content);\n Livewire.dispatch('success'");
-});
-
-it('guards shared log clipboard writes and handles Livewire preparation failures', function () {
- $view = bladeView('resources/views/livewire/project/shared/get-logs.blade.php');
-
- expect($view)
- ->toContain('navigator.clipboard?.writeText')
- ->toContain("Livewire.dispatch('error', ['Clipboard is not available. Please use HTTPS or localhost.']);")
- ->toContain("Livewire.dispatch('error', ['Failed to copy logs to clipboard.']);")
- ->toContain("Livewire.dispatch('error', ['Failed to prepare logs for clipboard.']);")
- ->toContain("Livewire.dispatch('success', ['Logs copied to clipboard.']);");
-
- expect($view)
- ->toContain('$wire.copyLogs().then(logs =>')
- ->toContain('}).catch(() => {')
- ->not->toContain('navigator.clipboard.writeText(logs);');
-});
diff --git a/tests/Feature/LogFontStylingTest.php b/tests/Feature/LogFontStylingTest.php
deleted file mode 100644
index 1d5a566bc2..0000000000
--- a/tests/Feature/LogFontStylingTest.php
+++ /dev/null
@@ -1,59 +0,0 @@
-toContain("font-family: 'Geist Mono'")
- ->toContain("url('../fonts/geist-mono-variable.woff2')")
- ->toContain("font-family: 'Geist Sans'")
- ->toContain("url('../fonts/geist-sans-variable.woff2')")
- ->and($appCss)
- ->toContain("--font-sans: 'Geist Sans', Inter, sans-serif")
- ->toContain('@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-clip;')
- ->toContain("--font-logs: 'Geist Mono'")
- ->toContain("--font-geist-sans: 'Geist Sans'")
- ->and($fontPath)
- ->toBeFile()
- ->and($geistSansPath)
- ->toBeFile();
-});
-
-it('uses geist mono for shared logs and terminal rendering', function () {
- $sharedLogsView = file_get_contents(resource_path('views/livewire/project/shared/get-logs.blade.php'));
- $deploymentLogsView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
- $activityMonitorView = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php'));
- $dockerCleanupView = file_get_contents(resource_path('views/livewire/server/docker-cleanup-executions.blade.php'));
- $terminalClient = file_get_contents(resource_path('js/terminal.js'));
-
- expect($sharedLogsView)
- ->toContain('font-logs max-w-full cursor-default')
- ->toContain('font-logs max-w-full whitespace-pre-wrap break-all text-neutral-400')
- ->and($deploymentLogsView)
- ->toContain('flex min-w-0 flex-col font-logs')
- ->toContain('mb-2 font-logs text-neutral-400')
- ->and($activityMonitorView)
- ->toContain('and($dockerCleanupView)
- ->toContain('font-mono')
- ->and($terminalClient)
- ->toContain('"Geist Mono"');
-});
-
-it('constrains activity monitor logs inside the available viewport', function () {
- $activityMonitorView = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php'));
-
- expect($activityMonitorView)
- ->toContain('flex flex-col w-full min-w-0 max-w-full')
- ->toContain('toContain('');
-});
diff --git a/tests/Feature/LoginPageBrandingTest.php b/tests/Feature/LoginPageBrandingTest.php
deleted file mode 100644
index ffe7da67ce..0000000000
--- a/tests/Feature/LoginPageBrandingTest.php
+++ /dev/null
@@ -1,118 +0,0 @@
-toContain('title="Coolify"')
- ->not->toContain('title="Welcome back"');
-
- expect($register)
- ->toContain('title="Coolify"')
- ->not->toContain(":title=\"\$isFirstUser ? 'Create the root account' : 'Create your account'\"");
-
- expect($forgot)
- ->toContain('title="Coolify"')
- ->not->toContain("title=\"{{ __('auth.forgot_password_heading') }}\"");
-});
-
-test('page body uses the dynamic viewport height on mobile', function () {
- $styles = file_get_contents(resource_path('css/app.css'));
-
- expect($styles)
- ->toMatch('/body\s*\{[^}]*min-height:\s*100dvh;/s')
- ->not->toMatch('/body\s*\{[^}]*@apply[^;]*min-h-screen/s');
-});
-
-test('auth pages use the Coollabs purple background glow', function () {
- $styles = file_get_contents(resource_path('css/app.css'));
-
- expect($styles)
- ->toMatch('/\.auth-shell\s*\{[^}]*color-mix\(in oklab, var\(--color-coollabs\) 9%, transparent\)/s')
- ->not->toMatch('/\.auth-shell\s*\{[^}]*color-mix\(in oklab, var\(--color-accent\) 9%, transparent\)/s');
-});
-
-test('external login providers are centered and full width', function () {
- $login = file_get_contents(resource_path('views/auth/login.blade.php'));
-
- expect($login)
- ->toContain('class="flex flex-col gap-2"')
- ->toContain('class="w-full justify-center"')
- ->not->toContain('sm:w-[calc(50%-0.25rem)]');
-});
-
-test('external login providers display their icons except oidc', function () {
- $login = file_get_contents(resource_path('views/auth/login.blade.php'));
-
- expect($login)
- ->toContain("@if (\$provider_setting->provider !== 'oidc')")
- ->toContain("asset('svgs/'.\$provider_setting->provider.'.svg')")
- ->toContain('class="size-5 shrink-0 dark:invert"');
-
- foreach (['authentik', 'azure', 'bitbucket', 'clerk', 'discord', 'github', 'gitlab', 'google', 'infomaniak', 'zitadel'] as $provider) {
- expect(public_path("svgs/{$provider}.svg"))->toBeFile();
- }
-});
-
-test('error pages use the Coollabs purple background glow', function () {
- $styles = file_get_contents(resource_path('css/app.css'));
-
- expect($styles)
- ->toMatch('/\.error-shell\s*\{[^}]*color-mix\(in oklab, var\(--color-coollabs\) 9%, transparent\)/s')
- ->not->toMatch('/\.error-shell\s*\{[^}]*color-mix\(in oklab, var\(--color-accent\) 9%, transparent\)/s');
-});
-
-test('confirm password page uses the shared auth shell', function () {
- $confirm = file_get_contents(resource_path('views/auth/confirm-password.blade.php'));
-
- expect($confirm)
- ->toContain('x-auth.shell')
- ->toContain('title="Coolify"')
- ->toContain('x-auth.alert')
- ->toContain('auth-guidance')
- ->not->toContain('bg-gray-50 dark:bg-base')
- ->not->toContain('!text-5xl font-extrabold');
-});
-
-test('two factor challenge page uses the shared auth shell', function () {
- $challenge = file_get_contents(resource_path('views/auth/two-factor-challenge.blade.php'));
-
- expect($challenge)
- ->toContain('x-auth.shell')
- ->toContain('title="Coolify"')
- ->toContain('x-auth.alert')
- ->toContain('auth-guidance')
- ->toContain('Verify and continue')
- ->not->toContain('bg-gray-50 dark:bg-base')
- ->not->toContain('!text-5xl font-extrabold');
-});
-
-test('email verification page uses the shared auth shell', function () {
- $verification = file_get_contents(resource_path('views/auth/verify-email.blade.php'));
-
- expect($verification)
- ->toContain('x-layout-simple')
- ->toContain('x-auth.shell')
- ->toContain('title="Coolify"')
- ->toContain('auth-guidance')
- ->toContain('block sm:inline')
- ->not->toContain('
')
- ->not->toContain('md:h-screen');
-});
-
-test('team invitation page uses the shared auth shell', function () {
- $invitation = file_get_contents(resource_path('views/invitation/accept.blade.php'));
-
- expect($invitation)
- ->toContain('x-auth.shell')
- ->toContain('title="Coolify"')
- ->toContain('x-auth.alert')
- ->toContain('Accept invitation')
- ->not->toContain('bg-gray-50 dark:bg-base')
- ->not->toContain('!text-5xl font-extrabold');
-});
diff --git a/tests/Feature/MenuItemActiveNoBorderTest.php b/tests/Feature/MenuItemActiveNoBorderTest.php
deleted file mode 100644
index 4642298857..0000000000
--- a/tests/Feature/MenuItemActiveNoBorderTest.php
+++ /dev/null
@@ -1,26 +0,0 @@
-toContain('rounded-md')
- ->toContain('bg-black/[0.05]')
- ->and($menuSubitemActive[0] ?? '')
- ->toContain('rounded-md')
- ->toContain('bg-black/[0.05]');
-
- // Accent rail must be disabled (content: none), not drawn as a 3px accent bar.
- expect($appCss)
- ->toMatch('/\.menu-item-active::before,\s*\.menu-subitem-active::before\s*\{[^}]*content:\s*none/s')
- ->not->toMatch('/\.menu-item-active::before\s*\{[^}]*width:\s*3px/s')
- ->not->toMatch('/\.menu-item-active::before\s*\{[^}]*background:\s*var\(--color-accent\)/s');
-});
diff --git a/tests/Feature/MenuItemContrastTest.php b/tests/Feature/MenuItemContrastTest.php
deleted file mode 100644
index 6a997dac8a..0000000000
--- a/tests/Feature/MenuItemContrastTest.php
+++ /dev/null
@@ -1,30 +0,0 @@
-toContain('dark:text-fg-dim')
- ->toContain('text-neutral-600')
- ->not->toContain('dark:text-fg-faint')
- ->and($menuSubitem[0] ?? '')
- ->toContain('dark:text-fg-dim')
- ->toContain('text-neutral-600')
- ->not->toContain('dark:text-fg-faint')
- ->and($subMenuItem[0] ?? '')
- ->toContain('dark:text-fg-dim')
- ->toContain('text-neutral-600')
- ->and($navSection[0] ?? '')
- ->toContain('dark:text-fg-dim')
- ->not->toContain('dark:text-fg-faint');
-});
diff --git a/tests/Feature/MetricsDisabledEmptyStateTest.php b/tests/Feature/MetricsDisabledEmptyStateTest.php
deleted file mode 100644
index 1765345b60..0000000000
--- a/tests/Feature/MetricsDisabledEmptyStateTest.php
+++ /dev/null
@@ -1,11 +0,0 @@
-toContain('toContain('description="Enable Sentinel and metrics for this server before collecting application usage data."')
- ->toContain('icon-name="dashboard"')
- ->not->toContain('');
-});
diff --git a/tests/Feature/MetricsTooltipStylingTest.php b/tests/Feature/MetricsTooltipStylingTest.php
deleted file mode 100644
index 37d3764669..0000000000
--- a/tests/Feature/MetricsTooltipStylingTest.php
+++ /dev/null
@@ -1,42 +0,0 @@
-toContain('overflow-visible!')
- ->toContain('rounded-none!')
- ->toContain('border-0!')
- ->toContain('bg-transparent!')
- ->toContain('shadow-none!')
- ->not->toContain('dark:border-coolgray-300!')
- ->not->toContain('dark:bg-coolgray-200!')
- ->and($customTooltip[0] ?? '')
- ->toContain('border-neutral-200')
- ->toContain('dark:border-coolgray-300')
- ->toContain('rounded-lg')
- ->toContain('shadow-lg');
-});
-
-test('metrics charts render custom tooltip content', function (string $view) {
- expect(file_get_contents(resource_path($view)))
- ->toContain('apexcharts-tooltip-custom');
-})->with([
- 'dashboard server metrics' => 'views/livewire/dashboard/server-metrics-chart.blade.php',
- 'server metrics' => 'views/livewire/server/charts.blade.php',
- 'resource metrics' => 'views/livewire/project/shared/metrics.blade.php',
-]);
-
-test('server metric tooltips show local and UTC timestamps', function (string $view) {
- expect(file_get_contents(resource_path($view)))
- ->toContain('formatLocalTimestamp(timestamp)')
- ->toContain('formatUtcTimestamp(timestamp)')
- ->toContain('Your time:')
- ->toContain('UTC:');
-})->with([
- 'dashboard server metrics' => 'views/livewire/dashboard/server-metrics-chart.blade.php',
- 'server metrics' => 'views/livewire/server/charts.blade.php',
-]);
diff --git a/tests/Feature/MobileResourceMenuTest.php b/tests/Feature/MobileResourceMenuTest.php
deleted file mode 100644
index 182d11f85f..0000000000
--- a/tests/Feature/MobileResourceMenuTest.php
+++ /dev/null
@@ -1,230 +0,0 @@
-toBeGreaterThanOrEqual(2);
-
- expect($heading)
- ->not->toContain('name="play-circle" class="size-3.5 text-warning"')
- ->not->toContain('name="play-circle" class="size-3.5 text-orange-500');
- }
-});
-
-it('uses full-width split action menus on mobile resource headings', function () {
- $headings = [
- 'application' => file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')),
- 'database' => file_get_contents(resource_path('views/livewire/project/database/heading.blade.php')),
- 'service' => file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')),
- ];
-
- foreach ($headings as $resource => $heading) {
- expect(mobileActionsAreBeforeDesktopActions($heading, "{$resource}-mobile-actions", "{$resource}-desktop-actions"))->toBeTrue();
-
- expect($heading)
- ->toContain('')
- ->toContain("
")
- ->not->toContain("{$resource}-mobile-section")
- ->not->toContain('not->toContain('not->toContain('@selected');
-
- expect(mobileActionsMarkup($heading, "{$resource}-mobile-actions"))
- ->toContain('toContain('listbox-option justify-start! gap-2.5!')
- ->toContain('role="menuitem"')
- ->toContain('name="play-circle" class="size-3.5"')
- ->toContain('name="restart" class="size-3.5')
- ->toContain('name="stop-circle" class="size-3.5 text-error"');
- }
-
- expect(mobileActionsMarkup($headings['application'], 'application-mobile-actions'))
- ->toContain('wire:click="deploy"')
- ->toContain('wire:click="deploy(true)"')
- ->toContain('force_deploy_without_cache')
- ->toContain('Deploy (without cache)')
- ->toContain("document.getElementById('application-mobile-stop-trigger')?.click()")
- ->toContain("document.getElementById('application-mobile-restart-trigger')?.click()");
-
- expect($headings['application'])
- ->toContain('')
- ->toContain('')
- ->not->toContain('application-mobile-deploy-trigger')
- ->not->toContain('application-mobile-force-deploy-trigger')
- ->not->toContain('Confirm Application Deployment?');
-
- expect(mobileActionsMarkup($headings['database'], 'database-mobile-actions'))
- ->toContain('x-bind:disabled="busy"')
- ->toContain("document.getElementById('database-restart-trigger')?.click()")
- ->toContain("document.getElementById('database-stop-trigger')?.click()")
- ->toContain("\$wire.dispatch('startEvent')");
-
- expect($headings['database'])
- ->toContain('')
- ->toContain('')
- ->not->toContain('database-start-trigger')
- ->not->toContain('Confirm Database Start?');
-
- expect(mobileActionsMarkup($headings['service'], 'service-mobile-actions'))
- ->toContain('x-bind:disabled="deploying"')
- ->toContain("document.getElementById('service-restart-trigger')?.click()")
- ->toContain("document.getElementById('service-stop-trigger')?.click()")
- ->toContain("\$wire.dispatch('startEvent')")
- ->toContain("\$wire.dispatch('forceDeployEvent')")
- ->toContain("\$wire.dispatch('pullAndRestartEvent')");
-
- expect($headings['service'])
- ->toContain('')
- ->toContain('')
- ->not->toContain('service-start-trigger')
- ->not->toContain('service-forceDeploy-trigger')
- ->not->toContain('service-pullAndRestart-trigger')
- ->not->toContain('Confirm Service Deployment?')
- ->not->toContain('Confirm Service Force Deployment?');
-});
-
-function mobileActionsMarkup(string $heading, string $actionsId): string
-{
- $actionsPosition = strpos($heading, 'id="'.$actionsId.'"');
-
- if ($actionsPosition === false) {
- return '';
- }
-
- $endPosition = strpos($heading, ' ', $actionsPosition);
-
- if ($endPosition === false) {
- return '';
- }
-
- return substr($heading, $actionsPosition, $endPosition - $actionsPosition);
-}
-
-function mobileActionsAreBeforeDesktopActions(string $heading, string $mobileActionsId, string $desktopActionsId): bool
-{
- $mobilePosition = strpos($heading, 'id="'.$mobileActionsId.'"');
- $hudPosition = strpos($heading, "@teleport('#resource-action-hud-slot')");
- $desktopPosition = strpos($heading, 'id="'.$desktopActionsId.'"');
-
- return $mobilePosition !== false
- && $hudPosition !== false
- && $desktopPosition !== false
- && $mobilePosition < $hudPosition
- && $hudPosition < $desktopPosition;
-}
-
-it('places database backups immediately after configuration in navigation menus', function () {
- $databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
- $mobileMenuItems = str($databaseHeading)->between('$databasePageItems = [', '$databaseConfigurationItems = [')->toString();
- $desktopMenuItems = str($databaseHeading)->between('class="scrollbar hidden', '')->toString();
-
- foreach ([$mobileMenuItems, $desktopMenuItems] as $menuItems) {
- expect(strpos($menuItems, 'Configuration'))
- ->toBeLessThan(strpos($menuItems, 'Backups'))
- ->and(strpos($menuItems, 'Backups'))->toBeLessThan(strpos($menuItems, 'Logs'))
- ->and(strpos($menuItems, 'Logs'))->toBeLessThan(strpos($menuItems, 'Terminal'));
- }
-});
-
-it('shows configuration sidebars as a mobile link grid and a desktop rail instead of native selects', function () {
- expect(file_get_contents(resource_path('views/livewire/project/database/configuration.blade.php')))
- ->toContain('
')
- ->toContain('xl:grid-cols-[210px_minmax(0,1fr)]')
- ->not->toContain('sub-menu-wrapper');
-
- foreach ([
- 'views/components/database/configuration-sidebar.blade.php',
- 'views/livewire/project/service/configuration.blade.php',
- 'views/livewire/project/service/index.blade.php',
- 'views/components/service-database/sidebar.blade.php',
- 'views/components/server/sidebar.blade.php',
- 'views/components/server/sidebar-proxy.blade.php',
- 'views/components/server/sidebar-sentinel.blade.php',
- 'views/components/server/sidebar-security.blade.php',
- ] as $view) {
- expect(file_get_contents(resource_path($view)))
- ->toContain('
toContain('class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3')
- ->toContain('xl:grid-cols-1 xl:border-y-0 xl:py-0')
- ->not->toContain('sub-menu-wrapper')
- ->not->toContain('not->toContain('toContain('aria-label="Server configuration sections"')
- ->toContain("'route' => 'server.proxy'")
- ->toContain("'navigate' => false");
-
- $serverNavbar = file_get_contents(resource_path('views/livewire/server/navbar.blade.php'));
-
- expect($serverNavbar)
- ->toContain('')
- ->toContain('
')
- ->toContain('
')
- ->toContain('')
- ->toContain('')
- ->not->toContain('navbar-main')
- ->not->toContain('toContain('Restart Proxy')
- ->toContain('Stop Proxy')
- ->toContain('Refresh Proxy Status')
- ->not->toContain('Traefik');
-
- expect(strpos($serverMobileActions, 'Restart Proxy'))
- ->toBeLessThan(strpos($serverMobileActions, 'Stop Proxy'))
- ->and(strpos($serverMobileActions, 'Stop Proxy'))
- ->toBeLessThan(strpos($serverMobileActions, 'Refresh Proxy Status'));
-
- foreach ([
- 'views/livewire/server/show.blade.php',
- 'views/livewire/server/advanced.blade.php',
- 'views/livewire/server/private-key/show.blade.php',
- 'views/livewire/server/ca-certificate/show.blade.php',
- 'views/livewire/server/cloud-provider-token/show.blade.php',
- 'views/livewire/server/cloudflare-tunnel.blade.php',
- 'views/livewire/server/docker-cleanup.blade.php',
- 'views/livewire/server/destinations.blade.php',
- 'views/livewire/server/log-drains.blade.php',
- 'views/livewire/server/charts.blade.php',
- 'views/livewire/server/swarm.blade.php',
- 'views/livewire/server/delete.blade.php',
- 'views/livewire/server/proxy/show.blade.php',
- 'views/livewire/server/proxy/logs.blade.php',
- 'views/livewire/server/proxy/dynamic-configurations.blade.php',
- 'views/livewire/server/sentinel/show.blade.php',
- 'views/livewire/server/sentinel/logs.blade.php',
- 'views/livewire/server/security/patches.blade.php',
- 'views/livewire/server/security/terminal-access.blade.php',
- ] as $view) {
- expect(file_get_contents(resource_path($view)))
- ->toContain('server-settings-workspace application-settings-workspace')
- ->toContain('xl:grid-cols-[210px_minmax(0,1fr)]')
- ->toContain('not->toContain('sub-menu-wrapper')
- ->not->toContain('class="flex flex-col h-full gap-8 md:flex-row');
- }
-});
-
-function serverMobileActionsMarkup(string $navbar): string
-{
- $actionsPosition = strpos($navbar, 'id="server-mobile-actions"');
- $restartModalPosition = strpos($navbar, ' $restartModalPosition) {
- return '';
- }
-
- return substr($navbar, $actionsPosition, $restartModalPosition - $actionsPosition);
-}
diff --git a/tests/Feature/ModalConfirmationDestructiveActionsIconTest.php b/tests/Feature/ModalConfirmationDestructiveActionsIconTest.php
deleted file mode 100644
index 313dbd0178..0000000000
--- a/tests/Feature/ModalConfirmationDestructiveActionsIconTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-toContain(' ')
- ->not->toContain('d="M6 18L18 6M6 6l12 12"');
-
- // Action bullets use trash; header dismiss may still use close X.
- $actionSection = Str::after($modal, 'The following actions will be performed:');
- $actionSection = Str::before($actionSection, '@if (!$disableTwoStepConfirmation)');
-
- expect($actionSection)
- ->toContain('name="trash"')
- ->not->toContain('name="x"');
-});
diff --git a/tests/Feature/NativeSelectMigrationTest.php b/tests/Feature/NativeSelectMigrationTest.php
deleted file mode 100644
index 5fa1f58b61..0000000000
--- a/tests/Feature/NativeSelectMigrationTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
- ['privateKeyId', 'webhook_endpoint'],
- 'livewire/project/application/domains.blade.php' => ['newDomainService'],
- 'livewire/project/service/domains.blade.php' => ['newServiceApplicationId'],
- 'livewire/server/new/by-ip.blade.php' => ['is_build_server'],
- ];
-
- foreach ($views as $path => $ids) {
- $view = file_get_contents(resource_path('views/'.$path));
-
- foreach ($ids as $id) {
- expect($view)
- ->toContain('toContain('id="'.$id.'"')
- ->not->toMatch('/]*id="'.preg_quote($id, '/').'"/s');
- }
- }
-});
diff --git a/tests/Feature/NavbarFocusRingTest.php b/tests/Feature/NavbarFocusRingTest.php
deleted file mode 100644
index 7b3ce31361..0000000000
--- a/tests/Feature/NavbarFocusRingTest.php
+++ /dev/null
@@ -1,8 +0,0 @@
-toContain('class="-mx-1 flex min-h-0 flex-1 flex-col gap-y-0.5 overflow-y-auto px-1 pb-2 scrollbar"');
-});
diff --git a/tests/Feature/NavigableCardHoverIndicatorTest.php b/tests/Feature/NavigableCardHoverIndicatorTest.php
deleted file mode 100644
index 0ca3245faf..0000000000
--- a/tests/Feature/NavigableCardHoverIndicatorTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
-toContain('hover:-translate-y-px')
- ->toContain('hover:shadow-md')
- ->not->toContain('group-hover:translate-x-0.5 group-hover:opacity-100')
- ->not->toContain('pointer-events-none absolute top-1/2 right-3');
- }
-});
diff --git a/tests/Feature/NavigationCloakTest.php b/tests/Feature/NavigationCloakTest.php
deleted file mode 100644
index 430400ea73..0000000000
--- a/tests/Feature/NavigationCloakTest.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toContain("document.addEventListener('livewire:navigated'")
- ->toContain("querySelectorAll('[x-cloak]')")
- ->toContain("removeAttribute('x-cloak')");
-});
-
-it('keeps the initial-load x-cloak guard on the app wrapper', function () {
- $layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
-
- expect($layout)->toContain('x-cloak');
-});
diff --git a/tests/Feature/NewResourceCardInteractionTest.php b/tests/Feature/NewResourceCardInteractionTest.php
deleted file mode 100644
index 312ddc7172..0000000000
--- a/tests/Feature/NewResourceCardInteractionTest.php
+++ /dev/null
@@ -1,10 +0,0 @@
-toBe(4)
- ->and(substr_count($view, '@keydown.space.self.prevent='))->toBe(4)
- ->and($view)->not->toContain('@keydown.enter.prevent=')
- ->and($view)->not->toContain('@keydown.space.prevent=');
-});
diff --git a/tests/Feature/NotificationEmailResendLayoutTest.php b/tests/Feature/NotificationEmailResendLayoutTest.php
deleted file mode 100644
index 6d37287d92..0000000000
--- a/tests/Feature/NotificationEmailResendLayoutTest.php
+++ /dev/null
@@ -1,25 +0,0 @@
-/s', $instance, $instanceResend);
- preg_match('/settings-section title="Resend".*?<\/x-application.settings-section>/s', $team, $teamResend);
-
- expect($instanceResend[0] ?? '')
- ->not->toBeEmpty()
- ->toContain('grid gap-4 lg:grid-cols-2')
- ->toContain('id="resendEnabled"')
- ->toContain('id="resendApiKey"')
- ->not->toContain('lg:col-span-2')
- ->not->toContain('description=');
-
- expect($teamResend[0] ?? '')
- ->not->toBeEmpty()
- ->toContain('grid gap-4 lg:grid-cols-2')
- ->toContain('id="resendEnabled"')
- ->toContain('id="resendApiKey"')
- ->not->toContain('lg:col-span-2')
- ->not->toContain('description=');
-});
diff --git a/tests/Feature/NotificationSettingsNavigationTest.php b/tests/Feature/NotificationSettingsNavigationTest.php
deleted file mode 100644
index 08e095b1b3..0000000000
--- a/tests/Feature/NotificationSettingsNavigationTest.php
+++ /dev/null
@@ -1,94 +0,0 @@
-toContain('')
- ->not->toContain('toContain('application-settings-navigation')
- ->toContain('Notification settings')
- ->toContain("'label' => 'Email'")
- ->toContain("'icon' => 'mail'")
- ->toContain("'label' => 'Discord'")
- ->toContain("'label' => 'Telegram'")
- ->toContain("'label' => 'Slack'")
- ->toContain("'label' => 'Pushover'")
- ->toContain("'label' => 'Webhook'");
-
- foreach (['discord', 'telegram', 'slack', 'pushover'] as $channel) {
- expect(public_path("svgs/{$channel}.svg"))->toBeFile();
- }
-
- expect(file_get_contents(public_path('svgs/pushover.svg')))
- ->toContain('not->toContain('toContain("'brandIcon' => 'discord'")
- ->toContain("'brandIcon' => 'telegram'")
- ->toContain("'brandIcon' => 'slack'")
- ->toContain("'brandIcon' => 'pushover'")
- ->not->toContain("'color' =>");
-
- expect($navbar)
- ->toContain("request()->routeIs('notifications.*')")
- ->not->toContain("['label' => 'Discord', 'route' => 'notifications.discord'");
-});
-
-it('keeps telegram forum topics separate from event multiselects', function () {
- $grid = file_get_contents(resource_path('views/components/notification/event-grid.blade.php'));
- $telegram = file_get_contents(resource_path('views/livewire/notifications/telegram.blade.php'));
-
- expect($telegram)
- ->toContain('channel="telegram" threaded');
-
- expect($grid)
- ->toContain('title="Notification events"')
- ->toContain('title="Forum topics"')
- ->toContain('Enable one or more events above to assign forum topic IDs.')
- ->toContain('$enabledThreadEvents')
- ->not->toContain('label="{{ $event[\'label\'] }} thread ID"')
- ->not->toContain('border-l border-neutral-200 pl-3');
-});
-
-it('renders settings section descriptions in the header', function () {
- $section = file_get_contents(resource_path('views/components/application/settings-section.blade.php'));
-
- expect($section)
- ->toContain('filled($description)')
- ->toContain('{{ $description }}');
-});
-
-it('uses action buttons and browser validation for notification channel state', function () {
- $actions = file_get_contents(resource_path('views/components/notification/channel-actions.blade.php'));
-
- expect($actions)
- ->toContain('{{ $enabled ? \'Disable\' : \'Enable\' }}')
- ->toContain('reportValidity()')
- // @js() must live on plain HTML (x-data), not on attributes —
- // Blade leaves @js uncompiled inside component tag attributes, which breaks Alpine.
- ->toContain('enabled: @js((bool) $enabled)')
- ->toContain('enabledProperty: @js($enabledProperty)')
- ->toContain('toggleMethod: @js($toggleMethod)')
- ->toContain('testMethod: @js($testMethod)')
- ->toContain('$wire.$set(enabledProperty, !enabled)')
- ->toContain('$wire.$call(toggleMethod)')
- ->toContain('$wire.$call(testMethod)')
- ->not->toContain('$wire.$set(@js($enabledProperty)');
-
- foreach (['discord', 'telegram', 'slack', 'pushover', 'webhook'] as $channel) {
- $view = file_get_contents(resource_path("views/livewire/notifications/{$channel}.blade.php"));
-
- expect($view)
- ->toContain('not->toContain("id=\"{$channel}Enabled\"");
- }
-});
diff --git a/tests/Feature/PageTitleXlHiddenTest.php b/tests/Feature/PageTitleXlHiddenTest.php
deleted file mode 100644
index 29a9b43713..0000000000
--- a/tests/Feature/PageTitleXlHiddenTest.php
+++ /dev/null
@@ -1,71 +0,0 @@
-toContain("'titleOnDesktop' => false")
- ->toContain("'lg:hidden' => ! \$titleOnDesktop")
- ->toContain('lg:h-12')
- ->not->toContain('lg:h-10');
-});
-
-test('fixed layer-2 spacers match the fixed bar height', function () {
- $paths = [
- resource_path('views/components/dashboard/navbar.blade.php'),
- resource_path('views/livewire/server/navbar.blade.php'),
- 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 ($paths as $path) {
- $blade = file_get_contents($path);
- expect($blade)->toContain('lg:h-12');
- expect($blade)->not->toContain('lg:h-10');
- expect($blade)->not->toContain('lg:block lg:h-5');
- }
-});
-
-test('resource in-flow titles show until the desktop topbar takes over at lg', function () {
- $paths = [
- resource_path('views/livewire/server/navbar.blade.php'),
- 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 ($paths as $path) {
- $blade = file_get_contents($path);
-
- expect($blade)
- ->toContain('w-full lg:hidden')
- ->toContain('w-full md:hidden')
- ->not->toContain('w-full xl:hidden');
- }
-});
-
-test('slide-over shells do not reserve layout space', function () {
- expect(file_get_contents(resource_path('views/components/slide-over.blade.php')))
- ->toContain("'class' => 'contents'")
- ->not->toContain('relative w-auto h-auto');
-});
-
-test('collection index headers stack so title and actions never overlap', function () {
- $paths = [
- resource_path('views/livewire/server/index.blade.php'),
- resource_path('views/livewire/project/index.blade.php'),
- resource_path('views/source/all.blade.php'),
- resource_path('views/livewire/destination/index.blade.php'),
- resource_path('views/livewire/storage/index.blade.php'),
- ];
-
- foreach ($paths as $path) {
- expect(file_get_contents($path))
- ->toContain('flex-col gap-3 sm:flex-row')
- ->toContain('text-[24px]!');
- }
-});
diff --git a/tests/Feature/PrivateKeyIndexNavbarActionsTest.php b/tests/Feature/PrivateKeyIndexNavbarActionsTest.php
deleted file mode 100644
index 55e96cb8bc..0000000000
--- a/tests/Feature/PrivateKeyIndexNavbarActionsTest.php
+++ /dev/null
@@ -1,70 +0,0 @@
-toContain("'titleOnDesktop' => false")
- ->toContain(':titleOnDesktop="$titleOnDesktop"');
-
- expect($notificationNavbar)
- ->toContain("'titleOnDesktop' => false")
- ->toContain(':titleOnDesktop="$titleOnDesktop"');
-});
-
-test('security list views place create actions in collection card headers', function () {
- $pages = [
- resource_path('views/livewire/security/private-key/index.blade.php') => [
- 'New private key',
- 'New key',
- 'Delete unused keys',
- 'Delete unused',
- ],
- resource_path('views/livewire/security/cloud-provider-tokens.blade.php') => [
- 'New token',
- ],
- resource_path('views/livewire/security/cloud-init-scripts.blade.php') => [
- 'New script',
- ],
- ];
-
- foreach ($pages as $path => $labels) {
- $blade = file_get_contents($path);
-
- expect($blade)
- ->toContain('toContain('')
- ->not->toContain('');
-
- foreach ($labels as $label) {
- expect($blade)->toContain($label);
- }
- }
-});
-
-test('layer-2 navbar stacks tabs and actions on small screens', function () {
- $navbar = file_get_contents(resource_path('views/components/dashboard/navbar.blade.php'));
-
- expect($navbar)
- ->toContain('flex-col gap-2 sm:flex-row')
- ->toContain('[&_.button]:whitespace-nowrap');
-});
-
-test('security detail views keep the resource title on desktop', function () {
- $shows = [
- resource_path('views/livewire/security/private-key/show.blade.php'),
- resource_path('views/livewire/security/cloud-provider-token/show.blade.php'),
- resource_path('views/livewire/security/cloud-init-script/show.blade.php'),
- ];
-
- foreach ($shows as $path) {
- expect(file_get_contents($path))
- ->toContain(':titleOnDesktop="true"')
- ->toContain('');
- }
-});
diff --git a/tests/Feature/ProcessDialogTest.php b/tests/Feature/ProcessDialogTest.php
deleted file mode 100644
index 66e1e9ccc8..0000000000
--- a/tests/Feature/ProcessDialogTest.php
+++ /dev/null
@@ -1,80 +0,0 @@
-toContain("'class' => 'contents'")
- ->toContain('processDialogOpen')
- ->toContain('role="dialog"')
- ->toContain('items-center justify-center')
- ->toContain('min-h-[min(70dvh,28rem)]')
- ->toContain('sm:min-w-[32rem]')
- ->not->toContain('translate-x-full');
-});
-
-test('service database and proxy startup use the centered process dialog', function () {
- $paths = [
- resource_path('views/livewire/project/service/heading.blade.php'),
- resource_path('views/livewire/project/database/heading.blade.php'),
- resource_path('views/livewire/server/navbar.blade.php'),
- ];
-
- foreach ($paths as $path) {
- expect(file_get_contents($path))
- ->toContain('toContain('processDialogOpen = true')
- ->not->toContain('toContain('.process-dialog')
- ->toContain('.process-dialog-body')
- ->toContain('min-height: min(70dvh, 28rem)');
-});
-
-test('runtime log terminal fills the available dialog width', function () {
- $view = file_get_contents(resource_path('views/livewire/project/shared/get-logs.blade.php'));
-
- expect($view)->toContain(" \$collapsible])>");
-});
-
-test('process and log viewers use centered dialogs', function () {
- $paths = [
- resource_path('views/livewire/project/service/index.blade.php'),
- resource_path('views/livewire/project/database/keydb/general.blade.php'),
- resource_path('views/livewire/project/database/redis/general.blade.php'),
- resource_path('views/livewire/project/database/postgresql/general.blade.php'),
- resource_path('views/livewire/project/database/clickhouse/general.blade.php'),
- resource_path('views/livewire/project/database/mongodb/general.blade.php'),
- resource_path('views/livewire/project/database/dragonfly/general.blade.php'),
- resource_path('views/livewire/project/database/mariadb/general.blade.php'),
- resource_path('views/livewire/project/database/mysql/general.blade.php'),
- resource_path('views/livewire/server/security/patches.blade.php'),
- resource_path('views/livewire/server/cloudflare-tunnel.blade.php'),
- ];
-
- foreach ($paths as $path) {
- expect(file_get_contents($path))
- ->toContain('
not->toContain('not->toContain('toContain("'open' => false")
- ->toContain('processDialogOpen: @js($open)');
-});
diff --git a/tests/Feature/ProfileAppearanceNavigationTest.php b/tests/Feature/ProfileAppearanceNavigationTest.php
deleted file mode 100644
index fdba4e9916..0000000000
--- a/tests/Feature/ProfileAppearanceNavigationTest.php
+++ /dev/null
@@ -1,56 +0,0 @@
-toContain("Route::get('/profile/appearance', ProfileAppearance::class)->name('profile.appearance')")
- ->and($profileView)
- ->not->toContain(' ')
- ->not->toContain('Profile \n ');
-});
-
-it('opens the email change form without a Livewire request', function () {
- $profileView = file_get_contents(resource_path('views/livewire/profile/index.blade.php'));
-
- expect($profileView)
- ->toContain('@click="openEmailModal()"')
- ->toContain('this.$refs.newEmailInput?.focus()')
- ->toContain('x-ref="newEmailInput"')
- ->toContain('x-show="emailModalOpen"')
- ->toContain('x-teleport="body"')
- ->toContain('Change email')
- ->toContain('Send code')
- ->toContain('Verify email')
- ->not->toContain('>Cancel')
- ->not->toContain('wire:click="showEmailChangeForm"');
-});
-
-it('shows the two-factor authentication state as a header badge', function () {
- $profileView = file_get_contents(resource_path('views/livewire/profile/index.blade.php'));
-
- expect($profileView)
- ->toContain(' ');
-});
-
-it('offers full and centered page width preferences on the profile appearance view', function () {
- $appearanceView = file_get_contents(resource_path('views/livewire/profile/appearance.blade.php'));
- $appLayout = file_get_contents(resource_path('views/layouts/app.blade.php'));
-
- expect($appearanceView)
- ->not->toContain(' ')
- ->toContain('Color theme')
- ->toContain('Page width')
- ->toContain("pageWidth: localStorage.getItem('pageWidth') || 'full'")
- ->toContain("['value' => 'full'")
- ->toContain("['value' => 'centered'")
- ->toContain("@click=\"setWidth('{{ \$option['value'] }}')\"")
- ->toContain("localStorage.setItem('pageWidth', width)")
- ->not->toContain('Interface density')
- ->not->toContain('setZoom(')
- ->and($appLayout)
- ->toContain("pageWidth: localStorage.getItem('pageWidth') || 'full'")
- ->toContain('@page-width-changed.window="pageWidth = $event.detail"')
- ->toContain("pageWidth === 'centered' ? 'mx-auto max-w-[1400px]' : 'max-w-none'");
-});
diff --git a/tests/Feature/ProjectEnvironmentPageHeaderTest.php b/tests/Feature/ProjectEnvironmentPageHeaderTest.php
deleted file mode 100644
index 29e64b66dc..0000000000
--- a/tests/Feature/ProjectEnvironmentPageHeaderTest.php
+++ /dev/null
@@ -1,65 +0,0 @@
-toBeFalse();
-
- foreach ($projectPages as $page) {
- expect(file_get_contents(resource_path($page)))
- ->not->toContain('not->toContain('resource-heading-navbar');
- }
-});
-
-it('gives every project and environment page an in-flow title', function () use ($projectPages) {
- foreach ($projectPages as $page) {
- expect(file_get_contents(resource_path($page)))
- ->toContain('')
- ->toContain('class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim"');
- }
-});
-
-it('keeps the environment clone action in the page header', function () {
- $view = file_get_contents(resource_path('views/livewire/project/environment-edit.blade.php'));
-
- expect($view)
- ->toContain('{{ $environment->name }} ')
- ->toContain('Environment settings in {{ $project->name }}')
- ->toContain('Clone environment')
- // The action moved out of the card header into the page header.
- ->toMatch('/]*>.*Clone environment.*<\/header>/s');
-});
-
-it('does not repeat the resources action outside the resources page', function () {
- foreach (['views/livewire/project/environment-edit.blade.php', 'views/livewire/project/clone-me.blade.php'] as $page) {
- expect(file_get_contents(resource_path($page)))->not->toContain('New resource');
- }
-
- expect(file_get_contents(resource_path('views/livewire/project/resource/index.blade.php')))
- ->toContain('New resource');
-});
-
-it('uses a stable class for the environment resource count on mobile', function () {
- $view = file_get_contents(resource_path('views/livewire/project/show.blade.php'));
- $css = file_get_contents(resource_path('css/app.css'));
-
- expect($view)
- ->toContain('class="environment-resource-count">Resources
')
- ->toContain('class="environment-resource-count text-[12px]');
-
- expect($css)
- ->toContain('.environments-table-grid .environment-resource-count')
- ->not->toContain('.environments-table-grid > :nth-child(2)');
-});
diff --git a/tests/Feature/ProjectNewSelectAccentTest.php b/tests/Feature/ProjectNewSelectAccentTest.php
deleted file mode 100644
index 13da2e5c27..0000000000
--- a/tests/Feature/ProjectNewSelectAccentTest.php
+++ /dev/null
@@ -1,9 +0,0 @@
-not->toContain('dark:group-hover:text-warning')
- ->and(substr_count($view, 'class="button button-highlighted ml-auto"'))->toBe(4);
-});
diff --git a/tests/Feature/ProxyConfigurationLoadingStateTest.php b/tests/Feature/ProxyConfigurationLoadingStateTest.php
deleted file mode 100644
index f5779e0ac5..0000000000
--- a/tests/Feature/ProxyConfigurationLoadingStateTest.php
+++ /dev/null
@@ -1,30 +0,0 @@
-toContain('wire:loading.attr="disabled"')
- ->toContain('wire:target="submit,resetProxyConfiguration"')
- ->toContain('wire:loading.class="pointer-events-none opacity-50"')
- ->toContain('Updating proxy configuration')
- ->toContain('aria-live="polite"');
-});
-
-it('loads only the compose file from the frontend and shows the shared loading indicator', function () {
- $page = file_get_contents(resource_path('views/livewire/server/proxy/show.blade.php'));
- $proxy = file_get_contents(resource_path('views/livewire/server/proxy.blade.php'));
- $component = file_get_contents(app_path('Livewire/Server/Proxy.php'));
-
- expect($page)
- ->toContain(' ')
- ->not->toContain(' ');
-
- expect($proxy)
- ->toContain('x-init="$wire.loadProxyConfiguration()"')
- ->toContain('wire:loading.flex wire:target="loadProxyConfiguration"')
- ->toContain(' ');
-
- expect($component)
- ->not->toContain('$this->loadProxyConfiguration();');
-});
diff --git a/tests/Feature/ResourceDetailsVisibilityTest.php b/tests/Feature/ResourceDetailsVisibilityTest.php
deleted file mode 100644
index e11c86fe99..0000000000
--- a/tests/Feature/ResourceDetailsVisibilityTest.php
+++ /dev/null
@@ -1,63 +0,0 @@
-put('default', new MessageBag);
- view()->share('errors', $errors);
-});
-
-it('keeps the resource details helper text visible below the modal header', function () {
- $html = view('livewire.project.shared.resource-details', [
- 'resource' => (object) [
- 'name' => 'Crash Loop Example',
- 'uuid' => 'crashloop',
- ],
- 'environment_uuid' => null,
- 'environment_name' => null,
- 'project_uuid' => null,
- 'project_name' => null,
- 'server_uuid' => null,
- 'server_name' => null,
- 'stack_applications' => [],
- 'stack_databases' => [],
- ])->render();
-
- expect($html)
- ->toContain('readonly')
- ->toContain('pt-1')
- ->not->toContain('-mt-4');
-});
-
-it('renders copy fields as visible readonly controls with an accessible copy action', function () {
- $html = Blade::render(' ');
-
- expect($html)
- ->toContain('label class="flex gap-1 items-center mb-1 text-sm font-medium text-black dark:text-white"')
- ->toContain('readonly')
- ->toContain('x-data="copyButton"')
- ->toContain('input-with-copy-button')
- ->toContain('aria-label="Copy to clipboard"')
- ->toContain('title="Copy to clipboard"');
-});
-
-it('uses the shared copy button for newly issued api tokens', function () {
- $blade = file_get_contents(resource_path('views/livewire/security/api-tokens.blade.php'));
-
- expect($blade)
- ->toContain('not->toContain('navigator.clipboard');
-});
-
-it('keeps copy button padding above settings-workspace input overrides', function () {
- $css = file_get_contents(resource_path('css/app.css'));
-
- expect($css)
- ->toContain('.input.input-with-copy-button')
- ->toContain('.application-settings-workspace .input.input-with-copy-button')
- ->toContain('.application-settings-form .input.input-with-copy-button')
- ->toContain('padding-right: 2.5rem');
-});
diff --git a/tests/Feature/ResourceHeadingOverflowTest.php b/tests/Feature/ResourceHeadingOverflowTest.php
deleted file mode 100644
index e8f2b61e61..0000000000
--- a/tests/Feature/ResourceHeadingOverflowTest.php
+++ /dev/null
@@ -1,36 +0,0 @@
-toContain('data-resource-heading-overflow')
- ->toContain('measureInlineWidth')
- ->toContain('availableWidth')
- ->toContain('reserved += 200')
- ->toContain('hudSiblings')
- ->toContain('Actions')
- ->toContain('listbox-panel top-full! right-0! left-auto!')
- ->toContain("\$dispatch('resource-actions-toggled', { open })");
-
- expect($css)
- ->toContain('.resource-heading-overflow-items')
- ->toContain('.resource-heading-overflow-items.is-measuring')
- ->toContain('.resource-heading-overflow.is-collapsed');
-});
-
-it('uses the overflow group for application, service, database, and server desktop actions', function () {
- $files = [
- resource_path('views/livewire/project/application/heading.blade.php') => 'application-desktop-actions',
- resource_path('views/livewire/project/service/heading.blade.php') => 'service-desktop-actions',
- resource_path('views/livewire/project/database/heading.blade.php') => 'database-desktop-actions',
- resource_path('views/livewire/server/navbar.blade.php') => 'server-desktop-actions',
- ];
-
- foreach ($files as $path => $id) {
- expect(file_get_contents($path))
- ->toContain('toContain($id);
- }
-});
diff --git a/tests/Feature/ResourceHeadingUnifiedNavbarTest.php b/tests/Feature/ResourceHeadingUnifiedNavbarTest.php
deleted file mode 100644
index e68b9fde7f..0000000000
--- a/tests/Feature/ResourceHeadingUnifiedNavbarTest.php
+++ /dev/null
@@ -1,737 +0,0 @@
-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 current version')
- ->toContain('Pull latest and restart')
- ->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 split control', 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('toContain('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 current version')
- ->toContain('Pull latest and restart')
- ->toContain("\$wire.dispatch('pullAndRestartEvent')")
- ->not->toContain('Pull Latest Images & Restart')
- ->not->toContain('Restart (pull latest)')
- ->not->toContain('before("@teleport('#resource-action-hud-slot')")->toString();
-
- expect($mobile)
- ->toContain('Restart')
- ->toContain('Restart (pull latest)')
- ->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 current version'))
- ->toBeLessThan(strpos($actions, 'Pull latest and restart'))
- ->and(strpos($actions, 'Pull latest and restart'))
- ->toBeLessThan(strpos($actions, 'Stop'));
-});
-
-it('promotes the primary application action in the desktop split control', 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('id="application-desktop-actions"')
- ->toContain('')
- ->toContain('Deploy')
- ->toContain('Deploy (without cache)')
- ->not->toContain('Redeploy')
- ->toContain('force_deploy_without_cache')
- ->toContain('deploy(true)');
-
- $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('')
- ->toString();
-
- expect($mobileActions)->toContain('application-mobile-stop-trigger')
- ->and($desktopExitedActions)->toContain('application-mobile-stop-trigger')
- ->and($mobileActions)->toContain('Deploy (without cache)')
- ->and(strrpos($mobileActions, 'Deploy (without cache)'))
- ->toBeLessThan(strrpos($mobileActions, 'application-mobile-stop-trigger'));
-});
-
-it('uses the same deploy labels in the mobile and desktop application action menus', 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();
- $mobileActions = str($heading)->after('id="application-mobile-actions"')->before("@teleport('#resource-action-hud-slot')")->toString();
-
- expect($actions)
- ->toContain('Deploy')
- ->toContain('Deploy (without cache)')
- ->not->toContain('Redeploy')
- ->toContain("str(\$application->status)->startsWith('running') ? 'force_deploy_without_cache' : 'deploy(true)'")
- ->and($mobileActions)
- ->toContain('Deploy')
- ->toContain('Deploy (without cache)')
- ->not->toContain('Redeploy');
-});
-
-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('promotes a primary action 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);
- $control = str($heading)->after("id=\"{$id}\"")->before('')->toString();
-
- expect($control)
- ->toContain('
toContain('listbox-option');
- }
-});
-
-it('links the Traefik dashboard beside the server actions instead of inside the dropdown', function () {
- $navbar = file_get_contents(resource_path('views/livewire/server/navbar.blade.php'));
- $desktopActions = str($navbar)->after('id="server-desktop-actions"')->before('')->toString();
- $mobileActions = str($navbar)->after('id="server-mobile-actions"')->before('')->toString();
-
- expect($desktopActions)->not->toContain('Traefik')
- ->and($mobileActions)->not->toContain('Traefik')
- ->and($navbar)->toContain('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('toContain('toContain('Restart')
- ->toContain('Stop')
- ->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', 'Analytics']")
- ->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 in the modal body', function () {
- $previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php'));
-
- expect($previews)
- ->toContain('wire:click="load_prs"')
- ->toContain('wire:loading.remove wire:target="load_prs"')
- ->toContain('wire:loading wire:target="load_prs"')
- ->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'));
-
- // The desktop rail is fixed below the 3rem top bar and fills the rest of the viewport.
- preg_match('/@media \(min-width: 1280px\) \{(?:(?!@media).)*?\n\s*\.application-settings-navigation \{([^}]*)\}/s', $css, $matches);
- $railRule = $matches[1] ?? '';
-
- expect($sidebar)->toContain('application-settings-navigation')
- ->and($railRule)->toContain('position: fixed;')
- ->and($railRule)->toContain('top: 3rem;')
- ->and($railRule)->toContain('height: calc(100dvh - 3rem);')
- ->and($railRule)->toContain('overflow-y: auto;');
-});
-
-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('welds the deployment log sidebar to the main sidebar', 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)->not->toContain(':flush="true"')
- ->and($css)->toContain('left: var(--sidebar-w, 14rem);')
- ->and($css)->toContain('position: fixed;');
-});
-
-it('uses the same mobile heading gap on application pages', function () {
- $views = [
- resource_path('views/livewire/project/application/configuration.blade.php'),
- resource_path('views/livewire/project/application/backup/index.blade.php'),
- resource_path('views/livewire/project/application/backup/show.blade.php'),
- resource_path('views/livewire/project/application/deployment/show.blade.php'),
- resource_path('views/livewire/project/shared/logs.blade.php'),
- resource_path('views/livewire/project/shared/execute-container-command.blade.php'),
- ];
-
- foreach ($views as $view) {
- expect(file_get_contents($view))
- ->toContain('application-settings-workspace mt-4')
- ->toContain('lg:mt-0');
- }
-
- expect(file_get_contents(resource_path('views/livewire/project/application/deployment/index.blade.php')))
- ->toContain("'mt-4 max-w-none lg:mt-0' => ! \$embedded");
-});
-
-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('min-h-[calc(100dvh-7.5rem)]')
- ->toContain('xl:h-[32rem] xl:min-h-0 xl:flex-none')
- ->toContain('overflow-hidden');
-});
-
-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="mb-3 w-full xl:hidden"', '')->toString();
- $mobileServiceActions = str($service)->between('
', '@teleport')->toString();
-
- expect($mobileServiceTitle)
- ->toContain('
toContain('toContain('relative flex w-full min-w-0 items-center gap-2')
- ->toContain('compact')
- ->not->toContain('full-width');
-
- expect($mobileServiceActions)
- ->not->toContain('toContain('public bool $compact = false');
-
- expect($serviceLinks)
- ->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('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('toContain("'chevron-down' => 'not->toContain('chevron-down\' => \'toContain('x-show="item.description"')
- ->toContain('x-text="item.description"')
- ->not->toContain('item.description || item.fqdn || item.uuid')
- ->not->toContain('item.fqdn || item.uuid');
-});
-
-test('resource grid does not show a placeholder when a domain is missing', function () {
- $view = file_get_contents(resource_path('views/livewire/project/resource/index.blade.php'));
- $gridView = explode('x-show="viewMode === \'grid\'"', $view, 2)[1];
-
- expect($gridView)
- ->not->toContain('x-show="!item.fqdn"')
- ->not->toContain('>-');
-});
diff --git a/tests/Feature/ResourceIndexFiltersTest.php b/tests/Feature/ResourceIndexFiltersTest.php
deleted file mode 100644
index de6375465f..0000000000
--- a/tests/Feature/ResourceIndexFiltersTest.php
+++ /dev/null
@@ -1,38 +0,0 @@
-toContain('typeFilters: []')
- ->toContain('tagFilters: []')
- ->toContain('serverFilters: []')
- ->toContain('statusFilters: []')
- ->toContain('get filterGroups()')
- ->toContain("label: 'Tags'")
- ->toContain("label: 'Servers'")
- ->toContain("label: 'Statuses'")
- ->toContain('toggleFilter(group.key, option.value)')
- ->toContain('clearFilters()')
- ->toContain('Clear filters')
- ->not->toContain("typeFilter: 'all'");
-});
-
-it('uses stable classes to hide resource table columns on mobile', function () {
- $view = file_get_contents(resource_path('views/livewire/project/resource/index.blade.php'));
- $css = file_get_contents(resource_path('css/app.css'));
-
- expect($view)
- ->toContain('class="resource-type"')
- ->toContain('class="resource-server"')
- ->toContain('class="resource-type truncate')
- ->toContain('class="resource-server truncate')
- ->toContain('class="mobile-resource-domain min-w-0"');
-
- expect($css)
- ->toContain('.environment-resource-grid .resource-type,')
- ->toContain('.environment-resource-grid .resource-server')
- ->toContain('.environment-resource-grid .mobile-resource-domain')
- ->not->toContain('.environment-resource-grid > :nth-child(2),')
- ->not->toContain('.environment-resource-grid > :nth-child(5)');
-});
diff --git a/tests/Feature/ResourceLimitsCpuDocsHeaderTest.php b/tests/Feature/ResourceLimitsCpuDocsHeaderTest.php
deleted file mode 100644
index cfa95e897d..0000000000
--- a/tests/Feature/ResourceLimitsCpuDocsHeaderTest.php
+++ /dev/null
@@ -1,16 +0,0 @@
-toContain('')
- ->toContain('Docker CPU constraints')
- ->toContain('https://docs.docker.com/engine/containers/resource_constraints/#cpu')
- ->toContain('class="button"')
- ->toContain('name="external-link"')
- ->not->toContain('mt-4 inline-flex items-center gap-1 text-xs text-neutral-500');
-});
diff --git a/tests/Feature/ResourceLimitsIconTest.php b/tests/Feature/ResourceLimitsIconTest.php
deleted file mode 100644
index f8dfa89ee2..0000000000
--- a/tests/Feature/ResourceLimitsIconTest.php
+++ /dev/null
@@ -1,21 +0,0 @@
-toMatch("/'Resource Limits'\\s*=>\\s*'cpu'/")
- ->not->toMatch("/'Resource Limits'\\s*=>\\s*'subscription'/");
-
- expect($database)
- ->toMatch("/'label'\\s*=>\\s*'Resource Limits'[\\s\\S]{0,120}?'icon'\\s*=>\\s*'cpu'/")
- ->not->toMatch("/'label'\\s*=>\\s*'Resource Limits'[\\s\\S]{0,120}?'icon'\\s*=>\\s*'subscription'/");
-
- expect($reicon)
- ->toContain("'cpu' =>");
-});
diff --git a/tests/Feature/SecuritySettingsIconsTest.php b/tests/Feature/SecuritySettingsIconsTest.php
deleted file mode 100644
index 6bf9da201c..0000000000
--- a/tests/Feature/SecuritySettingsIconsTest.php
+++ /dev/null
@@ -1,15 +0,0 @@
-toContain("'label' => 'Private Keys'", "'icon' => 'keys'")
- ->toContain("'label' => 'Cloud Tokens'", "'icon' => 'cloud'")
- ->toContain("'label' => 'Cloud-Init Scripts'", "'icon' => 'file-content'")
- ->toContain("'label' => 'API Tokens'", "'icon' => 'code'")
- ->and($icons)
- ->toContain("'cloud' =>")
- ->toContain("'code' =>");
-});
diff --git a/tests/Feature/SecuritySettingsNavigationTest.php b/tests/Feature/SecuritySettingsNavigationTest.php
deleted file mode 100644
index 9eab92cc28..0000000000
--- a/tests/Feature/SecuritySettingsNavigationTest.php
+++ /dev/null
@@ -1,33 +0,0 @@
-toContain('')
- ->not->toContain('toContain('application-settings-navigation')
- ->toContain("'label' => 'Private Keys'")
- ->toContain("'label' => 'Cloud Tokens'")
- ->toContain("'label' => 'Integration Tokens'")
- ->toContain("'label' => 'Cloud-Init Scripts'")
- ->toContain("'label' => 'API Tokens'");
-
- expect($navbar)
- ->toContain("request()->routeIs('security.*')")
- ->not->toContain("['label' => 'API Tokens', 'route' => 'security.api-tokens'");
-});
diff --git a/tests/Feature/SelectControlStyleTest.php b/tests/Feature/SelectControlStyleTest.php
deleted file mode 100644
index 1bd3d6f0fd..0000000000
--- a/tests/Feature/SelectControlStyleTest.php
+++ /dev/null
@@ -1,9 +0,0 @@
-toMatch('/\.application-settings-workspace \.select,\s*\.application-settings-form \.select \{\s*padding-right: 2\.5rem;/')
- ->toMatch('/@media \(max-width: 767px\) \{\s*\.application-settings-workspace \.select,\s*\.application-settings-form \.select \{\s*font-size: 0\.75rem !important;/');
-});
diff --git a/tests/Feature/SentinelUnsavedBarFlashTest.php b/tests/Feature/SentinelUnsavedBarFlashTest.php
deleted file mode 100644
index 937d7cad5c..0000000000
--- a/tests/Feature/SentinelUnsavedBarFlashTest.php
+++ /dev/null
@@ -1,207 +0,0 @@
-toContain('x-unsaved-bar')
- ->toContain('targets="sentinelCustomUrl,sentinelToken"')
- ->not->toContain('trafficTopn')
- ->not->toContain('sentinelMetricsRefreshRateSeconds')
- ->not->toContain('sentinelMetricsHistoryDays')
- ->not->toContain('sentinelPushIntervalSeconds')
- ->not->toMatch('/x-unsaved-bar\s+action="submit"\s*\/>/');
-});
-
-test('metrics unsaved bar scopes dirty tracking to metrics collection fields', function () {
- $contents = file_get_contents(resource_path('views/livewire/server/charts.blade.php'));
-
- expect($contents)
- ->toContain('x-unsaved-bar action="saveMetricsSettings"')
- ->toContain('targets="sentinelMetricsRefreshRateSeconds,sentinelMetricsHistoryDays,sentinelPushIntervalSeconds"');
-});
-
-test('unsaved bar component accepts optional wire:target list', function () {
- $path = resource_path('views/components/unsaved-bar.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain("'targets' => null")
- ->toContain('wire:target="{{ $targets }}"');
-});
-
-test('unsaved bar delays show and hides while loading to avoid instant-save flash', function () {
- $path = resource_path('views/components/unsaved-bar.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain('wire:dirty.class="is-dirty"')
- ->toContain('wire:loading.class="is-saving"')
- ->toContain('[&.is-dirty]:delay-300')
- ->toContain('delay-0');
-});
-
-test('unsaved bar uses a stable transition for its entrance', function () {
- $contents = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
-
- expect($contents)
- ->toContain('scale-95')
- ->toContain('transition-[opacity,transform,scale]')
- ->toContain('duration-300')
- ->toContain('ease-[cubic-bezier(0.16,1,0.3,1)]')
- ->toContain('[&.is-dirty]:scale-100')
- ->not->toContain('[&.is-dirty]:animate-in');
-});
-
-test('unsaved bar transitions out without restarting its entrance animation', function () {
- $contents = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
-
- expect($contents)
- ->toContain('[&.is-saving]:translate-y-6')
- ->toContain('[&.is-saving]:scale-95')
- ->toContain('[&.is-saving]:opacity-0')
- ->toContain('[&.is-saving]:duration-200')
- ->toContain('[&.is-saving]:ease-in')
- ->toContain('[&.is-saving]:pointer-events-none')
- ->not->toContain('[&.is-saving]:animate-out');
-});
-
-test('unsaved bar saves with enter and shows the shortcut on the save button', function () {
- $contents = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
-
- expect($contents)
- ->toContain('@keydown.enter.window')
- ->toContain("classList.contains('is-dirty')")
- ->toContain('$wire.{{ $action }}()')
- ->toContain('toContain('Enter ');
-});
-
-test('unsaved bar keyboard shortcut remains visible in light mode', function () {
- $contents = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
-
- expect($contents)
- ->toContain('border-current/20 bg-current/10')
- ->toContain('text-current')
- ->not->toContain('text-coollabs-200');
-});
-
-test('unsaved bar uses a light surface in light mode', function () {
- $contents = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
-
- expect($contents)
- ->toContain('border-neutral-200 bg-white')
- ->toContain('text-neutral-800 dark:text-fg')
- ->toContain('bg-neutral-100')
- ->toContain('dark:bg-white/[0.07]');
-});
-
-test('unsaved bar stays above floating notifications so save actions remain accessible', function () {
- $unsavedBar = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
- $popup = file_get_contents(resource_path('views/components/popup-small.blade.php'));
-
- expect($unsavedBar)
- ->toContain('z-[1000]')
- ->and($popup)->toContain('z-999');
-});
-
-test('unsaved bar stays above the mobile keyboard', function () {
- $contents = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
-
- expect($contents)
- ->toContain('window.visualViewport')
- ->toContain('keyboardInset')
- ->toContain('--keyboard-inset')
- ->toContain('removeEventListener');
-});
-
-test('settings general unsaved bar excludes auto-saving instance timezone', function () {
- $path = resource_path('views/livewire/settings/index.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain('targets="fqdn,instance_name,public_ipv4,public_ipv6,dev_helper_version"')
- ->toContain('id="instance_timezone"')
- ->toContain('onChange="submit"')
- ->toContain('x-forms.searchable-listbox')
- ->not->toMatch('/x-unsaved-bar\s+action="submit"\s*\/>/');
-});
-
-test('settings general instance timezone uses searchable listbox for label alignment with name', function () {
- $path = resource_path('views/livewire/settings/index.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain('x-forms.searchable-listbox id="instance_timezone"')
- ->toContain('id="instance_name"')
- ->not->toContain('selectTimezone(timezone)');
-});
-
-test('sentinel custom docker image x-init only sets wire when a value exists', function () {
- $path = resource_path('views/livewire/server/sentinel.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain("x-init=\"if (customImage) { \$wire.set('sentinelCustomDockerImage', customImage) }\"")
- ->not->toContain("x-init=\"\$wire.set('sentinelCustomDockerImage', customImage)\"");
-});
-
-test('sentinel custom docker image does not rerender while typing and has an explicit apply action', function () {
- $contents = file_get_contents(resource_path('views/livewire/server/sentinel.blade.php'));
-
- expect($contents)
- ->toContain('async applyCustomImage()')
- ->toContain("await \$wire.set('sentinelCustomDockerImage', this.customImage || null)")
- ->toContain('await $wire.restartSentinel()')
- ->toContain('Apply and restart')
- ->not->toContain('@input.debounce.500ms="saveCustomImage()"');
-});
-
-/**
- * Instant-save listboxes (e.g. MCP server) entangle + call instantSave. Until the
- * round-trip finishes, the component is dirty — so an unscoped unsaved bar flashes.
- * Pages that mix instantSave and explicit-save fields must pass targets.
- */
-test('settings advanced unsaved bar scopes dirty tracking away from instantSave fields', function () {
- $path = resource_path('views/livewire/settings/advanced.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain('x-unsaved-bar')
- ->toContain('targets="custom_dns_servers,allowed_ips,webhook_allowed_internal_hosts,webhook_allow_localhost,domain_connect_private_key"')
- ->toContain('onChange="instantSave"')
- ->not->toMatch('/x-unsaved-bar\s+action="submit"\s*\/>/');
-});
-
-test('settings updates unsaved bar scopes dirty tracking away from instantSave fields', function () {
- $path = resource_path('views/livewire/settings/updates.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain('targets="update_check_frequency,auto_update_frequency"')
- ->not->toMatch('/x-unsaved-bar\s+action="submit"\s*\/>/');
-});
-
-test('docker cleanup unsaved bar scopes dirty tracking away from instantSave fields', function () {
- $path = resource_path('views/livewire/server/docker-cleanup.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain('targets="dockerCleanupFrequency,dockerCleanupThreshold"')
- ->not->toMatch('/x-unsaved-bar\s+action="submit"\s*\/>/');
-});
-
-test('application advanced gpu unsaved bar scopes dirty tracking to gpu fields only', function () {
- $path = resource_path('views/livewire/project/application/advanced.blade.php');
- $contents = file_get_contents($path);
-
- expect($contents)
- ->toContain('targets="gpuDriver,gpuCount,gpuDeviceIds,gpuOptions"');
-});
diff --git a/tests/Feature/ServerCreatePageLayoutTest.php b/tests/Feature/ServerCreatePageLayoutTest.php
deleted file mode 100644
index 3e8a8b48b3..0000000000
--- a/tests/Feature/ServerCreatePageLayoutTest.php
+++ /dev/null
@@ -1,59 +0,0 @@
-toContain('New server ')
- ->toContain('class="mb-5 flex min-h-9 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"')
- ->not->toContain('Back to servers')
- ->not->toContain('title="Add a server"');
-});
-
-test('provider pages only show the token action in the account panel', function () {
- $view = file_get_contents(resource_path('views/livewire/server/create.blade.php'));
-
- expect($view)
- ->not->toContain('New token')
- ->not->toContain('new-server-token-')
- ->not->toContain('tokenProviderName');
-});
-
-test('server selection separates existing servers from cloud provisioning', function (string $viewPath) {
- $view = file_get_contents(resource_path($viewPath));
-
- expect($view)
- ->toContain('Add a server')
- ->toContain('IP address or domain')
- ->toContain('Provision a server')
- ->and(strpos($view, 'Add a server'))->toBeLessThan(strpos($view, 'Provision a server'));
-})->with([
- 'new server page' => 'views/livewire/server/create.blade.php',
- 'onboarding' => 'views/livewire/boarding/index.blade.php',
-]);
-
-test('new server sections have vertical spacing', function () {
- $view = file_get_contents(resource_path('views/livewire/server/create.blade.php'));
-
- expect($view)->toContain('