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/tests/Browser/Visit/SubdomainTest.php b/tests/Browser/Visit/SubdomainTest.php index 702157b6..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 => ' @@ -40,3 +41,141 @@ ->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'); +}); + +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"'); +});