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
8 changes: 8 additions & 0 deletions bin/lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,14 @@ class RouteUtils {
});
return { success: true };
}

// unrouteAll drops the handlers inside Playwright, but the pending Route
// objects we handed out stay in the map and can never be answered again.
static forgetRoutes(routes, ownerId) {
for (const [routeId, info] of routes.entries()) {
if (info?.contextId === ownerId) routes.delete(routeId);
}
}
}

const logger = new Logger();
Expand Down
61 changes: 61 additions & 0 deletions bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ContextHandler extends BaseHandler {
setDefaultNavigationTimeout: () => context.setDefaultNavigationTimeout(command.timeout),
route: () => RouteUtils.setupRoute(context, command.contextId, command.url, this.generateId, this.routes, this.extractRequestData, this.sendFramedResponse),
unroute: () => context.unroute(command.url),
unrouteAll: () => this.unrouteAll(context, command.contextId, command.options),
cookies: async () => ({ cookies: await context.cookies(command.urls) }),
storageState: async () => ({ storageState: await context.storageState(command.options) }),
setStorageState: () => context.setStorageState(command.storageState),
Expand Down Expand Up @@ -110,6 +111,11 @@ class ContextHandler extends BaseHandler {
return this.wrapResult(result);
}

async unrouteAll(context, contextId, options) {
await context.unrouteAll(options || {});
RouteUtils.forgetRoutes(this.routes, contextId);
}

setThrottling(command) {
if (command.throttling && typeof command.throttling === 'object') {
this.contextThrottling.set(command.contextId, {
Expand Down Expand Up @@ -237,7 +243,15 @@ class PageHandler extends BaseHandler {
goto: () => this.goto(page, command),
evaluate: () => this.evaluate(page, command),
addInitScript: () => page.addInitScript(command.script),
emulateMedia: () => page.emulateMedia(command.options || {}),
requestGC: () => page.requestGC(),
waitForResponse: () => this.waitForResponse(page, command),
waitForRequest: () => this.waitForRequest(page, command),
requests: () => this.getRequests(page),
opener: () => this.getOpener(page, command),
consoleMessages: () => this.consoleMessages(page, command),
clearConsoleMessages: () => page.clearConsoleMessages(),
clearPageErrors: () => page.clearPageErrors(),
content: async () => ({ content: await page.content() }),
setContent: () => page.setContent(command.html, command.options),
querySelector: () => this.querySelector(page, command),
Expand All @@ -260,6 +274,7 @@ class PageHandler extends BaseHandler {
handleDialog: () => this.handleDialog(command),
route: () => RouteUtils.setupRoute(page, command.pageId, command.url, this.generateId, this.routes, this.extractRequestData, this.sendFramedResponse, () => `route_${++this.routeCounter.value}`),
unroute: () => page.unroute(command.url),
unrouteAll: () => this.unrouteAll(page, command.pageId, command.options),
goBack: () => this.followNavigationRedirects(command.pageId, page, () => page.goBack(command.options)),
goForward: () => this.followNavigationRedirects(command.pageId, page, () => page.goForward(command.options)),
reload: () => this.followNavigationRedirects(command.pageId, page, () => page.reload(command.options)),
Expand Down Expand Up @@ -333,6 +348,52 @@ class PageHandler extends BaseHandler {
return { response: this.serializeResponse(response) };
}

async waitForRequest(page, command) {
const jsAction = command.jsAction;
const [request] = await Promise.all([
page.waitForRequest(command.url, command.options),
jsAction ? page.evaluate(jsAction) : Promise.resolve(),
]);
return { request: this.extractRequestData(request) };
}

async getRequests(page) {
const requests = await page.requests();
return { requests: requests.map(request => this.extractRequestData(request)) };
}

async getOpener(page, command) {
const opener = await page.opener();
if (!opener) return { pageId: null };

for (const [pageId, candidate] of this.pages.entries()) {
if (candidate === opener) return { pageId };
}

// The opener predates this server's page map only when nothing ever
// surfaced it to PHP. Adopt it now rather than report "no opener".
const openerPageId = this.generateId('page');
this.pages.set(openerPageId, opener);
const contextId = this.pageContexts.get(command.pageId);
if (contextId) this.pageContexts.set(openerPageId, contextId);
if (this.setupPageEventListeners) this.setupPageEventListeners(opener, openerPageId);

return { pageId: openerPageId };
}

async consoleMessages(page, command) {
// An empty PHP options bag arrives as [], whose .filter is Array#filter.
// Forwarding that reaches Playwright as a function and fails validation.
const filter = command.options?.filter;
const messages = await page.consoleMessages(typeof filter === 'string' ? { filter } : {});
return { messages: messages.map(message => this.serializeConsoleMessage(message)) };
}

async unrouteAll(page, pageId, options) {
await page.unrouteAll(options || {});
RouteUtils.forgetRoutes(this.routes, pageId);
}

async querySelector(page, command) {
const element = await page.$(command.selector);
if (!element) return { elementHandleId: null };
Expand Down
21 changes: 14 additions & 7 deletions bin/playwright-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class PlaywrightServer extends BaseHandler {
responses: this.responses, routes: this.routes, generateId: this.generateId.bind(this),
navigationRedirects: this.navigationRedirects,
extractRequestData: this.extractRequestData.bind(this), serializeResponse: this.serializeResponse.bind(this),
serializeConsoleMessage: this.serializeConsoleMessage.bind(this),
sendFramedResponse,
routeCounter: { value: this.counters.route },
setupPageEventListeners: this.setupPageEventListeners.bind(this)
Expand Down Expand Up @@ -168,13 +169,7 @@ class PlaywrightServer extends BaseHandler {

formatEventParams(eventName, eventData) {
const formatters = {
console: () => ({
type: eventData.type(),
text: eventData.text(),
args: [],
location: eventData.location ? eventData.location() : {},
timestamp: eventData.timestamp ? eventData.timestamp() : null,
}),
console: () => this.serializeConsoleMessage(eventData),
dialog: () => {
const dialogId = this.generateId('dialog');
this.dialogs.set(dialogId, eventData);
Expand All @@ -187,6 +182,18 @@ class PlaywrightServer extends BaseHandler {
return formatters[eventName] ? formatters[eventName]() : {};
}

serializeConsoleMessage(message) {
return {
type: message.type(),
text: message.text(),
// JSHandle instances cannot cross the JSON-RPC boundary; the PHP
// ConsoleMessage contract expects the key, so ship an empty snapshot.
args: [],
location: message.location ? message.location() : {},
timestamp: message.timestamp ? message.timestamp() : null,
};
}

generateId(prefix) {
return prefix === 'dialog' ? `dialog_${Date.now()}_${Math.floor(Math.random() * 1000)}` : `${prefix}_${++this.counters[prefix]}`;
}
Expand Down
13 changes: 13 additions & 0 deletions src/Browser/BrowserContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,19 @@ public function unroute(string $url, ?callable $handler = null): void
]);
}

/**
* @param array{behavior?: 'default'|'wait'|'ignoreErrors'} $options
*/
public function unrouteAll(array $options = []): void
{
$this->routeHandlers = [];
$this->transport->send([
'action' => 'context.unrouteAll',
'contextId' => $this->contextId,
'options' => $options,
]);
}

public function getEnv(string $name): ?string
{
$response = $this->transport->send([
Expand Down
10 changes: 10 additions & 0 deletions src/Browser/BrowserContextInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ public function route(string $url, callable $handler): void;

public function unroute(string $url, ?callable $handler = null): void;

/**
* Removes every route registered on this context in one call.
*
* 'behavior' decides what happens to handlers still running: wait for them,
* wait but swallow their errors, or return without waiting.
*
* @param array{behavior?: 'default'|'wait'|'ignoreErrors'} $options
*/
public function unrouteAll(array $options = []): void;

public function getEnv(string $name): ?string;

/**
Expand Down
85 changes: 85 additions & 0 deletions src/Page/Options/EmulateMediaOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?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\Page\Options;

final readonly class EmulateMediaOptions
{
/**
* An omitted feature keeps its current emulation. Pass 'no-override' to
* drop the emulation and fall back to the browser default.
*
* @param 'light'|'dark'|'no-preference'|'no-override'|null $colorScheme
* @param 'no-preference'|'more'|'no-override'|null $contrast
* @param 'active'|'none'|'no-override'|null $forcedColors
* @param 'screen'|'print'|'no-override'|null $media
* @param 'reduce'|'no-preference'|'no-override'|null $reducedMotion
*/
public function __construct(
public ?string $colorScheme = null,
public ?string $contrast = null,
public ?string $forcedColors = null,
public ?string $media = null,
public ?string $reducedMotion = null,
) {
}

/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$options = [];
if (null !== $this->colorScheme) {
$options['colorScheme'] = $this->colorScheme;
}
if (null !== $this->contrast) {
$options['contrast'] = $this->contrast;
}
if (null !== $this->forcedColors) {
$options['forcedColors'] = $this->forcedColors;
}
if (null !== $this->media) {
$options['media'] = $this->media;
}
if (null !== $this->reducedMotion) {
$options['reducedMotion'] = $this->reducedMotion;
}

return $options;
}

/**
* @param array<string, mixed>|self $options
*/
public static function from(array|self $options = []): self
{
if ($options instanceof self) {
return $options;
}

/** @var 'light'|'dark'|'no-preference'|'no-override'|null $colorScheme */
$colorScheme = $options['colorScheme'] ?? null;
/** @var 'no-preference'|'more'|'no-override'|null $contrast */
$contrast = $options['contrast'] ?? null;
/** @var 'active'|'none'|'no-override'|null $forcedColors */
$forcedColors = $options['forcedColors'] ?? null;
/** @var 'screen'|'print'|'no-override'|null $media */
$media = $options['media'] ?? null;
/** @var 'reduce'|'no-preference'|'no-override'|null $reducedMotion */
$reducedMotion = $options['reducedMotion'] ?? null;

return new self($colorScheme, $contrast, $forcedColors, $media, $reducedMotion);
}
}
51 changes: 51 additions & 0 deletions src/Page/Options/WaitForRequestOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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\Page\Options;

final readonly class WaitForRequestOptions
{
public function __construct(
public ?float $timeout = null,
) {
}

/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$options = [];
if (null !== $this->timeout) {
$options['timeout'] = $this->timeout;
}

return $options;
}

/**
* @param array<string, mixed>|self $options
*/
public static function from(array|self $options = []): self
{
if ($options instanceof self) {
return $options;
}

/** @var float|null $timeout */
$timeout = $options['timeout'] ?? null;

return new self($timeout);
}
}
Loading
Loading