diff --git a/src/Playwright/Client.php b/src/Playwright/Client.php index 8d81e4ae..6a60ee0f 100644 --- a/src/Playwright/Client.php +++ b/src/Playwright/Client.php @@ -10,6 +10,7 @@ use Pest\Browser\Exceptions\PlaywrightOutdatedException; use PHPUnit\Framework\ExpectationFailedException; use RuntimeException; +use Throwable; use function Amp\Websocket\Client\connect; @@ -134,6 +135,32 @@ public function execute(string $guid, string $method, array $params = [], array } } + /** + * Drops the current connection so the next test can initialize again. + * + * Playwright accepts initialize only once per socket, and marks the + * socket initialized before the browser launch finishes. + */ + public function disconnect(): void + { + if (! $this->websocketConnection instanceof WebsocketConnection) { + return; + } + + $connection = $this->websocketConnection; + $this->websocketConnection = null; + + if ($connection->isClosed()) { + return; + } + + try { + $connection->close(); + } catch (Throwable) { + // The socket can already be gone when initialize fails. + } + } + /** * Sets the timeout in milliseconds for requests. */ diff --git a/src/Playwright/Playwright.php b/src/Playwright/Playwright.php index 99325098..e0be0dd0 100644 --- a/src/Playwright/Playwright.php +++ b/src/Playwright/Playwright.php @@ -6,6 +6,7 @@ use Pest\Browser\Enums\BrowserType; use Pest\Browser\Enums\ColorScheme; +use Throwable; /** * @internal @@ -18,6 +19,14 @@ final class Playwright */ public const int DEFAULT_SLOW_MO = 500; + /** + * Timeout for the one-time browser launch, in milliseconds. + * + * Kept apart from the action timeout so a slow Chromium start does not + * fail the limit used for page actions. + */ + private const int LAUNCH_TIMEOUT = 30_000; + /** * Browser types * @@ -307,49 +316,66 @@ public static function usingTimeout(int $timeout, callable $callback): mixed } /** - * Initialize Playwright + * Initializes Playwright on the current connection. + * + * Launch uses its own timeout, separate from the action timeout. A failed + * initialize drops the connection: Playwright accepts that call only once. */ private static function initialize(string $browser): BrowserFactory { - $response = Client::instance()->execute( - '', - 'initialize', - [ - 'sdkLanguage' => 'javascript', - ] - ); - - /** @var array{method: string|null, params: array{type: string|null, guid: string, initializer: array{name: string|null, preLaunchedBrowser?: array{guid: string}}}} $message */ - foreach ($response as $message) { - if ( - isset($message['method']) - && $message['method'] === '__create__' - && isset($message['params']['type']) - && $message['params']['type'] === 'Playwright' - && isset($message['params']['initializer']['preLaunchedBrowser']) - ) { - self::$browserTypes[$browser]->prelaunch( - $message['params']['initializer']['preLaunchedBrowser']['guid'], - ); - } - - if ( - isset($message['method']) - && $message['method'] === '__create__' - && isset($message['params']['type']) - && $message['params']['type'] === 'BrowserType' - ) { - $name = $message['params']['initializer']['name'] ?? ''; - - self::$browserTypes[$name] = new BrowserFactory( - $message['params']['guid'], - $name, - self::$headless, - self::$userAgent - ); - } - } + $client = Client::instance(); - return self::$browserTypes[$browser]; + try { + return self::usingTimeout( + max($client->timeout(), self::LAUNCH_TIMEOUT), + function () use ($browser, $client): BrowserFactory { + $response = $client->execute( + '', + 'initialize', + [ + 'sdkLanguage' => 'javascript', + ] + ); + + /** @var array{method: string|null, params: array{type: string|null, guid: string, initializer: array{name: string|null, preLaunchedBrowser?: array{guid: string}}}} $message */ + foreach ($response as $message) { + if ( + isset($message['method']) + && $message['method'] === '__create__' + && isset($message['params']['type']) + && $message['params']['type'] === 'Playwright' + && isset($message['params']['initializer']['preLaunchedBrowser']) + ) { + self::$browserTypes[$browser]->prelaunch( + $message['params']['initializer']['preLaunchedBrowser']['guid'], + ); + } + + if ( + isset($message['method']) + && $message['method'] === '__create__' + && isset($message['params']['type']) + && $message['params']['type'] === 'BrowserType' + ) { + $name = $message['params']['initializer']['name'] ?? ''; + + self::$browserTypes[$name] = new BrowserFactory( + $message['params']['guid'], + $name, + self::$headless, + self::$userAgent + ); + } + } + + return self::$browserTypes[$browser]; + } + ); + } catch (Throwable $throwable) { + self::$browserTypes = []; + $client->disconnect(); + + throw $throwable; + } } } diff --git a/tests/Browser/Visit/InitializeRecoveryTest.php b/tests/Browser/Visit/InitializeRecoveryTest.php new file mode 100644 index 00000000..53027e14 --- /dev/null +++ b/tests/Browser/Visit/InitializeRecoveryTest.php @@ -0,0 +1,30 @@ + '