Skip to content
Merged
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
7 changes: 7 additions & 0 deletions bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ const evaluateHandleOnTarget = async (target, { expression, arg }) => {

class ContextHandler extends BaseHandler {
async handle(command, method) {
// Closing a context drops it from the registry, and closing its browser drops it
// too, so an id we no longer know about belongs to a context that is closed.
if (method === 'isClosed') {
const known = this.contexts.get(command.contextId)?.context;
return this.wrapResult({ value: known ? known.isClosed() : true });
}

const context = this.validateResource(this.contexts, command.contextId, 'Context')?.context;
if (!context._initScriptPromise) context._initScriptPromise = Promise.resolve();

Expand Down
13 changes: 13 additions & 0 deletions bin/playwright-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class PlaywrightServer extends BaseHandler {
connect: () => this.connect(command),
connectOverCDP: () => this.connectOverCDP(command),
newContext: () => this.newContext(command),
bind: () => this.bindBrowser(command),
unbind: () => this.unbindBrowser(command),
close: () => this.closeBrowser(command),
exit: () => this.exit(),
launchServer: () => this.launchServer(command)
Expand Down Expand Up @@ -277,6 +279,17 @@ class PlaywrightServer extends BaseHandler {
return { contextId };
}

async bindBrowser(command) {
const browser = this.validateResource(this.browsers, command.browserId, 'Browser');
const { endpoint } = await browser.bind(command.title, command.options || {});
return { endpoint };
}

async unbindBrowser(command) {
const browser = this.validateResource(this.browsers, command.browserId, 'Browser');
await browser.unbind();
}

async closeBrowser(command) {
const browser = this.browsers.get(command.browserId);
if (!browser) return;
Expand Down
28 changes: 28 additions & 0 deletions src/Browser/Browser.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,32 @@ public function version(): string
{
return $this->version;
}

/**
* @param array{host?: string, port?: int, workspaceDir?: string, metadata?: array<string, mixed>} $options
*/
public function bind(string $title, array $options = []): string
{
$response = $this->transport->send([
'action' => 'bind',
'browserId' => $this->browserId,
'title' => $title,
'options' => $options,
]);

$endpoint = $response['endpoint'] ?? null;
if (!is_string($endpoint)) {
throw new ProtocolErrorException('Invalid endpoint returned from transport', 0);
}

return $endpoint;
}

public function unbind(): void
{
$this->transport->send([
'action' => 'unbind',
'browserId' => $this->browserId,
]);
}
}
15 changes: 15 additions & 0 deletions src/Browser/BrowserContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,21 @@ public function close(): void
]);
}

public function isClosed(): bool
{
$response = $this->transport->send([
'action' => 'context.isClosed',
'contextId' => $this->contextId,
]);

$value = $response['value'] ?? null;
if (!is_bool($value)) {
throw new ProtocolErrorException('Invalid isClosed response', 0);
}

return $value;
}

private function saveAutoTrace(): void
{
$dir = $this->config->traceDir ?? getcwd().'/traces';
Expand Down
5 changes: 5 additions & 0 deletions src/Browser/BrowserContextInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ public function clearPermissions(): void;

public function close(): void;

/**
* Whether the context is closed, including when its browser was closed instead.
*/
public function isClosed(): bool;

/**
* @param array<string>|null $urls
*
Expand Down
16 changes: 16 additions & 0 deletions src/Browser/BrowserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,20 @@ public function browserType(): BrowserType;
public function isConnected(): bool;

public function version(): string;

/**
* Exposes this browser so other Playwright clients can connect to it and drive it.
*
* Binding twice without an intervening unbind() is an error.
*
* @param array{host?: string, port?: int, workspaceDir?: string, metadata?: array<string, mixed>} $options
*
* @return string the endpoint to connect to: a local socket path, or a ws:// URL when host or port is given
*/
public function bind(string $title, array $options = []): string;

/**
* Tears down the server started by bind(); does nothing when the browser is not bound.
*/
public function unbind(): void;
}
12 changes: 12 additions & 0 deletions tests/Integration/Browser/BrowserContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ public function itCreatesANewPageInContext(): void
$page->close();
}

#[Test]
public function itReportsWhetherItIsClosed(): void
{
$context = $this->browser->newContext();

$this->assertFalse($context->isClosed());

$context->close();

$this->assertTrue($context->isClosed());
}

#[Test]
public function itManagesCookies(): void
{
Expand Down
46 changes: 46 additions & 0 deletions tests/Integration/Browser/BrowserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,50 @@ public function itsContextsPointBackToIt(): void

$context->close();
}

#[Test]
public function itBindsTheBrowserToALocalSocket(): void
{
$endpoint = $this->browser->bind('playwright-php-socket');

try {
$this->assertNotEmpty($endpoint);
} finally {
$this->browser->unbind();
}
}

#[Test]
public function itBindsTheBrowserToAWebSocketWhenAPortIsGiven(): void
{
$endpoint = $this->browser->bind('playwright-php-ws', ['host' => '127.0.0.1', 'port' => 0]);

try {
$this->assertStringStartsWith('ws://127.0.0.1:', $endpoint);
} finally {
$this->browser->unbind();
}
}

#[Test]
public function itUnbindsABrowserThatWasNeverBound(): void
{
$this->browser->unbind();

$this->assertTrue($this->browser->isConnected());
}

#[Test]
public function itReportsItsContextsAsClosedOnceTheBrowserIsClosed(): void
{
// A browser of its own: closing the shared one would force every later test to relaunch it.
$browser = $this->playwright->chromium()->launch();
$context = $browser->newContext();

$this->assertFalse($context->isClosed());

$browser->close();

$this->assertTrue($context->isClosed());
}
}
36 changes: 36 additions & 0 deletions tests/Unit/Browser/BrowserContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Playwright\Browser\StorageState;
use Playwright\Configuration\PlaywrightConfig;
use Playwright\Credentials\CredentialsInterface;
use Playwright\Exception\ProtocolErrorException;
use Playwright\Network\NetworkThrottling;
use Playwright\Page\PageInterface;
use Playwright\Tracing\TracingInterface;
Expand Down Expand Up @@ -111,6 +112,41 @@ public function testClose(): void
$this->context->close();
}

public function testIsClosed(): void
{
$this->mockTransport
->expects($this->once())
->method('send')
->with([
'action' => 'context.isClosed',
'contextId' => 'context_1',
])
->willReturn(['value' => true]);

$this->assertTrue($this->context->isClosed());
}

public function testIsClosedReturnsFalseForAnOpenContext(): void
{
$this->mockTransport
->method('send')
->willReturn(['value' => false]);

$this->assertFalse($this->context->isClosed());
}

public function testIsClosedThrowsOnANonBooleanResponse(): void
{
$this->mockTransport
->method('send')
->willReturn(['success' => true]);

$this->expectException(ProtocolErrorException::class);
$this->expectExceptionMessage('Invalid isClosed response');

$this->context->isClosed();
}

public function testAddCookies(): void
{
$cookies = [
Expand Down
50 changes: 50 additions & 0 deletions tests/Unit/Browser/BrowserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Playwright\Browser\Browser;
use Playwright\Browser\BrowserType;
use Playwright\Configuration\PlaywrightConfig;
use Playwright\Exception\ProtocolErrorException;
use Playwright\Transport\TransportInterface;

#[CoversClass(Browser::class)]
Expand All @@ -38,6 +39,55 @@ public function testBrowserTypeDefaultsToChromium(): void
$this->assertSame(BrowserType::CHROMIUM, $browser->browserType());
}

public function testBindSendsTheTitleAndReturnsTheEndpoint(): void
{
$transport = $this->createMock(TransportInterface::class);
$transport
->expects($this->once())
->method('send')
->with([
'action' => 'bind',
'browserId' => 'b',
'title' => 'my-browser',
'options' => ['port' => 0],
])
->willReturn(['endpoint' => 'ws://127.0.0.1:4242/abc']);

$browser = new Browser($transport, 'b', 'ctx_default', '1.0', new PlaywrightConfig());

$this->assertSame('ws://127.0.0.1:4242/abc', $browser->bind('my-browser', ['port' => 0]));
}

public function testBindThrowsWhenNoEndpointComesBack(): void
{
$transport = $this->createMock(TransportInterface::class);
$transport->method('send')->willReturn(['success' => true]);

$browser = new Browser($transport, 'b', 'ctx_default', '1.0', new PlaywrightConfig());

$this->expectException(ProtocolErrorException::class);
$this->expectExceptionMessage('Invalid endpoint returned from transport');

$browser->bind('my-browser');
}

public function testUnbindSendsTheBrowserId(): void
{
$transport = $this->createMock(TransportInterface::class);
$transport
->expects($this->once())
->method('send')
->with([
'action' => 'unbind',
'browserId' => 'b',
])
->willReturn([]);

$browser = new Browser($transport, 'b', 'ctx_default', '1.0', new PlaywrightConfig());

$browser->unbind();
}

private function browser(BrowserType $type): Browser
{
return new Browser($this->transport(), 'b', 'ctx_default', '1.0', new PlaywrightConfig(), $type);
Expand Down
Loading