diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 34283cd47..0e1a4c47e 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -55,6 +55,10 @@ class General extends Component public ?string $customNetworkAliases = null; + public ?string $currentInternalHostname = null; + + public bool $currentInternalHostnameLoaded = false; + public ?string $dockerfile = null; public ?string $dockerfileLocation = null; @@ -448,6 +452,32 @@ class General extends Component } } + public function loadCurrentInternalHostname(): void + { + if ($this->application->build_pack === 'dockercompose') { + $this->currentInternalHostnameLoaded = true; + + return; + } + + try { + $containers = getCurrentApplicationContainerStatus( + $this->application->destination->server, + $this->application->id, + 0 + ); + $currentContainer = $containers->first( + fn ($container) => data_get($container, 'State') === 'running' + ) ?? $containers->first(); + + $this->currentInternalHostname = data_get($currentContainer, 'Names'); + } catch (\Throwable) { + $this->currentInternalHostname = null; + } finally { + $this->currentInternalHostnameLoaded = true; + } + } + public function setSiteType(): void { $this->isStatic = $this->siteType !== 'dynamic'; diff --git a/resources/css/app.css b/resources/css/app.css index c64f9311e..3f0e28743 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -223,6 +223,14 @@ position: relative; z-index: 2; } + + @media (max-width: 767px) { + :root input, + :root textarea, + :root select { + font-size: 16px !important; + } + } } /* @@ -233,14 +241,6 @@ body { @apply w-full min-h-full bg-gray-50 dark:bg-app dark:text-fg-dim; } -@media (max-width: 767px) { - input, - textarea, - select { - font-size: 16px; - } -} - body { /* overflow-x: clip (not hidden) — hidden creates a scroll containment that breaks position:sticky on settings sidebars and other in-page pins. */ diff --git a/resources/css/v5/app.css b/resources/css/v5/app.css index e9b44ecca..1f7361441 100644 --- a/resources/css/v5/app.css +++ b/resources/css/v5/app.css @@ -159,10 +159,10 @@ } @media (max-width: 767px) { - input, - textarea, - select { - font-size: 16px; + :root input, + :root textarea, + :root select { + font-size: 16px !important; } } diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php index 05dbcc90c..4170b188f 100644 --- a/resources/views/auth/two-factor-challenge.blade.php +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -1,137 +1,96 @@ -
+
-
-
-
-

- Coolify -

-

- Two-Factor Authentication -

-
+ }, + pasteCode(event) { + event.preventDefault(); -
- @if (session('status')) -
-

{{ session('status') }}

-
- @endif + const pastedDigits = event.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6).split(''); + const inputs = event.currentTarget.querySelectorAll('input[type=text]'); - @if ($errors->any()) -
- @foreach ($errors->all() as $error) -

{{ $error }}

- @endforeach -
- @endif + pastedDigits.forEach((digit, index) => this.digits[index] = digit); + this.updateCode(); + inputs[Math.min(pastedDigits.length, 6) - 1]?.focus(); + }, + }"> + @if (session('status')) + {{ session('status') }} + @endif -
-
- - - -

- Enter the verification code from your authenticator app to continue. -

-
+ @if ($errors->any()) + +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach
+
+ @endif -
- @csrf -
- -
- -
- -
-
- - -
- - {{ __('auth.login') }} - -
- -
-
-
-
-
- - Need help? - -
-
- - - Back to Login - -
+
+ +

Enter the 6-digit code from your authenticator app.

+

Enter one of the recovery codes you saved when setting up two-factor authentication.

+ +
+ @csrf + +
+ +
+ +
+ +
+ +
+ + +
+ + + Verify and continue + +
-
-
\ No newline at end of file + + + Not your account? + Back to login + + + diff --git a/resources/views/components/application/configuration-sidebar.blade.php b/resources/views/components/application/configuration-sidebar.blade.php index 52a4a9b7a..b480a69ea 100644 --- a/resources/views/components/application/configuration-sidebar.blade.php +++ b/resources/views/components/application/configuration-sidebar.blade.php @@ -176,6 +176,7 @@ 'project.application.configuration' => array_values(array_filter([ ['id' => 'application-details-section', 'label' => 'Application details'], ['id' => 'public-access-section', 'label' => 'Public access'], + $isComposeApp ? null : ['id' => 'internal-access-section', 'label' => 'Internal access'], ['id' => 'build-pipeline-section', 'label' => 'Build pipeline'], $isComposeApp ? null : ['id' => 'container-image-section', 'label' => 'Container image'], $isComposeApp ? null : ['id' => 'networking-section', 'label' => 'Networking'], diff --git a/resources/views/components/deployment/configuration-diff.blade.php b/resources/views/components/deployment/configuration-diff.blade.php index 7916b8448..a186b234f 100644 --- a/resources/views/components/deployment/configuration-diff.blade.php +++ b/resources/views/components/deployment/configuration-diff.blade.php @@ -1,6 +1,6 @@ @props([ 'diff' => null, - 'compact' => false, + 'compact' => true, ]) @php @@ -8,18 +8,26 @@ @endphp @if ($changes->isNotEmpty()) -
+
$compact, 'space-y-5' => ! $compact])> @foreach ($changes->groupBy('section_label') as $sectionLabel => $sectionChanges)

+ @class([ + 'font-semibold uppercase tracking-wide text-neutral-500 dark:text-fg-faint', + 'mb-1 text-[10px]' => $compact, + 'mb-2 text-[11px]' => ! $compact, + ])> {{ $sectionLabel }}

-
-
+
+
+ @class([ + 'grid grid-cols-[minmax(8rem,0.75fr)_minmax(12rem,1fr)_1.25rem_minmax(12rem,1fr)_1.5rem] items-center text-[10px] font-medium text-neutral-500 dark:bg-white/[0.04] dark:text-fg-faint bg-neutral-100', + 'gap-2 px-2 py-1' => $compact, + 'gap-3 px-3 py-2' => ! $compact, + ])>
Field
Current
@@ -42,7 +50,11 @@ @endphp
+ @class([ + 'grid grid-cols-[minmax(8rem,0.75fr)_minmax(12rem,1fr)_1.25rem_minmax(12rem,1fr)_1.5rem] items-start transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]', + 'gap-2 px-2 py-1.5 text-xs' => $compact, + 'gap-3 px-3 py-2.5 text-sm' => ! $compact, + ])>
@if ($rowExpandable)
- +
@@ -86,13 +98,13 @@ x-on:click="expandedRows['{{ $changeKey }}'] = ! expandedRows['{{ $changeKey }}']" :aria-expanded="!! expandedRows['{{ $changeKey }}']" title="Toggle full value" - class="flex size-6 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"> + class="flex size-5 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"> + class="size-3" /> + x-cloak class="size-3" /> @endif
diff --git a/resources/views/components/popup-small.blade.php b/resources/views/components/popup-small.blade.php index 0d4b58a7b..9b17ddc35 100644 --- a/resources/views/components/popup-small.blade.php +++ b/resources/views/components/popup-small.blade.php @@ -7,27 +7,27 @@ x-transition:leave="transition ease-in duration-150" x-transition:leave-start="translate-y-0 opacity-100" x-transition:leave-end="translate-y-3 opacity-0" - class="fixed bottom-4 right-4 z-999 w-[calc(100%-2rem)] max-w-md"> -
+
@isset($icon)
+ class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning"> {{ $icon }}
@endisset -
-

+
+

{{ $title }}

-
+
{{ $description }}
diff --git a/resources/views/components/server-timing-hud.blade.php b/resources/views/components/server-timing-hud.blade.php index 50709a909..bf3217050 100644 --- a/resources/views/components/server-timing-hud.blade.php +++ b/resources/views/components/server-timing-hud.blade.php @@ -1,5 +1,6 @@ {{-- Dev-only Server-Timing HUD. Injected by AddServerTimingHeaders on full HTML responses. - JS docks into #server-timing-hud-slot in the desktop top bar (lg+); otherwise floats bottom-left. --}} + JS docks into the top bar: #server-timing-hud-slot (lg+) or #server-timing-hud-slot-mobile ( @@ -52,16 +53,21 @@ function pickSlot() { const desktop = document.getElementById('server-timing-hud-slot'); - // Match Tailwind `lg` (1024px): only dock into the desktop top bar. - // On mobile the header is cramped — float bottom-left instead of docking. + const mobile = document.getElementById('server-timing-hud-slot-mobile'); + // Match Tailwind `lg` (1024px): desktop top bar vs mobile top bar. const isLg = window.matchMedia('(min-width: 1024px)').matches; if (isLg && desktop) { return desktop; } - return null; + if (!isLg && mobile) { + return mobile; + } + // Fallback if one breakpoint's slot is missing from the shell. + return desktop || mobile || null; } function applyDockedStyles(root) { + const isMobileSlot = root.parentElement && root.parentElement.id === 'server-timing-hud-slot-mobile'; root.setAttribute('data-sth-mode', 'docked'); root.style.cssText = [ 'position:relative', @@ -71,15 +77,35 @@ 'right:auto', 'z-index:60', 'display:block', - 'flex-shrink:0', + 'flex-shrink:1', + 'min-width:0', + 'max-width:100%', 'visibility:visible', 'opacity:1', - 'margin-right:8px', + isMobileSlot ? 'margin-right:4px' : 'margin-right:8px', 'font:11px/1.3 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace', 'color:#e5e5e5', 'pointer-events:auto', ].join(';'); + const toggle = qs(root, '[data-sth-toggle]'); + if (toggle) { + if (isMobileSlot) { + // Compact pill: just "83ms" — no full metric breakdown in the bar. + toggle.style.padding = '4px 8px'; + toggle.style.fontSize = '11px'; + toggle.style.maxWidth = 'none'; + toggle.style.overflow = 'visible'; + toggle.style.whiteSpace = 'nowrap'; + } else { + toggle.style.padding = ''; + toggle.style.fontSize = ''; + toggle.style.maxWidth = '100%'; + toggle.style.overflow = 'hidden'; + toggle.style.textOverflow = 'ellipsis'; + } + } + const panel = qs(root, '[data-sth-panel]'); if (panel) { // Dropdown below the pill inside the top bar. @@ -89,6 +115,10 @@ panel.style.top = 'calc(100% + 8px)'; panel.style.bottom = 'auto'; panel.style.zIndex = '2147483001'; + if (isMobileSlot) { + // Keep panel on-screen: open near the right edge of the viewport. + panel.style.width = 'min(420px, calc(100vw - 16px))'; + } } } @@ -563,7 +593,13 @@ const q = latest.metrics.queries !== undefined ? Math.round(Number(latest.metrics.queries)) + 'q' : '—'; const db = latest.metrics.db !== undefined ? Number(latest.metrics.db).toFixed(0) + 'ms db' : '—'; const n = history.length; - summary.textContent = 'ST ' + app + ' · ' + db + ' · ' + q + (n > 1 ? ' · ×' + n : ''); + // Mobile navbar is tight — show only total app time; full breakdown lives in the panel. + const compactSummary = root.getAttribute('data-sth-mode') === 'docked' + && root.parentElement + && root.parentElement.id === 'server-timing-hud-slot-mobile'; + summary.textContent = compactSummary + ? app + : ('ST ' + app + ' · ' + db + ' · ' + q + (n > 1 ? ' · ×' + n : '')); if (countEl) { countEl.textContent = n + (n === 1 ? ' request' : ' requests'); } diff --git a/resources/views/components/server/status-summary.blade.php b/resources/views/components/server/status-summary.blade.php index ccf68af29..b18781ef2 100644 --- a/resources/views/components/server/status-summary.blade.php +++ b/resources/views/components/server/status-summary.blade.php @@ -64,13 +64,15 @@

@endif @if ($server->proxySet()) -
-
+
+ {{-- Dev Server-Timing HUD docks here on
+ @if ($buildPack !== 'dockercompose') + @php + $networkAliases = str($customNetworkAliases ?? '') + ->explode(',') + ->map(fn ($alias) => trim($alias)) + ->filter() + ->values(); + $exposedPorts = str($portsExposes ?? '') + ->explode(',') + ->map(fn ($port) => trim($port)) + ->filter() + ->implode(', '); + @endphp + +
+ @if ($currentInternalHostname) + + @else +
+

Internal hostname

+

+ {{ $currentInternalHostnameLoaded ? 'No deployed container found' : 'Loading…' }} +

+
+ @endif + + + +
+
+

+ Internal hostnames are only reachable by resources connected to this Docker network. +

+ +
+
+ @endif + @if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
diff --git a/resources/views/livewire/project/shared/configuration-checker.blade.php b/resources/views/livewire/project/shared/configuration-checker.blade.php index ee3072b69..578711ca8 100644 --- a/resources/views/livewire/project/shared/configuration-checker.blade.php +++ b/resources/views/livewire/project/shared/configuration-checker.blade.php @@ -6,26 +6,27 @@ The latest configuration has not been applied - + @if (data_get($configurationDiff, 'count')) - {{ data_get($configurationDiff, 'count') }} unapplied configuration - {{ data_get($configurationDiff, 'count') === 1 ? 'change' : 'changes' }} detected. + {{ data_get($configurationDiff, 'count') }} + {{ data_get($configurationDiff, 'count') === 1 ? 'change' : 'changes' }} + unapplied. @if (data_get($configurationDiff, 'requires_build')) - A rebuild is required. + Rebuild required. @else - Please redeploy to apply the new configuration. + Redeploy to apply. @endif @else - Please redeploy to apply the new configuration. + Redeploy to apply. @endif @@ -49,32 +50,26 @@ class="application-settings-form application-settings-section relative max-h-[calc(100dvh-2rem)] w-full max-w-6xl overflow-hidden" style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
-
+
+ class="size-3.5 text-amber-600 dark:text-warning" />

Configuration changes

+ class="application-settings-section-body min-h-0 flex-1 overflow-y-auto !p-3">
-
-
- -
+ class="mb-3 flex items-center justify-between gap-2 rounded-md bg-amber-50 px-2.5 py-1.5 ring-1 ring-amber-200 dark:bg-warning/[0.07] dark:ring-warning/15"> +
-

- {{ data_get($configurationDiff, 'count') }} configuration +

+ {{ data_get($configurationDiff, 'count') }} {{ data_get($configurationDiff, 'count') === 1 ? 'change' : 'changes' }} -

-

- Deploy again to apply the latest values. + · Deploy again to apply

@@ -82,7 +77,7 @@ :status="data_get($configurationDiff, 'requires_build') ? 'Rebuild required' : 'Redeploy required'" type="warning" />
- +
diff --git a/tests/Feature/AddServerTimingHeadersTest.php b/tests/Feature/AddServerTimingHeadersTest.php index 0a9433859..2c183a33a 100644 --- a/tests/Feature/AddServerTimingHeadersTest.php +++ b/tests/Feature/AddServerTimingHeadersTest.php @@ -109,23 +109,26 @@ test('removes stale Content-Length after injecting the HUD', function () { expect($response->headers->has('Content-Length'))->toBeFalse(); }); -test('app shell exposes Server-Timing HUD dock slot in the desktop top bar only', function () { +test('app shell exposes Server-Timing HUD dock slots in desktop and mobile top bars', function () { $layout = file_get_contents(resource_path('views/layouts/app.blade.php')); expect($layout) ->toContain('id="server-timing-hud-slot"') - ->toContain('data-server-timing-hud-slot') - ->not->toContain('id="server-timing-hud-slot-mobile"'); + ->toContain('id="server-timing-hud-slot-mobile"') + ->toContain('data-server-timing-hud-slot'); }); -test('Server-Timing HUD docks on desktop only and floats on smaller viewports', function () { +test('Server-Timing HUD docks into navbar slots and floats only as fallback', function () { $hud = file_get_contents(resource_path('views/components/server-timing-hud.blade.php')); expect($hud) ->toContain('server-timing-hud-slot') + ->toContain('server-timing-hud-slot-mobile') ->toContain("matchMedia('(min-width: 1024px)')") - ->toContain('float bottom-left instead of docking') - ->not->toContain('server-timing-hud-slot-mobile'); + ->toContain('floats bottom-left only if no navbar slot is available') + // Mobile pill is compact (app ms only); full "ST · db · q" breakdown stays desktop/float. + ->toContain("parentElement.id === 'server-timing-hud-slot-mobile'") + ->toContain('compactSummary'); }); test('does not inject HUD into non-HTML or fragment responses', function () { diff --git a/tests/Feature/ApplicationInternalAccessSectionTest.php b/tests/Feature/ApplicationInternalAccessSectionTest.php new file mode 100644 index 000000000..a6abf1439 --- /dev/null +++ b/tests/Feature/ApplicationInternalAccessSectionTest.php @@ -0,0 +1,36 @@ +toContain('id="internal-access-section"') + ->toContain('title="Internal access"') + ->toContain('Internal hostname') + ->toContain('Docker network') + ->toContain('Exposed ports') + ->toContain('Network aliases') + ->toContain('wire:init="loadCurrentInternalHostname"') + ->toContain('$currentInternalHostname') + ->not->toContain('Changes with each deployment') + ->toContain("window.scrollToSettingsSection?.('networking-section')") + ->and($generalComponent) + ->toContain('public ?string $currentInternalHostname = null;') + ->toContain('public function loadCurrentInternalHostname(): void') + ->toContain('getCurrentApplicationContainerStatus(') + ->toContain("data_get(\$currentContainer, 'Names')") + ->and($configurationSidebar) + ->toContain("['id' => 'internal-access-section', 'label' => 'Internal access']"); +}); + +it('does not show the internal access section for Docker Compose applications', function () { + $generalSettings = file_get_contents(resource_path('views/livewire/project/application/general.blade.php')); + $configurationSidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php')); + + expect($generalSettings) + ->toContain("@if (\$buildPack !== 'dockercompose')") + ->and($configurationSidebar) + ->toContain("\$isComposeApp ? null : ['id' => 'internal-access-section', 'label' => 'Internal access']"); +}); diff --git a/tests/Feature/Livewire/ConfigurationCheckerTest.php b/tests/Feature/Livewire/ConfigurationCheckerTest.php index d868024fa..b9ef79d4e 100644 --- a/tests/Feature/Livewire/ConfigurationCheckerTest.php +++ b/tests/Feature/Livewire/ConfigurationCheckerTest.php @@ -67,7 +67,7 @@ it('renders the changed configuration labels without a second backend request', Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()]) ->assertSee('The latest configuration has not been applied') - ->assertSee('A rebuild is required.') + ->assertSee('Rebuild required.') ->assertSee('Build command'); $view = file_get_contents(resource_path('views/livewire/project/shared/configuration-checker.blade.php')); @@ -114,7 +114,7 @@ it('shows an unapplied configuration warning after a directory mount is added', ->dispatch('configurationChanged') ->assertSet('isConfigurationChanged', true) ->assertSee('The latest configuration has not been applied') - ->assertSee('Please redeploy to apply the new configuration.') + ->assertSee('Redeploy to apply.') ->assertSee('Directory mount'); }); diff --git a/tests/Feature/LoginPageBrandingTest.php b/tests/Feature/LoginPageBrandingTest.php index ede7ddba3..cf8dc071f 100644 --- a/tests/Feature/LoginPageBrandingTest.php +++ b/tests/Feature/LoginPageBrandingTest.php @@ -32,3 +32,16 @@ test('confirm password page uses the shared auth shell', function () { ->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'); +}); diff --git a/tests/Feature/ServerNavbarStatusLayoutTest.php b/tests/Feature/ServerNavbarStatusLayoutTest.php index 03c6132a0..9ada9d5f5 100644 --- a/tests/Feature/ServerNavbarStatusLayoutTest.php +++ b/tests/Feature/ServerNavbarStatusLayoutTest.php @@ -21,7 +21,8 @@ it('collapses server subsystem badges into one status summary', function () { ->toContain('wire:loading.flex wire:target="checkProxyStatus"') ->toContain('Refreshing status') ->toContain('') - ->toContain('name="refresh" class="size-3 opacity-70"') + ->toContain('name="refresh" class="size-2.5 opacity-70"') + ->toContain('min-h-0! h-7! gap-1.5! px-2! py-1! text-[11px]') ->not->toContain('@click="open = false" role="menuitem"'); expect($badgeView) diff --git a/tests/Feature/SidebarNavigationMarkupTest.php b/tests/Feature/SidebarNavigationMarkupTest.php index e536ff4db..cd44473d9 100644 --- a/tests/Feature/SidebarNavigationMarkupTest.php +++ b/tests/Feature/SidebarNavigationMarkupTest.php @@ -34,6 +34,12 @@ it('draws a single border between the desktop sidebar and main content', functio ->and($layout)->not->toContain('lg:border-l border-neutral-200'); }); +it('separates the mobile sidebar from the page with a visible border', function () { + $layout = file_get_contents(resource_path('views/layouts/app.blade.php')); + + expect($layout)->toContain('max-w-56 min-w-0 flex-col border-l border-neutral-200 bg-white shadow-xl dark:border-white/[0.12] dark:bg-panel'); +}); + it('shows section titles and descriptions above settings navigation on smaller screens', function (string $view, string $title, string $description) { $layout = file_get_contents(resource_path("views/components/{$view}")); $css = file_get_contents(resource_path('css/app.css')); diff --git a/tests/Feature/StatusBadgeComponentsTest.php b/tests/Feature/StatusBadgeComponentsTest.php index 17bf98d31..d824111ea 100644 --- a/tests/Feature/StatusBadgeComponentsTest.php +++ b/tests/Feature/StatusBadgeComponentsTest.php @@ -36,6 +36,14 @@ it('renders service container statuses as shared status badges', function () { ->not->toContain('badge-success'); }); +it('uses bordered status badges in the top breadcrumb', function () { + $breadcrumb = file_get_contents(resource_path('views/components/top-breadcrumb.blade.php')); + $borderedBadgeClasses = 'rounded-full border border-neutral-200 bg-neutral-100'; + + expect(substr_count($breadcrumb, $borderedBadgeClasses))->toBe(4) + ->and(substr_count($breadcrumb, 'rounded-full bg-neutral-100'))->toBe(0); +}); + it('uses a shared refresh badge for resource status refresh actions', function () { $statusIndex = file_get_contents(resource_path('views/components/status/index.blade.php')); $serviceStatus = file_get_contents(resource_path('views/components/status/services.blade.php')); diff --git a/tests/Unit/MobileInputZoomPreventionTest.php b/tests/Unit/MobileInputZoomPreventionTest.php index 54dad0885..d3b49af90 100644 --- a/tests/Unit/MobileInputZoomPreventionTest.php +++ b/tests/Unit/MobileInputZoomPreventionTest.php @@ -2,12 +2,17 @@ it('uses at least sixteen pixel form controls on mobile to prevent input focus zoom', function (string $stylesheet) { $css = file_get_contents(dirname(__DIR__, 2).'/'.$stylesheet); + $baseLayerStart = strpos($css, '@layer base {'); + $mobileRuleStart = strpos($css, '@media (max-width: 767px)'); - expect($css)->toContain('@media (max-width: 767px)') - ->and($css)->toContain('input,') - ->and($css)->toContain('textarea,') - ->and($css)->toContain('select') - ->and($css)->toContain('font-size: 16px'); + expect($baseLayerStart)->not->toBeFalse() + ->and($mobileRuleStart)->toBeGreaterThan($baseLayerStart) + ->and(substr_count(substr($css, $baseLayerStart, $mobileRuleStart - $baseLayerStart), '{')) + ->toBeGreaterThan(substr_count(substr($css, $baseLayerStart, $mobileRuleStart - $baseLayerStart), '}')) + ->and($css)->toContain(':root input,') + ->and($css)->toContain(':root textarea,') + ->and($css)->toContain(':root select') + ->and($css)->toContain('font-size: 16px !important;'); })->with([ 'current interface' => 'resources/css/app.css', 'v5 interface' => 'resources/css/v5/app.css', diff --git a/tests/v4/Browser/ApplicationConfigurationTest.php b/tests/v4/Browser/ApplicationConfigurationTest.php index 75934361b..fa7fb4ffa 100644 --- a/tests/v4/Browser/ApplicationConfigurationTest.php +++ b/tests/v4/Browser/ApplicationConfigurationTest.php @@ -31,6 +31,8 @@ it('shows application configuration sections and navigation', function () { ->assertSee('Environment Variables') ->assertSee('Danger Zone') ->assertSee('Application details') + ->assertSee('Internal access') + ->assertSee('Docker network') ->assertSee('Build pipeline') ->screenshot(filename: 'application-configuration-overview'); });