fix(tests): repair tests that failed on main

- ServerSetting: default is_traffic_analytics_enabled to false on the
  model, so a new instance does not show null before a refresh.
- Update navbar, mobile menu, and proxy button tests to the current
  redesigned UI (split action menus, settings rail, neutral icons).
- Traffic nudge test: set server_role to mark a build server, the same
  as the product code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-09-25 23:52:01 +02:00
co-authored by Claude Opus 5.5
parent e17b15f5f1
commit f4fbcaacff
6 changed files with 181 additions and 199 deletions
+10
View File
@@ -135,6 +135,16 @@ class ServerSetting extends Model
'compose_version_checked_at',
];
/**
* Model-level defaults that mirror database column defaults, so a freshly
* created instance exposes them without a refresh.
*
* @var array<string, mixed>
*/
protected $attributes = [
'is_traffic_analytics_enabled' => false,
];
protected $casts = [
'force_disabled' => 'boolean',
'force_docker_cleanup' => 'boolean',
+125 -180
View File
@@ -1,6 +1,6 @@
<?php
it('uses the themed warning color for resource action icons', function () {
it('uses neutral play icons for resource start actions', function () {
foreach ([
'application',
'database',
@@ -8,150 +8,116 @@ it('uses the themed warning color for resource action icons', function () {
] as $resource) {
$heading = file_get_contents(resource_path("views/livewire/project/{$resource}/heading.blade.php"));
expect(substr_count($heading, 'name="play-circle" class="size-3.5 text-warning"'))
// One neutral start icon in the mobile split action and one in the desktop split action.
expect(substr_count($heading, 'name="play-circle" class="size-3.5"'))
->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 native mobile menus for databases and services', function () {
$applicationHeading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
$databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
$serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
$applicationMobileActions = mobileActionsMarkup($applicationHeading, 'application-mobile-actions', 'application-mobile-section');
$databaseMobileActions = mobileActionsMarkup($databaseHeading, 'database-mobile-actions', 'database-mobile-section');
$serviceMobileActions = mobileActionsMarkup($serviceHeading, 'service-mobile-actions', 'service-mobile-section');
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')),
];
expect(mobileActionsAreBeforeSelect($applicationHeading, 'application-mobile-actions', 'application-mobile-section'))->toBeTrue();
expect(mobileActionsAreBeforeSelect($databaseHeading, 'database-mobile-actions', 'database-mobile-section'))->toBeTrue();
expect(mobileActionsAreBeforeSelect($serviceHeading, 'service-mobile-actions', 'service-mobile-section'))->toBeTrue();
foreach ($headings as $resource => $heading) {
expect(mobileActionsAreBeforeDesktopActions($heading, "{$resource}-mobile-actions", "{$resource}-desktop-actions"))->toBeTrue();
expect($applicationHeading)
->toContain('application-mobile-actions')
->toContain("'route' => 'project.application.command'")
->toContain("'navigate' => false")
->toContain("value.startsWith('location|')")
->toContain('window.location.href = url')
->toContain('application-mobile-stop-trigger')
->toContain('application-mobile-restart-trigger')
expect($heading)
->toContain('<div class="w-full xl:hidden">')
->toContain("<x-split-action id=\"{$resource}-mobile-actions\" class=\"mb-3 flex w-full\">")
->not->toContain("{$resource}-mobile-section")
->not->toContain('<select')
->not->toContain('<optgroup')
->not->toContain('@selected');
expect(mobileActionsMarkup($heading, "{$resource}-mobile-actions"))
->toContain('<x-slot:main')
->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="force_deploy_without_cache"')
->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('<button id="application-mobile-stop-trigger" type="button">')
->toContain('<button id="application-mobile-restart-trigger" type="button">')
->not->toContain('application-mobile-deploy-trigger')
->not->toContain('application-mobile-force-deploy-trigger')
->not->toContain('Confirm Application Deployment?')
->not->toContain('<optgroup label="Actions">');
->not->toContain('Confirm Application Deployment?');
expect($applicationMobileActions)
->toContain('mb-3')
->toContain('Actions')
->toContain('<x-forms.button isError class="shrink-0"')
->not->toContain('button type="button" class="button shrink-0 text-error"')
->toContain('M7 4v16l13 -8z')
->toContain('M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747')
->toContain('M6 5m0 1a1 1 0 0 1 1 -1h2');
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($applicationHeading)
->toContain('application-mobile-section-label')
->toContain('Section');
expect($databaseHeading)
->toContain('database-mobile-section')
->toContain('database-mobile-actions')
->toContain('<optgroup label="Database">')
->toContain('<optgroup label="Configuration">')
->toContain("'route' => 'project.database.command'")
->toContain("'navigate' => false")
->toContain("value.startsWith('location|')")
->toContain('window.location.href = url')
->toContain('window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url')
->toContain("window.Livewire?.hook?.('morphed'")
->toContain('x-model="selected"')
->toContain('database-restart-trigger')
->toContain('database-stop-trigger')
->toContain("\$wire.dispatch('startEvent')")
expect($headings['database'])
->toContain('<button id="database-restart-trigger" type="button">')
->toContain('<button id="database-stop-trigger" type="button">')
->not->toContain('database-start-trigger')
->not->toContain('Confirm Database Start?')
->toContain('scrollbar hidden min-h-10')
->not->toContain('<optgroup label="Links">')
->not->toContain('<optgroup label="Actions">')
->not->toContain('@selected');
->not->toContain('Confirm Database Start?');
expect($databaseMobileActions)
->toContain('mb-3')
->toContain('Actions')
->toContain('<x-forms.button isError class="shrink-0"')
->not->toContain('button type="button" class="button shrink-0 text-error"')
->toContain('M7 4v16l13 -8z')
->toContain('M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747')
->toContain('M6 5m0 1a1 1 0 0 1 1 -1h2');
expect($databaseHeading)
->toContain('database-mobile-section-label')
->toContain('Section');
expect($serviceHeading)
->toContain('service-mobile-section')
->toContain('service-mobile-actions')
->toContain('<optgroup label="Service">')
->toContain('<optgroup label="Configuration">')
->toContain('<optgroup label="Resource">')
->toContain('<optgroup label="Links">')
->toContain("'route' => 'project.service.command'")
->toContain("'navigate' => false")
->toContain("value.startsWith('location|')")
->toContain('window.location.href = url')
->toContain('window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url')
->toContain("window.Livewire?.hook?.('morphed'")
->toContain('x-model="selected"')
->toContain('service-restart-trigger')
->toContain('service-stop-trigger')
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('service-pullAndRestart-trigger')
->toContain("\$wire.dispatch('pullAndRestartEvent')");
expect($headings['service'])
->toContain('<button id="service-restart-trigger" type="button">')
->toContain('<button id="service-stop-trigger" type="button">')
->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?')
->toContain('scrollbar hidden min-h-10')
->toContain('mb-4 w-full md:mb-0 md:hidden')
->toContain('hidden flex-wrap items-center gap-2 md:flex')
->toContain('flex flex-nowrap')
->toContain('overflow-x-auto')
->not->toContain('<optgroup label="Actions">')
->not->toContain('order-first flex flex-wrap items-center gap-2 sm:order-last')
->not->toContain('@selected');
expect($serviceMobileActions)
->toContain('mb-3')
->toContain('Actions')
->toContain('<x-forms.button isError class="shrink-0"')
->not->toContain('button type="button" class="button shrink-0 text-error"')
->toContain('M7 4v16l13 -8z')
->toContain('M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747')
->toContain('M6 5m0 1a1 1 0 0 1 1 -1h2');
expect($serviceHeading)
->toContain('service-mobile-section-label')
->toContain('Section');
->not->toContain('Confirm Service Force Deployment?');
});
function mobileActionsMarkup(string $heading, string $actionsId, string $selectId): string
function mobileActionsMarkup(string $heading, string $actionsId): string
{
$actionsPosition = strpos($heading, 'id="'.$actionsId.'"');
$selectPosition = strpos($heading, 'id="'.$selectId.'"');
if ($actionsPosition === false || $selectPosition === false || $actionsPosition > $selectPosition) {
if ($actionsPosition === false) {
return '';
}
return substr($heading, $actionsPosition, $selectPosition - $actionsPosition);
$endPosition = strpos($heading, '</x-split-action>', $actionsPosition);
if ($endPosition === false) {
return '';
}
return substr($heading, $actionsPosition, $endPosition - $actionsPosition);
}
function mobileActionsAreBeforeSelect(string $heading, string $actionsId, string $selectId): bool
function mobileActionsAreBeforeDesktopActions(string $heading, string $mobileActionsId, string $desktopActionsId): bool
{
$actionsPosition = strpos($heading, 'id="'.$actionsId.'"');
$selectPosition = strpos($heading, 'id="'.$selectId.'"');
$mobilePosition = strpos($heading, 'id="'.$mobileActionsId.'"');
$hudPosition = strpos($heading, "@teleport('#resource-action-hud-slot')");
$desktopPosition = strpos($heading, 'id="'.$desktopActionsId.'"');
return $actionsPosition !== false && $selectPosition !== false && $actionsPosition < $selectPosition;
return $mobilePosition !== false
&& $hudPosition !== false
&& $desktopPosition !== false
&& $mobilePosition < $hudPosition
&& $hudPosition < $desktopPosition;
}
it('places database backups immediately after configuration in navigation menus', function () {
@@ -167,81 +133,59 @@ it('places database backups immediately after configuration in navigation menus'
}
});
it('keeps configuration sidebars hidden until desktop breakpoint', function () {
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('sub-menu-wrapper hidden md:flex');
->toContain('<x-database.configuration-sidebar :database="$database" :current-route="$currentRoute" />')
->toContain('xl:grid-cols-[210px_minmax(0,1fr)]')
->not->toContain('sub-menu-wrapper');
expect(file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php')))
->toContain('sub-menu-wrapper hidden md:flex');
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('<aside class="application-settings-navigation min-w-0 xl:self-start"')
->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('<select')
->not->toContain('<optgroup');
}
expect(file_get_contents(resource_path('views/livewire/project/service/index.blade.php')))
->toContain('sub-menu-wrapper hidden md:flex');
expect(file_get_contents(resource_path('views/components/service-database/sidebar.blade.php')))
->toContain('sub-menu-wrapper hidden md:flex');
$serverSidebar = file_get_contents(resource_path('views/components/server/sidebar.blade.php'));
expect($serverSidebar)
->toContain('server-mobile-section-label')
->toContain('server-mobile-section')
->toContain('Section')
->toContain('select w-full')
->toContain('aria-label="Server menu"')
->toContain('border-b-2 border-solid border-neutral-200 pb-4 md:hidden dark:border-coolgray-200')
->toContain('<optgroup label="Server">')
->toContain('<optgroup label="Configuration">')
expect(file_get_contents(resource_path('views/components/server/sidebar.blade.php')))
->toContain('aria-label="Server configuration sections"')
->toContain("'route' => 'server.proxy'")
->toContain("'navigate' => false")
->toContain("value.startsWith('location|')")
->toContain('window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url')
->toContain('x-on:livewire:navigated.window="syncFromLocation()"')
->toContain('sub-menu-wrapper hidden md:flex')
->not->toContain('sub-menu-wrapper">');
->toContain("'navigate' => false");
$serverNavbar = file_get_contents(resource_path('views/livewire/server/navbar.blade.php'));
expect($serverNavbar)
->toContain('server-mobile-actions')
->toContain('Actions')
->toContain('pb-0 md:pb-6')
->toContain('navbar-main')
->toContain('server-mobile-actions" class="mt-2 mb-3')
->toContain('hidden min-h-10')
->toContain('md:flex')
->toContain('md:hidden')
->toContain('flex flex-nowrap gap-2 overflow-x-auto')
->toContain('server-mobile-restart-proxy-trigger')
->toContain('server-mobile-stop-proxy-trigger')
->toContain('class="button shrink-0"')
->toContain('hidden gap-2 md:flex');
->toContain('<div class="mb-3 w-full lg:hidden">')
->toContain('<div class="w-full xl:hidden">')
->toContain('<x-split-action id="server-mobile-actions" class="mb-3 flex w-full">')
->toContain('<button id="server-mobile-restart-proxy-trigger" type="button">')
->toContain('<button id="server-mobile-stop-proxy-trigger" type="button">')
->not->toContain('navbar-main')
->not->toContain('<select');
$serverMobileActions = serverMobileActionsMarkup($serverNavbar);
expect($serverMobileActions)
->toContain('Restart Proxy')
->toContain('Stop Proxy')
->toContain('Refresh Proxy Status')
->not->toContain('Traefik');
expect(strpos($serverMobileActions, 'Restart Proxy'))
->toBeLessThan(strpos($serverMobileActions, 'Stop Proxy'))
->toBeLessThan(strpos($serverMobileActions, 'Traefik Dashboard'));
foreach ([
'views/components/server/sidebar-proxy.blade.php' => 'Proxy menu',
'views/components/server/sidebar-sentinel.blade.php' => 'Sentinel menu',
'views/components/server/sidebar-security.blade.php' => 'Security menu',
] as $view => $label) {
$sidebar = file_get_contents(resource_path($view));
expect($sidebar)
->toContain('server-mobile-section-label')
->toContain('server-mobile-section')
->toContain('Section')
->toContain('select w-full')
->toContain('aria-label="'.$label.'"')
->toContain('border-b-2 border-solid border-neutral-200 pb-4 md:hidden dark:border-coolgray-200')
->toContain('<optgroup label="Server">')
->toContain('window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url')
->toContain('x-on:livewire:navigated.window="syncFromLocation()"')
->toContain('sub-menu-wrapper hidden md:flex')
->not->toContain('sub-menu-wrapper">');
}
->and(strpos($serverMobileActions, 'Stop Proxy'))
->toBeLessThan(strpos($serverMobileActions, 'Refresh Proxy Status'));
foreach ([
'views/livewire/server/show.blade.php',
@@ -265,9 +209,10 @@ it('keeps configuration sidebars hidden until desktop breakpoint', function () {
'views/livewire/server/security/terminal-access.blade.php',
] as $view) {
expect(file_get_contents(resource_path($view)))
->toContain('gap-4 md:gap-8 md:flex-row')
->toContain('md:flex-row')
->not->toContain('sm:flex-row')
->toContain('server-settings-workspace application-settings-workspace')
->toContain('xl:grid-cols-[210px_minmax(0,1fr)]')
->toContain('<x-server.sidebar')
->not->toContain('sub-menu-wrapper')
->not->toContain('class="flex flex-col h-full gap-8 md:flex-row');
}
});
+17 -3
View File
@@ -186,10 +186,24 @@ test('start proxy button shows a loading state while proxy startup actions run',
$this->actingAs($user);
session(['currentTeam' => $team]);
Livewire::test('server.navbar', ['server' => $mock])
$html = Livewire::test('server.navbar', ['server' => $mock])
->assertSeeHtml('wire:loading.attr="disabled"')
->assertSeeHtml('wire:loading.class="is-loading"')
->assertSeeHtml('wire:target="checkProxy,startProxy"');
->assertSeeHtml('wire:target="checkProxy,startProxy"')
->html();
// The split action main button shows its loading state through the
// `.split-action-main:disabled` style while Livewire disables it.
preg_match_all('/<button\b[^>]*class="split-action-main"[^>]*>(?:(?!<\/button>).)*?Start Proxy/s', $html, $startButtons);
expect($startButtons[0])->not->toBeEmpty();
foreach ($startButtons[0] as $startButton) {
expect($startButton)
->toContain('wire:loading.attr="disabled"')
->toContain('wire:target="checkProxy,startProxy"');
}
expect(file_get_contents(resource_path('css/app.css')))
->toMatch('/\.split-action-main:disabled,\s*\.split-action-caret:disabled\s*\{[^}]*opacity:/');
});
test('starting a proxy records a team audit event', function () {
@@ -505,10 +505,15 @@ it('keeps the application settings sidebar below the fixed header while scrollin
$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($css)->toContain('.application-settings-workspace > .application-settings-navigation')
->and($css)->toContain('top: 4rem;')
->and($css)->toContain('max-height: calc(100dvh - 5rem);');
->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 () {
+19 -13
View File
@@ -44,32 +44,38 @@ it('uses the branded input focus state for the server filter', function () {
->not->toContain('<x-reicon name="check"');
});
it('lists desktop proxy controls inline and keeps Traefik and refresh in Advanced', function () {
it('lists desktop proxy controls in one split action and links Traefik beside it', function () {
$navbar = file_get_contents(resource_path('views/livewire/server/navbar.blade.php'));
$overflow = file_get_contents(resource_path('views/components/resource-heading-overflow.blade.php'));
$advanced = file_get_contents(resource_path('views/components/server/advanced.blade.php'));
$desktopActions = str($navbar)->after("@teleport('#resource-action-hud-slot')")->before('@endteleport')->toString();
$splitAction = str($desktopActions)->after('id="server-desktop-actions"')->before('</x-split-action>')->toString();
$traefikLink = str($desktopActions)->before('<x-split-action id="server-desktop-actions"')->toString();
expect($desktopActions)
->toContain('Restart Proxy')
->toContain('Stop Proxy')
->toContain('Start Proxy')
->toContain('<x-server.advanced')
->not->toContain('Traefik Dashboard')
->not->toContain('Refresh Proxy Status')
->toContain('<x-split-action id="server-desktop-actions"')
->toContain('Traefik Dashboard')
->not->toContain('<x-server.advanced')
->not->toContain('resource-heading-overflow-separator')
->not->toContain('<x-modal-confirmation');
expect(strpos($desktopActions, '<x-server.advanced'))
expect(strpos($desktopActions, 'Traefik Dashboard'))
->toBeLessThan(strpos($desktopActions, 'id="server-desktop-actions"'));
expect($advanced)
->toContain('Advanced')
->toContain('name="grid"')
->toContain('Traefik Dashboard')
->toContain('name="external-link" class="size-3! opacity-70"')
->toContain('class="flex size-4 shrink-0 items-center justify-center"')
->toContain('Refresh Proxy Status');
expect($traefikLink)
->toContain('@if ($traefikDashboardAvailable)')
->toContain('target="_blank"')
->toContain('href="http://{{ $serverIp }}:8080"')
->toContain('name="external-link" class="size-3.5 shrink-0 opacity-70"');
expect($splitAction)
->toContain('<x-slot:main')
->toContain('listbox-option')
->toContain('wire:click="checkProxyStatus"')
->toContain('Refresh Proxy Status')
->not->toContain('Traefik');
expect($overflow)
->toContain('Actions')
@@ -1,5 +1,6 @@
<?php
use App\Enums\ServerRole;
use App\Livewire\Analytics;
use App\Livewire\Dashboard\TrafficAnalytics as DashboardTrafficAnalytics;
use App\Models\PrivateKey;
@@ -55,6 +56,7 @@ it('does not count swarm or build servers in the analytics-page nudge', function
$swarm->settings->save();
$build = disabledServer();
$build->settings->server_role = ServerRole::BUILD;
$build->settings->is_build_server = true;
$build->settings->save();