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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

### Added
- Launching a browser from a custom path through `withExecutablePath()` and `PW_EXECUTABLE_PATH`

## [1.5.0] - 2026-09-20

### Added
Expand Down
22 changes: 22 additions & 0 deletions docs/guide/browsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ vendor/bin/playwright-install chrome
`PW_CHANNEL=chrome` provides the same launch choice when configuration is built
with `PlaywrightConfigBuilder::fromEnv()`.

## Launch a browser from a custom path

An executable path launches a browser binary that Playwright does not manage,
such as a Chromium-based browser installed on the machine. Brave on macOS:

```php
$config = PlaywrightConfigBuilder::create()
->withExecutablePath('/Applications/Brave Browser.app/Contents/MacOS/Brave Browser')
->build();

$playwright = PlaywrightFactory::create($config);
$browser = $playwright->chromium()->launch();
```

The binary must match the launched browser type. Firefox and WebKit still need
Playwright's patched builds, so in practice this is for Chromium-based
browsers. Playwright does not guarantee that versions other than its bundled
one work.

`PW_EXECUTABLE_PATH` provides the same launch choice when configuration is
built with `PlaywrightConfigBuilder::fromEnv()`.

## Keep the browser cache consistent

Set `PLAYWRIGHT_BROWSERS_PATH` during installation and at runtime when managed
Expand Down
10 changes: 10 additions & 0 deletions src/Browser/BrowserBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ public function withChannel(string $channel): self
return $this;
}

/**
* Launches the browser binary at this path instead of the bundled one.
*/
public function withExecutablePath(string $path): self
{
$this->launchOptions['executablePath'] = $path;

return $this;
}

/**
* @param array{server: string, username?: string, password?: string, bypass?: string} $proxy
*/
Expand Down
4 changes: 4 additions & 0 deletions src/Configuration/PlaywrightConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public function __construct(
public readonly ?array $proxy = null,

public readonly ?LoggerInterface $logger = null,

public readonly ?string $executablePath = null,
) {
}

Expand Down Expand Up @@ -121,6 +123,7 @@ public function withNodePath(?string $nodePath): self
traceSnapshots: $this->traceSnapshots,
proxy: $this->proxy,
logger: $this->logger,
executablePath: $this->executablePath,
);
}

Expand All @@ -136,6 +139,7 @@ public function toArray(): array
'minNodeVersion' => $this->minNodeVersion,
'browser' => $this->browser->value,
'channel' => $this->channel,
'executablePath' => $this->executablePath,
'headless' => $this->headless,
'timeoutMs' => $this->timeoutMs,
'slowMoMs' => $this->slowMoMs,
Expand Down
15 changes: 14 additions & 1 deletion src/Configuration/PlaywrightConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ final class PlaywrightConfigBuilder

private ?string $channel = null;

private ?string $executablePath = null;

private bool $headless = true;

private int $timeoutMs = 30_000;
Expand Down Expand Up @@ -105,6 +107,9 @@ public static function fromEnv(): self
if ($channel = $get('PW_CHANNEL')) {
$b->withChannel($channel);
}
if ($executablePath = $get('PW_EXECUTABLE_PATH')) {
$b->withExecutablePath($executablePath);
}
if (($headless = $get('PW_HEADLESS')) !== null) {
$b->withHeadless(self::strToBool($headless));
}
Expand Down Expand Up @@ -166,6 +171,13 @@ public function withChannel(?string $channel): self
return $this;
}

public function withExecutablePath(?string $executablePath): self
{
$this->executablePath = $executablePath ?: null;

return $this;
}

public function withHeadless(bool $headless): self
{
$this->headless = $headless;
Expand Down Expand Up @@ -309,7 +321,8 @@ public function build(): PlaywrightConfig
traceScreenshots: $this->traceScreenshots,
traceSnapshots: $this->traceSnapshots,
proxy: $this->proxy,
logger: $this->logger
logger: $this->logger,
executablePath: $this->executablePath,
);
}

Expand Down
4 changes: 4 additions & 0 deletions src/PlaywrightClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ private function createBrowserBuilder(string $browserType): BrowserBuilder
$builder->withChannel($this->config->channel);
}

if (null !== $this->config->executablePath) {
$builder->withExecutablePath($this->config->executablePath);
}

if (null !== $this->config->proxy) {
$builder->withProxy($this->config->proxy);
}
Expand Down
14 changes: 14 additions & 0 deletions tests/Integration/Browser/BrowserBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ public function itSendsTheChannelInTheLaunchPayload(): void
$this->builder->withChannel('msedge')->launch();
}

#[Test]
public function itSendsTheExecutablePathInTheLaunchPayload(): void
{
$this->transport->expects($this->once())
->method('send')
->with($this->callback(static function (array $message): bool {
return 'launch' === $message['action']
&& '/usr/bin/chromium' === $message['options']['executablePath'];
}))
->willReturn(['browserId' => 'b', 'defaultContextId' => 'c', 'version' => '1.0']);

$this->builder->withExecutablePath('/usr/bin/chromium')->launch();
}

#[Test]
public function itSendsTheFullProxyArrayInTheLaunchPayload(): void
{
Expand Down
21 changes: 21 additions & 0 deletions tests/Integration/PlaywrightClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ public function itAppliesChannelProxyAndDownloadsDirFromTheConfig(): void
(new PlaywrightClient($transport, new NullLogger(), $config))->chromium()->launch();
}

#[Test]
public function itAppliesExecutablePathFromTheConfig(): void
{
$transport = $this->createMock(TransportInterface::class);
$transport->expects($this->once())
->method('send')
->with($this->callback(static function (array $message): bool {
return 'launch' === $message['action']
&& '/usr/bin/chromium' === $message['options']['executablePath'];
}))
->willReturn(['browserId' => 'b', 'defaultContextId' => 'c', 'version' => '1.0']);

$config = new PlaywrightConfig(
nodePath: '/usr/bin/node',
executablePath: '/usr/bin/chromium',
);

(new PlaywrightClient($transport, new NullLogger(), $config))->chromium()->launch();
}

#[Test]
public function itLeavesUnsetConfigOptionsOutOfTheLaunchPayload(): void
{
Expand All @@ -157,6 +177,7 @@ public function itLeavesUnsetConfigOptionsOutOfTheLaunchPayload(): void
->with($this->callback(static function (array $message): bool {
return 'launch' === $message['action']
&& !array_key_exists('channel', $message['options'])
&& !array_key_exists('executablePath', $message['options'])
&& !array_key_exists('proxy', $message['options'])
&& !array_key_exists('downloadsPath', $message['options']);
}))
Expand Down
7 changes: 7 additions & 0 deletions tests/Unit/Configuration/PlaywrightConfigBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ private function clearEnvironment(): void
'PLAYWRIGHT_BROWSERS_PATH',
'PW_BROWSER',
'PW_CHANNEL',
'PW_EXECUTABLE_PATH',
'PW_HEADLESS',
'PW_TIMEOUT_MS',
'PW_SLOWMO_MS',
Expand Down Expand Up @@ -101,6 +102,7 @@ public function itSupportsFluentInterface(): void
->withNodePath('/usr/local/bin/node')
->withMinNodeVersion('20.0.0')
->withChannel('stable')
->withExecutablePath('/usr/bin/chromium')
->withDownloadsDir('/tmp/downloads')
->withVideosDir('/tmp/videos')
->withLogger($logger)
Expand All @@ -113,6 +115,7 @@ public function itSupportsFluentInterface(): void
$this->assertEquals('/usr/local/bin/node', $config->nodePath);
$this->assertEquals('20.0.0', $config->minNodeVersion);
$this->assertEquals('stable', $config->channel);
$this->assertEquals('/usr/bin/chromium', $config->executablePath);
$this->assertEquals('/tmp/downloads', $config->downloadsDir);
$this->assertEquals('/tmp/videos', $config->videosDir);
$this->assertSame($logger, $config->logger);
Expand Down Expand Up @@ -239,6 +242,7 @@ public function itCreatesFromEnvironmentVariables(): void
putenv('PLAYWRIGHT_BROWSERS_PATH=/env/ms-playwright');
putenv('PW_BROWSER=firefox');
putenv('PW_CHANNEL=nightly');
putenv('PW_EXECUTABLE_PATH=/env/chromium');
putenv('PW_HEADLESS=false');
putenv('PW_TIMEOUT_MS=60000');
putenv('PW_SLOWMO_MS=500');
Expand All @@ -258,6 +262,7 @@ public function itCreatesFromEnvironmentVariables(): void
$this->assertEquals('/env/ms-playwright', $config->env['PLAYWRIGHT_BROWSERS_PATH'] ?? null);
$this->assertEquals(BrowserType::FIREFOX, $config->browser);
$this->assertEquals('nightly', $config->channel);
$this->assertEquals('/env/chromium', $config->executablePath);
$this->assertFalse($config->headless);
$this->assertEquals(60000, $config->timeoutMs);
$this->assertEquals(500, $config->slowMoMs);
Expand Down Expand Up @@ -329,12 +334,14 @@ public function itSanitizesEmptyStrings(): void
{
$config = PlaywrightConfigBuilder::create()
->withChannel('')
->withExecutablePath('')
->withDownloadsDir('')
->withVideosDir('')
->withTracing(true, '')
->build();

$this->assertNull($config->channel);
$this->assertNull($config->executablePath);
$this->assertNull($config->downloadsDir);
$this->assertNull($config->videosDir);
$this->assertNull($config->traceDir);
Expand Down
Loading