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
12 changes: 12 additions & 0 deletions src/Browser/BrowserBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Playwright\Browser;

use Playwright\Configuration\PlaywrightConfig;
use Playwright\Exception\MissingDependencyException;
use Playwright\Exception\PlaywrightException;
use Playwright\Transport\Sanitizer;
use Playwright\Transport\TransportInterface;
Expand Down Expand Up @@ -117,6 +118,17 @@ public function launch(): BrowserInterface
if (!is_string($response['error'])) {
throw new PlaywrightException('Browser launch failed with unknown error');
}

// Playwright makes both checks itself, but points at "npx playwright install" and
// "npx playwright install-deps", which is not how this package installs them.
if (str_contains($response['error'], "Executable doesn't exist")) {
throw MissingDependencyException::browsers($response['error']);
}

if (str_contains($response['error'], 'Host system is missing dependencies')) {
throw MissingDependencyException::hostLibraries($response['error']);
}

throw new PlaywrightException($response['error']);
}

Expand Down
70 changes: 70 additions & 0 deletions src/Exception/MissingDependencyException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

/*
* This file is part of the community-maintained Playwright PHP project.
* It is not affiliated with or endorsed by Microsoft.
*
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Playwright\Exception;

/**
* Thrown when the Playwright server or its browsers have not been installed.
*
* These are setup problems rather than runtime failures, so the message always names the
* command that fixes them.
*/
class MissingDependencyException extends RuntimeException
{
public const INSTALL_COMMAND = 'vendor/bin/playwright-install --browsers';
public const WITH_DEPENDENCIES_COMMAND = 'vendor/bin/playwright-install --with-deps';

/**
* The Node server could not load the "playwright" package, i.e. the server's dependencies
* were never installed.
*/
public static function server(?string $details = null): self
{
$message = \sprintf(
'The Playwright server is not installed. Run "%s" to install it.',
self::INSTALL_COMMAND,
);

if (null !== $details && '' !== $details) {
$message .= "\n\n".$details;
}

return new self($message);
}

/**
* Playwright is installed but the browser binaries it needs are missing.
*/
public static function browsers(string $details): self
{
return new self(\sprintf(
"The requested browser is not installed. Run \"%s\" to download it.\n\n%s",
self::INSTALL_COMMAND,
$details,
));
}

/**
* The browsers are installed but the host is missing libraries they link against. Playwright
* runs this check on Linux and Windows only.
*/
public static function hostLibraries(string $details): self
{
return new self(\sprintf(
"The host is missing system libraries the browsers need. Run \"%s\" to install them.\n\n%s",
self::WITH_DEPENDENCIES_COMMAND,
$details,
));
}
}
26 changes: 25 additions & 1 deletion src/Transport/JsonRpc/ProcessJsonRpcClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Playwright\Transport\JsonRpc;

use Playwright\Exception\DisconnectedException;
use Playwright\Exception\MissingDependencyException;
use Playwright\Exception\NetworkException;
use Playwright\Exception\TimeoutException;
use Psr\Clock\ClockInterface;
Expand Down Expand Up @@ -240,7 +241,20 @@ private function ensureProcessRunning(): void
{
if (!$this->process->isRunning()) {
$exitCode = $this->process->getExitCode() ?? -1;
throw new DisconnectedException(sprintf('Process exited with code %d', $exitCode), 0, null, ['exitCode' => $exitCode, 'pid' => $this->process->getPid()]);
$stderr = trim($this->processLauncher->getStderrOutput());

if (self::indicatesMissingServer($stderr)) {
throw MissingDependencyException::server($stderr);
}

// Without the stderr excerpt an exit code alone gives the user nothing to act on.
$message = sprintf('Process exited with code %d', $exitCode);

if ('' !== $stderr) {
$message .= sprintf(":\n\n%s", $stderr);
}

throw new DisconnectedException($message, 0, null, ['exitCode' => $exitCode, 'pid' => $this->process->getPid(), 'stderr' => $stderr]);
}

try {
Expand All @@ -249,4 +263,14 @@ private function ensureProcessRunning(): void
throw new DisconnectedException('Process health check failed: '.$e->getMessage(), 0, $e);
}
}

/**
* playwright-server.js starts with require('playwright'), so a missing install kills the
* process immediately with a module resolution error.
*/
private static function indicatesMissingServer(string $stderr): bool
{
return str_contains($stderr, "Cannot find module 'playwright'")
|| str_contains($stderr, 'Cannot find module "playwright"');
}
}
4 changes: 2 additions & 2 deletions src/Transport/ServerFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace Playwright\Transport;

use Playwright\Exception\NetworkException;
use Playwright\Exception\MissingDependencyException;
use Playwright\Node\NodeBinaryResolver;
use Playwright\Node\NodeBinaryResolverInterface;

Expand Down Expand Up @@ -109,7 +109,7 @@ public function findServer(): array
{
$playwrightPath = $this->findPlaywright();
if (!$playwrightPath) {
throw new NetworkException('Playwright not found. Please run: npm install playwright');
throw MissingDependencyException::server();
}

$nodeResolver = $this->nodeResolver ?? new NodeBinaryResolver();
Expand Down
48 changes: 48 additions & 0 deletions tests/Integration/Browser/BrowserBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use Playwright\Browser\Browser;
use Playwright\Browser\BrowserBuilder;
use Playwright\Configuration\PlaywrightConfig;
use Playwright\Exception\MissingDependencyException;
use Playwright\Exception\PlaywrightException;
use Playwright\Transport\TransportInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand Down Expand Up @@ -269,4 +271,50 @@ public function itSendsContextOptionsSoTheDefaultContextRecordsVideo(): void

$builder->launch();
}

#[Test]
public function itReportsMissingBrowsersWithTheInstallCommand(): void
{
$transport = $this->createMock(TransportInterface::class);
$transport->method('send')->willReturn([
'error' => "browserType.launch: Executable doesn't exist at /tmp/chrome\nPlease run the following command",
]);

$builder = new BrowserBuilder('chromium', $transport, new NullLogger(), new PlaywrightConfig());

$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessageMatches('/vendor\/bin\/playwright-install/');

$builder->launch();
}

#[Test]
public function itReportsMissingHostDependenciesWithTheWithDepsCommand(): void
{
$transport = $this->createMock(TransportInterface::class);
$transport->method('send')->willReturn([
'error' => "browserType.launch: Host system is missing dependencies to run browsers.\nMissing libraries: libnss3",
]);

$builder = new BrowserBuilder('chromium', $transport, new NullLogger(), new PlaywrightConfig());

$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessageMatches('/vendor\/bin\/playwright-install --with-deps/');

$builder->launch();
}

#[Test]
public function itLeavesUnrelatedLaunchErrorsAlone(): void
{
$transport = $this->createMock(TransportInterface::class);
$transport->method('send')->willReturn(['error' => 'browserType.launch: something else went wrong']);

$builder = new BrowserBuilder('chromium', $transport, new NullLogger(), new PlaywrightConfig());

$this->expectException(PlaywrightException::class);
$this->expectExceptionMessage('browserType.launch: something else went wrong');

$builder->launch();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Playwright\BrowserServer\BrowserServer;
use Playwright\Exception\MissingDependencyException;
use Playwright\Exception\ProcessLaunchException;
use Playwright\Exception\TransportException;
use Playwright\PlaywrightFactory;
Expand All @@ -43,7 +44,7 @@ public function testLaunchServerChromiumAndClose(): void

// Try graceful close
$server->close();
} catch (ProcessLaunchException|TransportException $e) {
} catch (MissingDependencyException|ProcessLaunchException|TransportException $e) {
$this->markTestSkipped('launchServer not available or environment missing browsers: '.$e->getMessage());
} catch (\Throwable $e) {
$this->fail('Unexpected failure launching browser server: '.$e->getMessage());
Expand Down
56 changes: 56 additions & 0 deletions tests/Unit/Exception/MissingDependencyExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

/*
* This file is part of the community-maintained Playwright PHP project.
* It is not affiliated with or endorsed by Microsoft.
*
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Playwright\Tests\Unit\Exception;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Playwright\Exception\MissingDependencyException;
use Playwright\Exception\RuntimeException;

#[CoversClass(MissingDependencyException::class)]
final class MissingDependencyExceptionTest extends TestCase
{
public function testServerDependenciesNamesTheInstallCommand(): void
{
$exception = MissingDependencyException::server();

$this->assertInstanceOf(RuntimeException::class, $exception);
$this->assertStringContainsString(MissingDependencyException::INSTALL_COMMAND, $exception->getMessage());
}

public function testServerDependenciesKeepsTheUnderlyingDetails(): void
{
$exception = MissingDependencyException::server("Error: Cannot find module 'playwright'");

$this->assertStringContainsString(MissingDependencyException::INSTALL_COMMAND, $exception->getMessage());
$this->assertStringContainsString("Cannot find module 'playwright'", $exception->getMessage());
}

public function testBrowsersNamesTheInstallCommandAndKeepsTheDetails(): void
{
$exception = MissingDependencyException::browsers("browserType.launch: Executable doesn't exist at /tmp/chrome");

$this->assertStringContainsString(MissingDependencyException::INSTALL_COMMAND, $exception->getMessage());
$this->assertStringContainsString("Executable doesn't exist at /tmp/chrome", $exception->getMessage());
}

public function testHostDependenciesNamesTheWithDepsCommandAndKeepsTheDetails(): void
{
$exception = MissingDependencyException::hostLibraries('Host system is missing dependencies to run browsers.');

$this->assertStringContainsString(MissingDependencyException::WITH_DEPENDENCIES_COMMAND, $exception->getMessage());
$this->assertStringContainsString('Host system is missing dependencies', $exception->getMessage());
}
}
31 changes: 31 additions & 0 deletions tests/Unit/Transport/ServerFinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Playwright\Exception\MissingDependencyException;
use Playwright\Node\NodeBinaryResolverInterface;
use Playwright\Transport\ServerFinder;

Expand All @@ -31,6 +32,36 @@ public function testFindPlaywrightReturnsPathOrNull(): void
$this->assertTrue(is_string($result) || null === $result);
}

public function testFindServerNamesTheInstallCommandWhenPlaywrightIsMissing(): void
{
$tmpCwd = sys_get_temp_dir().'/pwphp_missing_'.bin2hex(random_bytes(4));
$originalPath = getenv('PLAYWRIGHT_PATH');
$originalCwd = getcwd();

try {
mkdir($tmpCwd.'/a/b', 0777, true);
chdir($tmpCwd.'/a/b');
putenv('PLAYWRIGHT_PATH');

$this->expectException(MissingDependencyException::class);
$this->expectExceptionMessageMatches('/vendor\/bin\/playwright-install/');

(new ServerFinder())->findServer();
} finally {
if (is_string($originalCwd)) {
chdir($originalCwd);
}

if (false !== $originalPath) {
putenv('PLAYWRIGHT_PATH='.$originalPath);
}

@rmdir($tmpCwd.'/a/b');
@rmdir($tmpCwd.'/a');
@rmdir($tmpCwd);
}
}

public function testFindServerWorksWhenPlaywrightFound(): void
{
$nodeResolver = $this->createMock(NodeBinaryResolverInterface::class);
Expand Down
Loading