Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions src/Api/PendingAwaitablePage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<string, mixed> $options
*/
private function buildAwaitablePage(array $options): AwaitableWebpage
{
$browser = Playwright::browser($this->browserType)->launch();

Expand All @@ -155,16 +177,51 @@ private function createAwaitablePage(): AwaitableWebpage
'timezoneId' => 'UTC',
'colorScheme' => Playwright::defaultColorScheme()->value,
...$this->device->context(),
...$this->options,
...$options,
]);

$context->addInitScript(InitScript::get());

$url = ComputeUrl::from($this->url);

return new AwaitableWebpage(
$context->newPage()->goto($url, $this->options),
$context->newPage()->goto($url, $options),
$url,
);
}

/**
* @param array<string, mixed> &$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);
}
}
}
2 changes: 1 addition & 1 deletion src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
139 changes: 139 additions & 0 deletions tests/Browser/Visit/SubdomainTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 => '
Expand Down Expand Up @@ -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"');
});
Loading