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
22 changes: 22 additions & 0 deletions bin/playwright-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ class PlaywrightServer extends BaseHandler {
});
}

// Pages opened by the browser itself (window.open(), target="_blank", ...) never go through
// createNewPage()/waitForEvent('page')/waitForPopup - subscribe passively so the PHP client
// learns about them without having to anticipate every popup-opening action in advance.
registerContextPopups(context, contextId) {
context.on('page', (newPage) => {
setImmediate(() => {
for (const existing of this.pages.values()) {
if (existing === newPage) return;
}
const pageId = this.generateId('page');
this.pages.set(pageId, newPage);
this.pageContexts.set(pageId, contextId);
this.setupPageEventListeners(newPage, pageId);
sendFramedResponse({ objectId: contextId, event: 'page', params: { pageId } });
});
});
}

formatEventParams(eventName, eventData) {
const formatters = {
console: () => ({ type: eventData.type(), text: eventData.text(), args: [], location: eventData.location ? eventData.location() : {} }),
Expand Down Expand Up @@ -174,6 +192,7 @@ class PlaywrightServer extends BaseHandler {
const context = await browser.newContext();
const contextId = this.generateId('context');
this.contexts.set(contextId, { context, browserId, id: contextId });
this.registerContextPopups(context, contextId);
return { browserId, defaultContextId: contextId, version: browser.version() };
} catch (error) {
logger.error('launchBrowser error', { message: error.message });
Expand Down Expand Up @@ -204,6 +223,7 @@ class PlaywrightServer extends BaseHandler {
const context = await browser.newContext(command.options || {});
const contextId = this.generateId('context');
this.contexts.set(contextId, { context, browserId: command.browserId, id: contextId });
this.registerContextPopups(context, contextId);
return { contextId };
}

Expand Down Expand Up @@ -301,6 +321,7 @@ class PlaywrightServer extends BaseHandler {
const context = await attached.newContext();
const contextId = this.generateId('context');
this.contexts.set(contextId, { context, browserId, id: contextId });
this.registerContextPopups(context, contextId);
return { browserId, defaultContextId: contextId, version: attached.version() };
} catch (error) {
logger.error('connect error', { message: error.message });
Expand All @@ -321,6 +342,7 @@ class PlaywrightServer extends BaseHandler {
const context = await attached.newContext();
const contextId = this.generateId('context');
this.contexts.set(contextId, { context, browserId, id: contextId });
this.registerContextPopups(context, contextId);
return { browserId, defaultContextId: contextId, version: attached.version() };
} catch (error) {
logger.error('connectOverCDP error', { message: error.message });
Expand Down
87 changes: 87 additions & 0 deletions tests/Integration/Popup/PassivePopupDiscoveryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?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\Integration\Popup;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Playwright\Browser\BrowserContext;
use Playwright\Browser\BrowserContextInterface;
use Playwright\Testing\PlaywrightTestCaseTrait;

/**
* Regression test: a page opened by the browser itself (window.open()/target="_blank")
* must become visible through BrowserContext::pages() even when nothing armed a
* waitForPopup()/waitForEvent('page') listener before the triggering action ran.
*
* This is the scenario driver code built on top of Mink hits: it clicks a link and only
* afterwards inspects the set of open windows/tabs, with no opportunity to arm a listener
* around the specific click that happens to open one.
*/
#[CoversClass(BrowserContext::class)]
final class PassivePopupDiscoveryTest extends TestCase
{
use PlaywrightTestCaseTrait;

protected function setUp(): void
{
$this->setUpPlaywright();
}

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

$page->setContent(<<<HTML
<!DOCTYPE html>
<html>
<body>
<a id="open" href="about:blank" target="_blank">Open</a>
</body>
</html>
HTML);

self::assertCount(1, $context->pages());

// No waitForPopup()/waitForEvent('page') armed beforehand - this is the passive
// discovery path that used to depend on the (missing) server-side subscription.
$page->click('#open');

self::assertCount(2, $this->waitForPageCount($context, 2), 'New tab was not discovered passively after the click.');

$context->close();
}

/**
* @return array<\Playwright\Page\PageInterface>
*/
private function waitForPageCount(BrowserContextInterface $context, int $expectedCount, int $timeoutMs = 2000): array
{
$deadline = microtime(true) + $timeoutMs / 1000;
do {
$pages = $context->pages();
if (count($pages) >= $expectedCount) {
return $pages;
}
usleep(50000);
// Any transport round trip flushes buffered server-pushed events.
$context->cookies();
} while (microtime(true) < $deadline);

return $context->pages();
}
}
Loading