From c3daf95caca7d649cf62775315435e42f87480d2 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 28 Apr 2026 13:03:21 +0200 Subject: [PATCH 01/10] sync canonical url --- src/Configuration.php | 7 +++ src/Drivers/LaravelHttpServer.php | 84 ++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/Configuration.php b/src/Configuration.php index bf3c458f..6bc05d5c 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -4,9 +4,11 @@ namespace Pest\Browser; +use Pest\Browser\Drivers\LaravelHttpServer; use Pest\Browser\Enums\BrowserType; use Pest\Browser\Enums\ColorScheme; use Pest\Browser\Playwright\Playwright; +use Pest\Browser\ServerManager; /** * @internal @@ -102,6 +104,11 @@ public function withHost(?string $host): self { Playwright::setHost($host); + $http = ServerManager::instance()->http(); + if ($http instanceof LaravelHttpServer) { + $http->syncCanonicalUrl(); + } + return $this; } diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index 97ae5fdb..d03df8f9 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -70,7 +70,7 @@ public function __destruct() } /** - * Rewrite the given URL to match the server's host and port. + * Rewrite the given URL to match the server's canonical host and port. */ public function rewrite(string $url): string { @@ -85,7 +85,7 @@ public function rewrite(string $url): string $path = $parts['path'] ?? '/'; parse_str($parts['query'] ?? '', $queryParameters); - return (string) Uri::of($this->url()) + return (string) Uri::of($this->canonicalUrl()) ->withPath($path) ->withQuery($queryParameters); } @@ -148,10 +148,6 @@ public function bootstrap(): void { $this->start(); - $url = $this->url(); - - config(['app.url' => $url]); - config(['cors.paths' => ['*']]); if (app()->bound('url')) { @@ -160,10 +156,36 @@ public function bootstrap(): void assert($urlGenerator instanceof UrlGenerator); $this->setOriginalAssetUrl($urlGenerator->asset('')); + $urlGenerator->forceScheme('http'); + } + + $this->syncCanonicalUrl(); + } + + /** + * Re-sync Laravel's `app.url` and the URL generator's origin to the canonical URL. + * + * Called both at bootstrap and when the configured host changes mid-test (e.g. + * via `withHost(...)`), so that `route()`, `asset()`, and `config('app.url')` + * stay consistent with the host the browser is navigating to. + */ + public function syncCanonicalUrl(): void + { + if (! $this->socket instanceof AmpHttpServer) { + return; + } + + $url = $this->canonicalUrl(); + + config(['app.url' => $url]); + + if (app()->bound('url')) { + $urlGenerator = app('url'); + + assert($urlGenerator instanceof UrlGenerator); $urlGenerator->useOrigin($url); $urlGenerator->useAssetOrigin($url); - $urlGenerator->forceScheme('http'); } } @@ -194,7 +216,7 @@ public function throwLastThrowableIfNeeded(): void } /** - * Get the public path for the given path. + * Get the URL of the bound socket (always 127.0.0.1:). */ private function url(): string { @@ -205,6 +227,28 @@ private function url(): string return sprintf('http://%s:%d', $this->host, $this->port); } + /** + * Get the canonical host the user expects URLs to use. + * + * Defaults to the bound IP unless the test configured a host with `withHost(...)`. + */ + private function canonicalHost(): string + { + return Playwright::host() ?? $this->host; + } + + /** + * Get the canonical base URL used for generated links and request URIs. + */ + private function canonicalUrl(): string + { + if (! $this->socket instanceof AmpHttpServer) { + throw new ServerNotFoundException('The HTTP server is not running.'); + } + + return sprintf('http://%s:%d', $this->canonicalHost(), $this->port); + } + /** * Sets the original asset URL. */ @@ -224,11 +268,17 @@ private function handleRequest(AmpRequest $request): Response Execution::instance()->tick(); } + // Re-sync per request as a safety net — if the configured host changes + // mid-test, the URL generator and config should reflect it before the + // app handles the request. + $this->syncCanonicalUrl(); + $canonicalUrl = $this->canonicalUrl(); + $uri = $request->getUri(); $path = in_array($uri->getPath(), ['', '0'], true) ? '/' : $uri->getPath(); $query = $uri->getQuery() ?? ''; // @phpstan-ignore-line $fullPath = $path.($query !== '' ? '?'.$query : ''); - $absoluteUrl = mb_rtrim($this->url(), '/').$fullPath; + $absoluteUrl = mb_rtrim($canonicalUrl, '/').$fullPath; $filepath = public_path($path); if (file_exists($filepath) && ! is_dir($filepath)) { @@ -261,15 +311,13 @@ private function handleRequest(AmpRequest $request): Response $symfonyRequest->headers->add($request->getHeaders()); - // Set the Host header to match the configured host for subdomain routing - $configuredHost = Playwright::host(); - if ($configuredHost !== null) { - $hostHeader = sprintf('%s:%d', $configuredHost, $this->port); - $symfonyRequest->headers->set('Host', $hostHeader); - // Also set SERVER_NAME for Laravel routing - $symfonyRequest->server->set('SERVER_NAME', $configuredHost); - $symfonyRequest->server->set('HTTP_HOST', $hostHeader); - } + // Ensure the framework sees the canonical host (e.g. for subdomain routing) + // even when the browser arrived via a different network host (e.g. 127.0.0.1). + $canonicalHost = $this->canonicalHost(); + $hostHeader = sprintf('%s:%d', $canonicalHost, $this->port); + $symfonyRequest->headers->set('Host', $hostHeader); + $symfonyRequest->server->set('SERVER_NAME', $canonicalHost); + $symfonyRequest->server->set('HTTP_HOST', $hostHeader); $debug = config('app.debug'); From f08f5f49f9de1418e4be70829fe328825527607f Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 28 Apr 2026 13:46:38 +0200 Subject: [PATCH 02/10] support paratest --- src/Drivers/LaravelHttpServer.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index d03df8f9..e504f1a8 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -175,6 +175,13 @@ public function syncCanonicalUrl(): void return; } + // Guard against being called when the Laravel container is not (yet) bootstrapped, + // e.g. from a Pest `beforeAll` hook, or between test files in a parallel worker + // after the previous test's app has been torn down. + if (! app()->bound('config')) { + return; + } + $url = $this->canonicalUrl(); config(['app.url' => $url]); From e23ffc48e2c8361fa458847f358f30e52a6b16a0 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 28 Apr 2026 16:01:52 +0200 Subject: [PATCH 03/10] fix tests the new withHost() contract requires a hostname that resolves to the loopback (any *.localhost works on macOS/modern Linux via nss-myhostname); arbitrary made-up TLDs like test.domain no longer work because the browser actually navigates to that host instead of the plugin spoofing the Host header. Worth calling out in the PR description as a behavior change. --- tests/Browser/Visit/SubdomainTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Browser/Visit/SubdomainTest.php b/tests/Browser/Visit/SubdomainTest.php index bfb33678..192359ac 100644 --- a/tests/Browser/Visit/SubdomainTest.php +++ b/tests/Browser/Visit/SubdomainTest.php @@ -70,18 +70,18 @@ 'host' => request()->getHost(), ]); - // Set global host: test.domain - pest()->browser()->withHost('test.domain'); + // Set global host: test.localhost + pest()->browser()->withHost('test.localhost'); // 1. Visit withHost: api.localhost visit('/api/health') ->withHost('api.localhost') ->assertSee('"host":"api.localhost"') - ->assertDontSee('test.domain'); + ->assertDontSee('test.localhost'); - // 2. Visit without withHost: should use global host "test.domain" + // 2. Visit without withHost: should use global host "test.localhost" visit('/') - ->assertSee('"host":"test.domain"') + ->assertSee('"host":"test.localhost"') ->assertDontSee('api.localhost'); }); From e261574d3c2a53107eb7631f4de62b21a82ffc99 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 28 Apr 2026 20:23:58 +0200 Subject: [PATCH 04/10] raise Amp body size limit to allow large JSON POST bodies --- src/Drivers/LaravelHttpServer.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index e504f1a8..f66850d6 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -7,6 +7,7 @@ use Amp\ByteStream\ReadableResourceStream; use Amp\Http\Cookie\RequestCookie; use Amp\Http\Server\DefaultErrorHandler; +use Amp\Http\Server\Driver\DefaultHttpDriverFactory; use Amp\Http\Server\HttpServer as AmpHttpServer; use Amp\Http\Server\HttpServerStatus; use Amp\Http\Server\Request as AmpRequest; @@ -99,7 +100,18 @@ public function start(): void return; } - $this->socket = $server = SocketHttpServer::createForDirectAccess(new NullLogger()); + $logger = new NullLogger(); + + // Bump the body size limit well above Amp's 128 KiB default so that + // POSTs with large JSON payloads (e.g. the booking-engine order submit, + // which can exceed 180 KiB) don't deadlock when the request body is read. + $this->socket = $server = SocketHttpServer::createForDirectAccess( + logger: $logger, + httpDriverFactory: new DefaultHttpDriverFactory( + logger: $logger, + bodySizeLimit: 64 * 1024 * 1024, + ), + ); $server->expose("{$this->host}:{$this->port}"); $server->start( From 1f64e12fcba93e1117d8f169d38aad7f5f66ac41 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 30 Apr 2026 17:30:52 +0200 Subject: [PATCH 05/10] formatting --- src/Configuration.php | 1 + src/Drivers/LaravelHttpServer.php | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Configuration.php b/src/Configuration.php index 6bc05d5c..9da880e1 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -105,6 +105,7 @@ public function withHost(?string $host): self Playwright::setHost($host); $http = ServerManager::instance()->http(); + if ($http instanceof LaravelHttpServer) { $http->syncCanonicalUrl(); } diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index f66850d6..9d8f0f76 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -100,13 +100,11 @@ public function start(): void return; } - $logger = new NullLogger(); - // Bump the body size limit well above Amp's 128 KiB default so that // POSTs with large JSON payloads (e.g. the booking-engine order submit, // which can exceed 180 KiB) don't deadlock when the request body is read. $this->socket = $server = SocketHttpServer::createForDirectAccess( - logger: $logger, + logger: $logger = new NullLogger(), httpDriverFactory: new DefaultHttpDriverFactory( logger: $logger, bodySizeLimit: 64 * 1024 * 1024, From 37f5ff07b936af1c7075fea9e29f19ede0c465e5 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 30 Apr 2026 17:31:31 +0200 Subject: [PATCH 06/10] fix deprecation notice --- src/Execution.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Execution.php b/src/Execution.php index 7ffc945d..a3ebc01c 100644 --- a/src/Execution.php +++ b/src/Execution.php @@ -171,7 +171,6 @@ private function resetAssertions(int $originalCount): void $reflector = new ReflectionClass(Assert::class); $property = $reflector->getProperty('count'); - // @phpstan-ignore-next-line - $property->setValue(Assert::class, $originalCount); + $property->setValue(null, $originalCount); } } From d91b4561de4d324019deaff0e2c6820f2a0d2784 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 4 Jun 2026 14:55:39 +0200 Subject: [PATCH 07/10] add tests --- src/Drivers/LaravelHttpServer.php | 12 ++-- tests/Browser/Visit/CanonicalUrlTest.php | 78 ++++++++++++++++++++++++ tests/Browser/Visit/LargeBodyTest.php | 59 ++++++++++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 tests/Browser/Visit/CanonicalUrlTest.php create mode 100644 tests/Browser/Visit/LargeBodyTest.php diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php index 9d8f0f76..d1813ad2 100644 --- a/src/Drivers/LaravelHttpServer.php +++ b/src/Drivers/LaravelHttpServer.php @@ -100,9 +100,9 @@ public function start(): void return; } - // Bump the body size limit well above Amp's 128 KiB default so that - // POSTs with large JSON payloads (e.g. the booking-engine order submit, - // which can exceed 180 KiB) don't deadlock when the request body is read. + // Increase the body size limit beyond Amp's 128-KiB + // default to prevent deadlocks when reading large + // JSON payloads in POST, PUT, and PATCH requests. $this->socket = $server = SocketHttpServer::createForDirectAccess( logger: $logger = new NullLogger(), httpDriverFactory: new DefaultHttpDriverFactory( @@ -173,10 +173,8 @@ public function bootstrap(): void } /** - * Re-sync Laravel's `app.url` and the URL generator's origin to the canonical URL. - * - * Called both at bootstrap and when the configured host changes mid-test (e.g. - * via `withHost(...)`), so that `route()`, `asset()`, and `config('app.url')` + * Re-sync Laravel's `app.url` and the URL generator's origin to the + * canonical URL, so that `route()`, `asset()`, and `config('app.url')` * stay consistent with the host the browser is navigating to. */ public function syncCanonicalUrl(): void diff --git a/tests/Browser/Visit/CanonicalUrlTest.php b/tests/Browser/Visit/CanonicalUrlTest.php new file mode 100644 index 00000000..7317d904 --- /dev/null +++ b/tests/Browser/Visit/CanonicalUrlTest.php @@ -0,0 +1,78 @@ +url()`, and the URL + * Playwright actually navigates to all line up on `http://{host}:{port}` — not + * a mix of the configured host (header-only) and the bound socket IP. + * + * This matters for SPAs (Inertia, Livewire) that compare a clicked link's + * origin to `window.location.origin` to decide whether navigation is internal. + * If the page is at `app.localhost:` but `route()` produces + * `127.0.0.1:`, the SPA treats every link as cross-origin and falls back + * to a full reload — which then 404s in the test harness. + */ + +it('exposes the canonical host on request()->url() when withHost is set', function (): void { + Route::domain('app.localhost')->get('/canonical/request-url', fn (Request $request) => $request->url()); + + pest()->browser()->withHost('app.localhost'); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + visit('/canonical/request-url') + ->assertUrlIs("http://app.localhost:{$port}/canonical/request-url") + ->assertSee("http://app.localhost:{$port}/canonical/request-url"); +}); + +it('generates route() URLs against the canonical host when withHost is set', function (): void { + Route::domain('app.localhost')->as('canonical.test')->get('/canonical/route-url', fn () => route('canonical.test')); + + pest()->browser()->withHost('app.localhost'); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + visit('/canonical/route-url') + ->assertSee("http://app.localhost:{$port}/canonical/route-url"); + + expect(route('canonical.test'))->toBe("http://app.localhost:{$port}/canonical/route-url"); +}); + +it('reverts to the bound IP origin when withHost(null) clears the configured host', function (): void { + Route::as('canonical.no-host')->get('/canonical/no-host', fn () => route('canonical.no-host')); + + // Ensure no leaking host from earlier tests. + pest()->browser()->withHost(null); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + visit('/canonical/no-host') + ->assertSee("http://127.0.0.1:{$port}/canonical/no-host"); + + expect(route('canonical.no-host'))->toBe("http://127.0.0.1:{$port}/canonical/no-host"); +}); + +it('updates the URL generator immediately when withHost changes mid-test', function (): void { + Route::domain('first.localhost')->as('canonical.first')->get('/canonical/first', fn () => route('canonical.first')); + Route::domain('second.localhost')->as('canonical.second')->get('/canonical/second', fn () => route('canonical.second')); + + $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line + + pest()->browser()->withHost('first.localhost'); + + expect(route('canonical.first'))->toBe("http://first.localhost:{$port}/canonical/first"); + + pest()->browser()->withHost('second.localhost'); + + // Per-request resync should also surface the new host on the page itself. + visit('/canonical/second') + ->assertSee("http://second.localhost:{$port}/canonical/second"); + + expect(route('canonical.second'))->toBe("http://second.localhost:{$port}/canonical/second"); +}); diff --git a/tests/Browser/Visit/LargeBodyTest.php b/tests/Browser/Visit/LargeBodyTest.php new file mode 100644 index 00000000..6f9a7c14 --- /dev/null +++ b/tests/Browser/Visit/LargeBodyTest.php @@ -0,0 +1,59 @@ +getBody()` to block forever, the test would hang, + * and the spinner-on-a-submit-button would never resolve. + * + * The plugin now wires a DefaultHttpDriverFactory with a 64 MiB body limit, so + * large JSON payloads round-trip through the test server without stalling. + */ + +it('accepts JSON request bodies larger than the default 128 KiB limit', function (): void { + Route::post('/large-body/echo', fn (Request $request): array => [ + 'received_bytes' => mb_strlen($request->getContent()), + 'received_count' => count($request->json('items', [])), + ]); + + // Build a payload that comfortably exceeds Amp's 128 KiB default. + $items = []; + for ($i = 0; $i < 5000; $i++) { + $items[] = [ + 'id' => $i, + 'name' => str_repeat('x', 32), + 'description' => str_repeat('y', 64), + ]; + } + $payload = json_encode(['items' => $items]); + + expect(mb_strlen($payload))->toBeGreaterThan(128 * 1024); + + // Render a tiny page that POSTs the payload via fetch and writes the JSON + // response into the DOM so we can assert against it. + Route::get('/large-body/post-from-page', fn () => ' + +

+            
+        
+    ');
+
+    visit('/large-body/post-from-page')
+        ->assertSee('"received_bytes":'.mb_strlen($payload))
+        ->assertSee('"received_count":5000');
+});

From 7ce93507039de49e0c2d8af46c36dae35bbdb767 Mon Sep 17 00:00:00 2001
From: Hafez Divandari 
Date: Thu, 4 Jun 2026 14:57:52 +0200
Subject: [PATCH 08/10] formatting

---
 tests/Browser/Visit/CanonicalUrlTest.php | 13 -------------
 tests/Browser/Visit/LargeBodyTest.php    | 12 ------------
 2 files changed, 25 deletions(-)

diff --git a/tests/Browser/Visit/CanonicalUrlTest.php b/tests/Browser/Visit/CanonicalUrlTest.php
index 7317d904..8a0f8c99 100644
--- a/tests/Browser/Visit/CanonicalUrlTest.php
+++ b/tests/Browser/Visit/CanonicalUrlTest.php
@@ -6,19 +6,6 @@
 use Illuminate\Support\Facades\Route;
 use Pest\Browser\ServerManager;
 
-/*
- * These tests cover the "canonical URL" wiring: when `withHost(...)` is set, the
- * URL generator, `route()` helper, server-side `request()->url()`, and the URL
- * Playwright actually navigates to all line up on `http://{host}:{port}` — not
- * a mix of the configured host (header-only) and the bound socket IP.
- *
- * This matters for SPAs (Inertia, Livewire) that compare a clicked link's
- * origin to `window.location.origin` to decide whether navigation is internal.
- * If the page is at `app.localhost:` but `route()` produces
- * `127.0.0.1:`, the SPA treats every link as cross-origin and falls back
- * to a full reload — which then 404s in the test harness.
- */
-
 it('exposes the canonical host on request()->url() when withHost is set', function (): void {
     Route::domain('app.localhost')->get('/canonical/request-url', fn (Request $request) => $request->url());
 
diff --git a/tests/Browser/Visit/LargeBodyTest.php b/tests/Browser/Visit/LargeBodyTest.php
index 6f9a7c14..10d67292 100644
--- a/tests/Browser/Visit/LargeBodyTest.php
+++ b/tests/Browser/Visit/LargeBodyTest.php
@@ -5,18 +5,6 @@
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Route;
 
-/*
- * Regression test for the Amp http-server body-size deadlock: by default
- * SocketHttpServer caps request bodies at 128 KiB (HttpDriver::DEFAULT_BODY_SIZE_LIMIT).
- * Any POST larger than that — common for SPA endpoints that submit nested form
- * state, e.g. a booking engine sending its full order graph as JSON — would
- * cause `(string) $request->getBody()` to block forever, the test would hang,
- * and the spinner-on-a-submit-button would never resolve.
- *
- * The plugin now wires a DefaultHttpDriverFactory with a 64 MiB body limit, so
- * large JSON payloads round-trip through the test server without stalling.
- */
-
 it('accepts JSON request bodies larger than the default 128 KiB limit', function (): void {
     Route::post('/large-body/echo', fn (Request $request): array => [
         'received_bytes' => mb_strlen($request->getContent()),

From 75672c36bfaca19a942b8cd7295f33ffa6dbf728 Mon Sep 17 00:00:00 2001
From: Hafez Divandari 
Date: Thu, 4 Jun 2026 15:05:46 +0200
Subject: [PATCH 09/10] formatting

---
 src/Drivers/LaravelHttpServer.php | 5 ++---
 src/Execution.php                 | 3 ++-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Drivers/LaravelHttpServer.php b/src/Drivers/LaravelHttpServer.php
index d1813ad2..4a664295 100644
--- a/src/Drivers/LaravelHttpServer.php
+++ b/src/Drivers/LaravelHttpServer.php
@@ -173,9 +173,8 @@ public function bootstrap(): void
     }
 
     /**
-     * Re-sync Laravel's `app.url` and the URL generator's origin to the
-     * canonical URL, so that `route()`, `asset()`, and `config('app.url')`
-     * stay consistent with the host the browser is navigating to.
+     * Re-sync Laravel's `app.url` and the URL generator's origin to the canonical URL, so that `route()`,
+     * `asset()`, and `config('app.url')` stay consistent with the host the browser is navigating to.
      */
     public function syncCanonicalUrl(): void
     {
diff --git a/src/Execution.php b/src/Execution.php
index a3ebc01c..7ffc945d 100644
--- a/src/Execution.php
+++ b/src/Execution.php
@@ -171,6 +171,7 @@ private function resetAssertions(int $originalCount): void
         $reflector = new ReflectionClass(Assert::class);
         $property = $reflector->getProperty('count');
 
-        $property->setValue(null, $originalCount);
+        // @phpstan-ignore-next-line
+        $property->setValue(Assert::class, $originalCount);
     }
 }

From 4f8cac6463ecf82eb0d0fd48792e7f14c9fee272 Mon Sep 17 00:00:00 2001
From: Hafez Divandari 
Date: Thu, 4 Jun 2026 15:33:07 +0200
Subject: [PATCH 10/10] fix a test

---
 tests/Browser/Visit/CanonicalUrlTest.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/tests/Browser/Visit/CanonicalUrlTest.php b/tests/Browser/Visit/CanonicalUrlTest.php
index 8a0f8c99..d3b0ebc1 100644
--- a/tests/Browser/Visit/CanonicalUrlTest.php
+++ b/tests/Browser/Visit/CanonicalUrlTest.php
@@ -51,14 +51,15 @@
 
     $port = ServerManager::instance()->http()->port; // @phpstan-ignore-line
 
-    pest()->browser()->withHost('first.localhost');
+    visit('/canonical/first')
+        ->withHost('first.localhost')
+        ->assertSee("http://first.localhost:{$port}/canonical/first");
 
     expect(route('canonical.first'))->toBe("http://first.localhost:{$port}/canonical/first");
 
-    pest()->browser()->withHost('second.localhost');
-
     // Per-request resync should also surface the new host on the page itself.
     visit('/canonical/second')
+        ->withHost('second.localhost')
         ->assertSee("http://second.localhost:{$port}/canonical/second");
 
     expect(route('canonical.second'))->toBe("http://second.localhost:{$port}/canonical/second");