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

declare(strict_types=1);

namespace Pest\Browser\Api\Concerns;

use Pest\Browser\Api\PendingAwaitablePopup;

trait InteractsWithPopups
{
/**
* Set up a popup handler for this page.
*/
public function pendingPopup(): PendingAwaitablePopup
{
return $this->page->pendingPopup();
}

/**
* Remove any previously set popup handler.
*/
public function removePendingPopup(): self
{
$this->page->removePendingPopup();

return $this;
}

/**
* Check if a popup handler is currently set.
*/
public function hasPendingPopup(): bool
{
return $this->page->hasPendingPopup();
}
}
63 changes: 63 additions & 0 deletions src/Api/PendingAwaitablePopup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Pest\Browser\Api;

use Pest\Browser\Exceptions\BrowserExpectationFailedException;
use Pest\Browser\Execution;
use Pest\Browser\Playwright\Page;
use PHPUnit\Framework\ExpectationFailedException;

/**
* @mixin AwaitableWebpage
*/
final class PendingAwaitablePopup
{
/**
* The webpage instance that will be returned when the popup is created.
*/
private ?AwaitableWebpage $waitablePage = null;

/**
* Creates a new pending awaitable popup instance.
*/
public function __construct(
private readonly Page $opener,
) {
//
}

/**
* Calls the given method on page, waiting if needed for it to be created.
*
* @param array<int, mixed> $arguments
*/
public function __call(string $name, array $arguments): mixed
{
if ($this->waitablePage instanceof AwaitableWebpage) {
// @phpstan-ignore-next-line
return $this->waitablePage->{$name}(...$arguments);
}

$result = Execution::instance()->waitForExpectation(function () use ($name, $arguments): mixed {
if (is_null($this->waitablePage)) {
$e = new ExpectationFailedException('No popup opened');
throw BrowserExpectationFailedException::from($this->opener, $e);
}

// @phpstan-ignore-next-line
return $this->waitablePage->{$name}(...$arguments);
});

return $result === $this->waitablePage
? $this
: $result;
}

public function handlePopupCreation(string $popupGuid, string $frameGuid): void
{
$page = new Page($this->opener->context(), $popupGuid, $frameGuid);
$this->waitablePage = new AwaitableWebpage($page, '(popup)');
}
}
1 change: 1 addition & 0 deletions src/Api/Webpage.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Concerns\HasWaitCapabilities,
Concerns\InteractsWithElements,
Concerns\InteractsWithFrames,
Concerns\InteractsWithPopups,
Concerns\InteractsWithScreen,
Concerns\InteractsWithTab,
Concerns\InteractsWithToolbar,
Expand Down
61 changes: 60 additions & 1 deletion 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 WeakReference;

use function Amp\Websocket\Client\connect;

Expand All @@ -26,6 +27,13 @@ final class Client
*/
private ?WebsocketConnection $websocketConnection = null;

/**
* Registry of Page instances for handling events.
*
* @var array<string, WeakReference<Page>>
*/
private array $pages = [];

/**
* Default timeout for requests in milliseconds.
*/
Expand Down Expand Up @@ -87,8 +95,10 @@ public function execute(string $guid, string $method, array $params = [], array
$this->websocketConnection->sendText($requestJson);

while (true) {
// @phpstan-ignore-next-line
$responseJson = $this->fetch($this->websocketConnection);
/** @var array{id: string|null, params: array{add: string|null}, error: array{error: array{message: string|null}}} $response */

/** @var array{id: string|null, guid: string|null, method: string|null, params: array{add: string|null, type: string|null, guid: string|null, initializer: array{mainFrame: array{guid: string}, opener: array{guid: string}}|null }, error: array{error: array{message: string|null}}} $response */
$response = json_decode($responseJson, true);

if (isset($response['error']['error']['message'])) {
Expand All @@ -101,6 +111,17 @@ public function execute(string $guid, string $method, array $params = [], array
throw new ExpectationFailedException($message);
}

if (isset($response['method']) && $response['method'] === '__create__'
&& isset($response['params']['type']) && $response['params']['type'] === 'Page'
&& isset($response['guid'], $response['params']['guid'], $response['params']['initializer']['opener']['guid'])) {
$this->handlePopupCreation($response['params']['initializer']['opener']['guid'], $response['params']['guid'], $response['params']['initializer']);
}

if (isset($response['method']) && $response['method'] === '__dispose__'
&& isset($response['guid']) && $this->getPage($response['guid']) instanceof Page) {
$this->unregisterPage($response['guid']);
}

yield $response;

if (
Expand Down Expand Up @@ -128,6 +149,44 @@ public function timeout(): int
return $this->timeout;
}

/**
* Registers the current page for event handling.
*/
public function registerPage(string $guid, Page $page): void
{
$this->pages[$guid] = WeakReference::create($page);
}

/**
* Removes page from event handling.
*/
public function unregisterPage(string $guid): void
{
unset($this->pages[$guid]);
}

private function getPage(string $guid): ?Page
{
if (! array_key_exists($guid, $this->pages)) {
return null;
}

return $this->pages[$guid]->get();
}

/**
* Handles popup creation events.
*
* @param array{mainFrame: array{guid: string}, opener: array{guid: string}} $initializer
*/
private function handlePopupCreation(string $openerGuid, string $popupGuid, array $initializer): void
{
$opener = $this->getPage($openerGuid);
if ($opener instanceof Page && $opener->hasPendingPopup()) {
$opener->handlePopupCreation($popupGuid, $initializer['mainFrame']['guid']);
}
}

/**
* Fetches the response from the Playwright server.
*/
Expand Down
45 changes: 44 additions & 1 deletion src/Playwright/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Pest\Browser\Playwright;

use Generator;
use Pest\Browser\Api\PendingAwaitablePopup;
use Pest\Browser\Execution;
use Pest\Browser\Support\ImageDiffView;
use Pest\Browser\Support\JavaScriptSerializer;
Expand Down Expand Up @@ -32,6 +33,11 @@ final class Page
*/
private bool $strictLocators = true;

/**
* Pending AwaitablePage for a Popup.
*/
private ?PendingAwaitablePopup $pendingPopup = null;

/**
* Creates a new page instance.
*/
Expand All @@ -40,7 +46,7 @@ public function __construct(
private readonly string $guid,
private readonly string $frameGuid,
) {
//
Client::instance()->registerPage($guid, $this);
}

/**
Expand Down Expand Up @@ -566,6 +572,43 @@ public function expectScreenshot(bool $fullPage, bool $openDiff): void
}
}

/**
* Sets up a popup handler for this page.
*/
public function pendingPopup(): PendingAwaitablePopup
{
$this->pendingPopup = new PendingAwaitablePopup($this);

return $this->pendingPopup;
}

/**
* Removes any previously set popup handler from this page.
*/
public function removePendingPopup(): void
{
$this->pendingPopup = null;
}

/**
* Checks if a popup handler is currently set.
*/
public function hasPendingPopup(): bool
{
return $this->pendingPopup instanceof PendingAwaitablePopup;
}

/**
* Handles a popup creation event from the Playwright server.
*/
public function handlePopupCreation(string $popupGuid, string $frameGuid): void
{
if ($this->pendingPopup instanceof PendingAwaitablePopup) {
$this->pendingPopup->handlePopupCreation($popupGuid, $frameGuid);
$this->removePendingPopup();
}
}

/**
* Closes the page.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Support/JavaScriptSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public static function parseValue(mixed $value): mixed

// Handle arrays
if (isset($value['a'])) {
return array_map(fn (mixed $item): mixed => self::parseValue($item), $value['a']);
return array_map(self::parseValue(...), $value['a']);
}

// Handle objects
Expand Down
Loading