Skip to content
Closed
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
107 changes: 107 additions & 0 deletions src/Api/MultiBrowserPendingPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

namespace Pest\Browser\Api;

use ArrayIterator;
use Countable;
use IteratorAggregate;
use Pest\Browser\Playwright\Playwright;
use Traversable;

/**
* @internal
*
* @implements IteratorAggregate<int, PendingAwaitablePage>
*/
final readonly class MultiBrowserPendingPage implements Countable, IteratorAggregate
{
/**
* @param array<int, PendingAwaitablePage> $pendingPages
*/
public function __construct(
private array $pendingPages,
) {
//
}

/**
* Forward method calls to all pending pages for configuration.
*
* @param array<int, mixed> $arguments
*/
public function __call(string $name, array $arguments): self
{
foreach ($this->pendingPages as $pendingPage) {
$pendingPage->{$name}(...$arguments); // @phpstan-ignore-line
}

return $this;
}

public function getIterator(): Traversable
{
return new ArrayIterator($this->pendingPages);
}

public function count(): int
{
return count($this->pendingPages);
}

/**
* Execute a callback on each browser sequentially.
* Each browser is closed before switching to the next.
* If any browser fails, the exception is thrown immediately.
*
* @param callable(PendingAwaitablePage): mixed $callback
*/
public function each(callable $callback): self
{
$previousBrowserType = Playwright::defaultBrowserType();

try {
foreach ($this->pendingPages as $pendingPage) {
$browserType = $pendingPage->getBrowserType();

Playwright::closeOthers($browserType);
Playwright::setDefaultBrowserType($browserType);

$callback($pendingPage);
}
} finally {
Playwright::setDefaultBrowserType($previousBrowserType);
}

return $this;
}

/**
* Execute a callback and get results from each browser.
* If any browser fails, the exception is thrown immediately.
*
* @param callable(PendingAwaitablePage): mixed $callback
* @return array<int, mixed>
*/
public function eachResult(callable $callback): array
{
$results = [];
$previousBrowserType = Playwright::defaultBrowserType();

try {
foreach ($this->pendingPages as $pendingPage) {
$browserType = $pendingPage->getBrowserType();

Playwright::closeOthers($browserType);
Playwright::setDefaultBrowserType($browserType);

$results[] = $callback($pendingPage);
}
} finally {
Playwright::setDefaultBrowserType($previousBrowserType);
}

return $results;
}
}
28 changes: 28 additions & 0 deletions src/Api/PendingAwaitablePage.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ public function __call(string $name, array $arguments): mixed
return $this->waitablePage->{$name}(...$arguments);
}

/**
* Get the browser type for this pending page.
*/
public function getBrowserType(): BrowserType
{
return $this->browserType;
}

/**
* Sets the color scheme to dark mode.
*/
Expand Down Expand Up @@ -154,6 +162,26 @@ public function geolocation(float $latitude, float $longitude): self
]);
}

/**
* Sets the browsers to run the test on.
*
* @param array<int, BrowserType> $browserTypes
*/
public function browser(array $browserTypes): MultiBrowserPendingPage
{
$pendingPages = array_map(
fn (BrowserType $browserType): PendingAwaitablePage => new self(
$browserType,
$this->device,
$this->url,
$this->options,
),
$browserTypes,
);

return new MultiBrowserPendingPage($pendingPages);
}

/**
* Creates the webpage instance.
*/
Expand Down
29 changes: 27 additions & 2 deletions src/Playwright/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Generator;
use Pest\Browser\Exceptions\PlaywrightOutdatedException;
use PHPUnit\Framework\ExpectationFailedException;
use Throwable;

use function Amp\Websocket\Client\connect;

Expand Down Expand Up @@ -46,10 +47,10 @@ public static function instance(): self
/**
* Connects to the Playwright server.
*/
public function connectTo(string $url): void
public function connectTo(string $url, ?string $browser = null): void
{
if (! $this->websocketConnection instanceof WebsocketConnection) {
$browser = Playwright::defaultBrowserType()->toPlaywrightName();
$browser ??= Playwright::defaultBrowserType()->toPlaywrightName();

$launchOptions = json_encode([
'headless' => Playwright::isHeadless(),
Expand Down Expand Up @@ -128,6 +129,30 @@ public function timeout(): int
return $this->timeout;
}

/**
* Disconnects from the Playwright server.
*/
public function disconnect(): void
{
if ($this->websocketConnection instanceof WebsocketConnection) {
try {
$this->websocketConnection->close();
} catch (Throwable) {
// Ignore close errors
}
}

$this->websocketConnection = null;
}

/**
* Check if the client is connected.
*/
public function isConnected(): bool
{
return $this->websocketConnection instanceof WebsocketConnection;
}

/**
* Fetches the response from the Playwright server.
*/
Expand Down
34 changes: 34 additions & 0 deletions src/Playwright/Playwright.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Pest\Browser\Enums\BrowserType;
use Pest\Browser\Enums\ColorScheme;
use Pest\Browser\ServerManager;
use Throwable;

/**
* @internal
Expand Down Expand Up @@ -79,6 +81,29 @@ public static function close(): void
}

self::$browserTypes = [];

Client::instance()->disconnect();
}

/**
* Close all browsers except the specified type and reconnect.
*/
public static function closeOthers(BrowserType $exceptBrowserType): void
{
$exceptName = $exceptBrowserType->toPlaywrightName();

foreach (self::$browserTypes as $name => $browserType) {
if ($name !== $exceptName) {
$browserType->close();
}
}

self::$browserTypes = [];

Client::instance()->disconnect();

$url = ServerManager::instance()->playwright()->url();
Client::instance()->connectTo($url, $exceptName);
}

/**
Expand Down Expand Up @@ -195,6 +220,15 @@ public static function reset(): void
foreach (self::$browserTypes as $browserType) {
$browserType->reset();
}

if (! Client::instance()->isConnected()) {
try {
$url = ServerManager::instance()->playwright()->url();
Client::instance()->connectTo($url, self::defaultBrowserType()->toPlaywrightName());
} catch (Throwable) {
// Ignore - ServerManager may not be initialized yet
}
}
}

/**
Expand Down
15 changes: 15 additions & 0 deletions tests/Browser/Webpage/MultiBrowserErrorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

use Pest\Browser\Enums\BrowserType;

it('throws exception when browser assertion fails', function (): void {
Route::get('/error-test', fn (): string => '<h1>Error Test</h1>');

visit('/error-test')
->browser([BrowserType::CHROME, BrowserType::FIREFOX])
->each(function ($page): void {
$page->assertSee('Non-existent text');
});
})->throws(PHPUnit\Framework\ExpectationFailedException::class);
47 changes: 47 additions & 0 deletions tests/Browser/Webpage/MultiBrowserIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

use Pest\Browser\Enums\BrowserType;

it('may run assertions on multiple browsers using visit()', function (): void {
Route::get('/multi', fn (): string => '<h1>Multi Browser</h1>');

visit('/multi')->browser([BrowserType::CHROME, BrowserType::FIREFOX])
->each(function ($page): void {
$page->assertSee('Multi Browser');
});
});

it('may run assertions on multiple browsers with visit() and chaining', function (): void {
Route::get('/multi-dark', fn (): string => '<h1>Dark Mode Test</h1>');

visit('/multi-dark')
->browser([BrowserType::CHROME, BrowserType::FIREFOX])
->inDarkMode()
->each(function ($page): void {
$page->assertSee('Dark Mode Test');
});
});

it('may chain configuration options before each()', function (): void {
Route::get('/chain', fn (): string => '<h1>Chain Test</h1>');

visit('/chain')
->browser([BrowserType::CHROME, BrowserType::FIREFOX])
->inDarkMode()
->withLocale('en-US')
->each(function ($page): void {
$page->assertSee('Chain Test');
});
});

it('may use eachResult() to get results from each browser', function (): void {
Route::get('/each-result', fn (): string => '<h1>Result Test</h1>');

$results = visit('/each-result')
->browser([BrowserType::CHROME, BrowserType::FIREFOX])
->eachResult(fn ($page): string => $page->url());

expect($results)->toHaveCount(2);
});
59 changes: 59 additions & 0 deletions tests/Browser/Webpage/MultiBrowserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

use Pest\Browser\Api\MultiBrowserPendingPage;
use Pest\Browser\Api\PendingAwaitablePage;
use Pest\Browser\Enums\BrowserType;
use Pest\Browser\Enums\Device;

it('creates multi-browser pending pages', function (): void {
$page = new PendingAwaitablePage(
BrowserType::CHROME,
Device::DESKTOP,
'/test',
[],
);

$pages = $page->browser([BrowserType::CHROME, BrowserType::FIREFOX]);

expect($pages)->toBeInstanceOf(MultiBrowserPendingPage::class);
expect($pages)->toHaveCount(2);
});

it('iterates over multiple browsers', function (): void {
$page = new PendingAwaitablePage(
BrowserType::CHROME,
Device::DESKTOP,
'/test',
[],
);

$pages = $page->browser([BrowserType::CHROME, BrowserType::FIREFOX]);

$count = 0;
foreach ($pages as $p) {
expect($p)->toBeInstanceOf(PendingAwaitablePage::class);
$count++;
}

expect($count)->toBe(2);
});

it('executes callback for each browser', function (): void {
$page = new PendingAwaitablePage(
BrowserType::CHROME,
Device::DESKTOP,
'/test',
[],
);

$pages = $page->browser([BrowserType::CHROME, BrowserType::FIREFOX]);

$browsers = [];
$pages->each(function (PendingAwaitablePage $p) use (&$browsers): void {
$browsers[] = $p->getBrowserType();
});

expect($browsers)->toBe([BrowserType::CHROME, BrowserType::FIREFOX]);
});
Loading