From 9cd919501db874bd6baffcfd5a9420e908561904 Mon Sep 17 00:00:00 2001 From: Lars Schou Date: Sat, 10 Jan 2026 21:15:00 +0100 Subject: [PATCH 1/5] Make ->withHost available when using visit(..). Do not override global host in visit chain --- src/Api/PendingAwaitablePage.php | 61 ++++++++++++++++++++++++++- src/Configuration.php | 2 +- src/Playwright/Browser.php | 2 +- src/Playwright/Page.php | 2 + tests/Browser/Visit/SubdomainTest.php | 45 +++++++++++++++++++- 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/Api/PendingAwaitablePage.php b/src/Api/PendingAwaitablePage.php index f8aad8d9..83ad7b77 100644 --- a/src/Api/PendingAwaitablePage.php +++ b/src/Api/PendingAwaitablePage.php @@ -118,6 +118,17 @@ public function withUserAgent(string $userAgent): self ]); } + /** + * Sets the host for the server. + */ + public function withHost(string $host): self + { + return new self($this->browserType, $this->device, $this->url, [ + 'host' => $host, + ...$this->options, + ]); + } + /** * Sets the timezone for the page. */ @@ -147,6 +158,17 @@ public function geolocation(float $latitude, float $longitude): self * Creates the webpage instance. */ private function createAwaitablePage(): AwaitableWebpage + { + $options = $this->options; + $host = $this->extractHost($options); + + return $this->withTemporaryHost($host, fn (): AwaitableWebpage => $this->buildAwaitablePage($options)); + } + + /** + * @param array $options + */ + private function buildAwaitablePage(array $options): AwaitableWebpage { $browser = Playwright::browser($this->browserType)->launch(); @@ -155,7 +177,7 @@ private function createAwaitablePage(): AwaitableWebpage 'timezoneId' => 'UTC', 'colorScheme' => Playwright::defaultColorScheme()->value, ...$this->device->context(), - ...$this->options, + ...$options, ]); $context->addInitScript(InitScript::get()); @@ -163,8 +185,43 @@ private function createAwaitablePage(): AwaitableWebpage $url = ComputeUrl::from($this->url); return new AwaitableWebpage( - $context->newPage()->goto($url, $this->options), + $context->newPage()->goto($url, $options), $url, ); } + + /** + * @param array &$options + */ + private function extractHost(array &$options): ?string + { + if (! array_key_exists('host', $options)) { + return null; + } + + $host = $options['host']; + + unset($options['host']); + + return is_string($host) ? $host : null; + } + + /** + * @param callable(): AwaitableWebpage $callback + */ + private function withTemporaryHost(?string $host, callable $callback): AwaitableWebpage + { + if ($host === null) { + return $callback(); + } + + $previousHost = Playwright::host(); + Playwright::setHost($host); + + try { + return $callback(); + } finally { + Playwright::setHost($previousHost); + } + } } diff --git a/src/Configuration.php b/src/Configuration.php index 0c2639f7..bf3c458f 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -98,7 +98,7 @@ public function userAgent(string $userAgent): self /** * Sets the host for the server. */ - public function withHost(string $host): self + public function withHost(?string $host): self { Playwright::setHost($host); diff --git a/src/Playwright/Browser.php b/src/Playwright/Browser.php index 12b0df26..f910328d 100644 --- a/src/Playwright/Browser.php +++ b/src/Playwright/Browser.php @@ -45,7 +45,7 @@ public function newContext(array $options = []): Context throw new BrowserAlreadyClosedException('The browser is already closed.'); } - $response = Client::instance()->execute($this->guid, 'newContext', $options); + $response = Client::instance()->execute(guid: $this->guid, method: 'newContext', params: $options); /** @var array{result: array{context: array{guid: string|null}}} $message */ foreach ($response as $message) { diff --git a/src/Playwright/Page.php b/src/Playwright/Page.php index d97f3db3..834e2693 100644 --- a/src/Playwright/Page.php +++ b/src/Playwright/Page.php @@ -581,6 +581,8 @@ public function close(): void $this->processVoidResponse($response); $this->closed = true; + + Playwright::setHost(null); } /** diff --git a/tests/Browser/Visit/SubdomainTest.php b/tests/Browser/Visit/SubdomainTest.php index 702157b6..5af57599 100644 --- a/tests/Browser/Visit/SubdomainTest.php +++ b/tests/Browser/Visit/SubdomainTest.php @@ -15,7 +15,7 @@ '); - pest()->browser()->withHost('app.localhost'); + pest()->browser()->withHost('api.localhost'); visit('/app-test') ->assertSee('Welcome to NON Subdomain') @@ -40,3 +40,46 @@ ->assertSee('"subdomain":"api"') ->assertSee('"host":"api.localhost"'); }); + +it('Can chain withHost on visit', function (): void { + Route::domain('{subdomain}.localhost')->group(function (): void { + Route::get('/api/health', fn (): array => [ + 'status' => 'ok', + 'subdomain' => request()->route('subdomain'), + 'host' => request()->getHost(), + ]); + }); + + visit('/api/health') + ->withHost('api.localhost') + ->assertSee('"status":"ok"') + ->assertSee('"subdomain":"api"') + ->assertSee('"host":"api.localhost"'); +}); + +it('Chaining withHost will not override global host', function (): void { + Route::domain('{subdomain}.localhost')->group(function (): void { + Route::get('/api/health', fn (): array => [ + 'subdomain' => request()->route('subdomain'), + 'host' => request()->getHost(), + ]); + }); + + Route::get('/', fn (): array => [ + 'host' => request()->getHost(), + ]); + + // Set global host: test.domain + pest()->browser()->withHost('test.domain'); + + // 1. Visit withHost: api.localhost + visit('/api/health') + ->withHost('api.localhost') + ->assertSee('"host":"api.localhost"') + ->assertDontSee('test.domain'); + + // 2. Visit without withHost: should use global host "test.domain" + visit('/') + ->assertSee('"host":"test.domain"') + ->assertDontSee('api.localhost'); +}); From ca9dbf46f63056e69aa0348281bfb552fbb8eae5 Mon Sep 17 00:00:00 2001 From: Lars Schou Date: Sat, 10 Jan 2026 21:34:53 +0100 Subject: [PATCH 2/5] Assert app route --- tests/Browser/Visit/SubdomainTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Browser/Visit/SubdomainTest.php b/tests/Browser/Visit/SubdomainTest.php index 5af57599..458f29b6 100644 --- a/tests/Browser/Visit/SubdomainTest.php +++ b/tests/Browser/Visit/SubdomainTest.php @@ -15,7 +15,7 @@ '); - pest()->browser()->withHost('api.localhost'); + pest()->browser()->withHost('app.localhost'); visit('/app-test') ->assertSee('Welcome to NON Subdomain') From dc7e66c6addfbbcb86d11f6e60023ba056a14e7f Mon Sep 17 00:00:00 2001 From: Lars Schou Date: Sun, 11 Jan 2026 20:10:49 +0100 Subject: [PATCH 3/5] Remove debug code --- src/Playwright/Page.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Playwright/Page.php b/src/Playwright/Page.php index 834e2693..d97f3db3 100644 --- a/src/Playwright/Page.php +++ b/src/Playwright/Page.php @@ -581,8 +581,6 @@ public function close(): void $this->processVoidResponse($response); $this->closed = true; - - Playwright::setHost(null); } /** From 2dfd31ef654efbfd0be3e77d076b8d75fcb3153d Mon Sep 17 00:00:00 2001 From: Lars Schou Date: Sun, 11 Jan 2026 20:30:02 +0100 Subject: [PATCH 4/5] Add more tests for edge cases in changing usage of withHost --- tests/Browser/Visit/SubdomainTest.php | 96 +++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/Browser/Visit/SubdomainTest.php b/tests/Browser/Visit/SubdomainTest.php index 458f29b6..bfb33678 100644 --- a/tests/Browser/Visit/SubdomainTest.php +++ b/tests/Browser/Visit/SubdomainTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; +use Pest\Browser\Playwright\Playwright; it('can visit non-subdomain routes with subdomain host browser testing', function (): void { Route::get('/app-test', fn (): string => ' @@ -83,3 +84,98 @@ ->assertSee('"host":"test.domain"') ->assertDontSee('api.localhost'); }); + +it('uses the first withHost when chained multiple times', function (): void { + // Because of the spread operator "...$this->options" in PendingAwaitablePage + Route::domain('{subdomain}.localhost')->group(function (): void { + Route::get('/api/info', fn (): array => [ + 'subdomain' => request()->route('subdomain'), + 'host' => request()->getHost(), + ]); + }); + + visit('/api/info') + ->withHost('first.localhost') + ->withHost('api.localhost') + ->assertSee('"host":"first.localhost"') + ->assertSee('"subdomain":"first"') + ->assertDontSee('api.localhost'); +}); + +it('withHost works correctly when combined with other options', function (): void { + Route::domain('{subdomain}.localhost')->group(function (): void { + Route::get('/api/locale', fn (): array => [ + 'host' => request()->getHost(), + 'subdomain' => request()->route('subdomain'), + ]); + }); + + visit('/api/locale') + ->withHost('api.localhost') + ->inDarkMode() + ->assertSee('"host":"api.localhost"') + ->assertSee('"subdomain":"api"'); +}); + +it('correctly alternates hosts across multiple visits', function (): void { + Route::domain('{subdomain}.localhost')->group(function (): void { + Route::get('/check', fn (): array => [ + 'host' => request()->getHost(), + 'subdomain' => request()->route('subdomain'), + ]); + }); + + // Visit 1: api.localhost + visit('/check') + ->withHost('api.localhost') + ->assertSee('"host":"api.localhost"') + ->assertSee('"subdomain":"api"'); + + // Visit 2: admin.localhost + visit('/check') + ->withHost('admin.localhost') + ->assertSee('"host":"admin.localhost"') + ->assertSee('"subdomain":"admin"'); + + // Visit 3: back to api.localhost + visit('/check') + ->withHost('api.localhost') + ->assertSee('"host":"api.localhost"') + ->assertSee('"subdomain":"api"'); +}); + +it('withHost works when no global host is configured', function (): void { + Route::domain('{subdomain}.localhost')->group(function (): void { + Route::get('/standalone', fn (): array => [ + 'host' => request()->getHost(), + 'subdomain' => request()->route('subdomain'), + ]); + }); + + // No pest()->browser()->withHost() call - just use per-visit withHost + visit('/standalone') + ->withHost('custom.localhost') + ->assertSee('"host":"custom.localhost"') + ->assertSee('"subdomain":"custom"'); +}); + +it('restores global host even when page creation encounters issues', function (): void { + Route::get('/restore-check', fn (): array => [ + 'host' => request()->getHost(), + ]); + + $originalHost = 'original.localhost'; + pest()->browser()->withHost($originalHost); + + // Perform a visit with a different host + visit('/restore-check') + ->withHost('temporary.localhost') + ->assertSee('"host":"temporary.localhost"'); + + // Verify global host is still the original after the visit + expect(Playwright::host())->toBe($originalHost); + + // Verify next visit without withHost uses the global host + visit('/restore-check') + ->assertSee('"host":"original.localhost"'); +}); From 1bca25a43461adb2d0bf0bf6abd533df9163c555 Mon Sep 17 00:00:00 2001 From: Lars Schou Date: Sun, 11 Jan 2026 20:33:58 +0100 Subject: [PATCH 5/5] Remove named parameters, as it has nothing to do with this PR --- src/Playwright/Browser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Playwright/Browser.php b/src/Playwright/Browser.php index f910328d..12b0df26 100644 --- a/src/Playwright/Browser.php +++ b/src/Playwright/Browser.php @@ -45,7 +45,7 @@ public function newContext(array $options = []): Context throw new BrowserAlreadyClosedException('The browser is already closed.'); } - $response = Client::instance()->execute(guid: $this->guid, method: 'newContext', params: $options); + $response = Client::instance()->execute($this->guid, 'newContext', $options); /** @var array{result: array{context: array{guid: string|null}}} $message */ foreach ($response as $message) {