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
5 changes: 5 additions & 0 deletions bin/lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ class BaseHandler {
}
wrapResult(value) { return value === undefined || value === null ? { success: true } : value; }
createValueResult(value) { return { value }; }
storeHandle(handle) {
const handleId = this.generateId('element');
this.elementHandles.set(handleId, handle);
return { handleId };
}
async executeWithRegistry(registry, method, ...args) { return await registry.execute(method, ...args); }
async followNavigationRedirects(pageId, page, action) {
let nextAction = action;
Expand Down
96 changes: 91 additions & 5 deletions bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ const { logger, ErrorHandler, CommandRegistry, BaseHandler, PromiseUtils, FrameU
const { globalCoordinator } = require('./coordination');
const { PopupCoordinator } = require('./popup-coordinator');

// The two callbacks below never run in this Node process: Playwright ships
// their source to the browser, so their eval() has page scope only, exactly
// like the callbacks passed to page.evaluate() elsewhere in this file.
//
// They are needed because evaluateHandle() does not accept a function as a
// string. Playwright evaluates a string pageFunction as a plain expression and
// never calls it, so forwarding "() => value" would hand back a handle to the
// function itself rather than to its result.
const evaluateHandleInPage = async ({ expression, arg }) => {
const value = eval(`(${expression})`);
return typeof value === 'function' ? await value(arg) : await value;
};

const evaluateHandleOnTarget = async (target, { expression, arg }) => {
const value = eval(`(${expression})`);
return typeof value === 'function' ? await value(target, arg) : await value;
};

class ContextHandler extends BaseHandler {
async handle(command, method) {
const context = this.validateResource(this.contexts, command.contextId, 'Context')?.context;
Expand Down Expand Up @@ -403,10 +421,10 @@ class PageHandler extends BaseHandler {
}

async evaluateHandle(page, command) {
const handle = await page.evaluateHandle(command.expression, command.arg);
const handleId = this.generateId('element');
this.elementHandles.set(handleId, handle);
return { elementHandleId: handleId };
return this.storeHandle(await page.evaluateHandle(evaluateHandleInPage, {
expression: command.expression,
arg: command.arg,
}));
}

async handleDialog(command) {
Expand Down Expand Up @@ -595,6 +613,8 @@ class LocatorHandler extends BaseHandler {
selectOption: () => PromiseUtils.wrapValues(locator.selectOption(command.values, command.options)),
screenshot: () => PromiseUtils.wrapBinary(locator.screenshot(command.options)),
evaluate: () => this.evaluateLocator(locator, command),
evaluateHandle: () => this.evaluateHandle(locator, command),
waitForFunction: () => this.waitForFunction(locator, command),
dragAndDrop: () => this.handleDragAndDrop(page, command)
});

Expand Down Expand Up @@ -657,6 +677,20 @@ class LocatorHandler extends BaseHandler {
}
}

async evaluateHandle(locator, command) {
return this.storeHandle(await locator.evaluateHandle(evaluateHandleOnTarget, {
expression: command.expression,
arg: command.arg,
}));
}

async waitForFunction(locator, command) {
// Forwarded as a string: Playwright evaluates it in the page. Turning it
// into a function here would run it in this Node process instead.
await locator.waitForFunction(command.pageFunction, command.arg, command.options);
return { success: true };
}

async handleDragAndDrop(page, command) {
logger.debug('Handling drag and drop', {
selector: command.selector,
Expand Down Expand Up @@ -738,6 +772,8 @@ class FrameHandler extends BaseHandler {
addStyleTag: async () => { await (await nativeFrame()).addStyleTag(command.options); return { success: true }; },
name: () => evalInFrame(() => window.name || '').then(v => this.createValueResult(v ?? '')),
evaluate: () => FrameUtils.evaluateInFrame(page, frameLocator, isMainFrame, command.expression, command.arg).then(result => ({ result })),
evaluateHandle: () => this.evaluateHandle(nativeFrame, command),
frameElement: () => this.frameElement(nativeFrame),
title: () => evalInFrame(() => document.title).then(v => this.createValueResult(v ?? '')),
url: () => evalInFrame(() => document.location.href).then(v => this.createValueResult(v ?? '')),
isDetached: () => this.checkDetached(isMainFrame, frameLocator),
Expand All @@ -756,6 +792,19 @@ class FrameHandler extends BaseHandler {
return { success: true };
}

async evaluateHandle(nativeFrame, command) {
const frame = await nativeFrame();
return this.storeHandle(await frame.evaluateHandle(evaluateHandleInPage, {
expression: command.expression,
arg: command.arg,
}));
}

async frameElement(nativeFrame) {
const frame = await nativeFrame();
return this.storeHandle(await frame.frameElement());
}

async checkDetached(isMainFrame, frameLocator) {
if (isMainFrame) return this.createValueResult(false);
const count = await frameLocator.locator('html').count();
Expand Down Expand Up @@ -800,6 +849,43 @@ class FrameHandler extends BaseHandler {
}
}

class JSHandleHandler extends BaseHandler {
async handle(command, method) {
const handle = this.validateResource(this.elementHandles, command.handleId, 'JSHandle');

const registry = CommandRegistry.create({
asElement: () => this.asElement(handle),
dispose: () => this.dispose(handle, command.handleId),
evaluate: async () => ({ result: await handle.evaluate(evaluateHandleOnTarget, { expression: command.expression, arg: command.arg }) }),
evaluateHandle: async () => this.storeHandle(await handle.evaluateHandle(evaluateHandleOnTarget, { expression: command.expression, arg: command.arg })),
getProperties: () => this.getProperties(handle),
getProperty: async () => this.storeHandle(await handle.getProperty(command.propertyName)),
jsonValue: () => PromiseUtils.wrapValue(handle.jsonValue())
});

return await this.executeWithRegistry(registry, method);
}

async dispose(handle, handleId) {
await handle.dispose();
this.elementHandles.delete(handleId);
return { success: true };
}

asElement(handle) {
const element = handle.asElement();
return element ? this.storeHandle(element) : { handleId: null };
}

async getProperties(handle) {
const properties = {};
for (const [name, value] of (await handle.getProperties()).entries()) {
properties[name] = this.storeHandle(value).handleId;
}
return { properties };
}
}

class SelectorsHandler extends BaseHandler {
async handle(command, method) {
const { playwright } = require('playwright');
Expand All @@ -822,4 +908,4 @@ class SelectorsHandler extends BaseHandler {
}
}

module.exports = { ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, SelectorsHandler };
module.exports = { ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, JSHandleHandler, SelectorsHandler };
4 changes: 3 additions & 1 deletion bin/playwright-server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const {chromium, firefox, webkit} = require('playwright');
const { logger, ErrorHandler, LspFraming, sendFramedResponse, CommandRegistry, BaseHandler } = require('./lib/core');
const { ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, SelectorsHandler } = require('./lib/handlers');
const { ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, JSHandleHandler, SelectorsHandler } = require('./lib/handlers');
const { globalCoordinator } = require('./lib/coordination');

class PlaywrightServer extends BaseHandler {
Expand Down Expand Up @@ -42,6 +42,7 @@ class PlaywrightServer extends BaseHandler {
this.locatorHandler = new LocatorHandler(deps);
this.interactionHandler = new InteractionHandler(deps);
this.frameHandler = new FrameHandler(deps);
this.jsHandleHandler = new JSHandleHandler(deps);
this.selectorsHandler = new SelectorsHandler(deps);
}

Expand Down Expand Up @@ -75,6 +76,7 @@ class PlaywrightServer extends BaseHandler {
keyboard: () => this.interactionHandler.handleKeyboard(command, actionMethod),
touchscreen: () => this.interactionHandler.handleTouchscreen(command, actionMethod),
frame: () => this.frameHandler.handle(command, actionMethod),
jsHandle: () => this.jsHandleHandler.handle(command, actionMethod),
browserServer: () => this.handleBrowserServer(command, actionMethod),
selectors: () => this.selectorsHandler.handle(command, actionMethod),
clock: () => this.contextHandler.handleClock(command, actionMethod),
Expand Down
56 changes: 50 additions & 6 deletions src/Frame/Frame.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

use Playwright\Exception\PlaywrightException;
use Playwright\Exception\ProtocolErrorException;
use Playwright\Exception\RuntimeException;
use Playwright\JSHandle\JSHandle;
use Playwright\JSHandle\JSHandleInterface;
use Playwright\Locator\Locator;
use Playwright\Locator\LocatorInterface;
use Playwright\Locator\RoleSelectorBuilder;
Expand All @@ -29,6 +32,7 @@
use Playwright\Page\Options\WaitForFunctionOptions;
use Playwright\Page\Options\WaitForNavigationOptions;
use Playwright\Page\Options\WaitForUrlOptions;
use Playwright\Page\PageInterface;
use Playwright\Transport\TransportInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand All @@ -42,6 +46,7 @@ public function __construct(
private readonly string $pageId,
private readonly string $frameSelector,
?LoggerInterface $logger = null,
private readonly ?PageInterface $page = null,
) {
$this->logger = $logger ?? new NullLogger();
}
Expand All @@ -58,7 +63,7 @@ public function locator(string $selector): LocatorInterface
'selector' => $selector,
]);

return new Locator($this->transport, $this->pageId, $selector, $this->frameSelector, $this->logger);
return new Locator($this->transport, $this->pageId, $selector, $this->frameSelector, $this->logger, [], $this->page);
}

/**
Expand Down Expand Up @@ -99,7 +104,7 @@ public function getByRole(string $role, array $options = []): LocatorInterface
'selector' => $selector,
]);

return new Locator($this->transport, $this->pageId, $selector, $this->frameSelector, $this->logger, $locatorOptions);
return new Locator($this->transport, $this->pageId, $selector, $this->frameSelector, $this->logger, $locatorOptions, $this->page);
}

public function getByTestId(string $testId): LocatorInterface
Expand Down Expand Up @@ -133,7 +138,7 @@ public function frameLocator(string $selector): FrameLocatorInterface
'newSelector' => $newSelector,
]);

return new FrameLocator($this->transport, $this->pageId, $newSelector, $this->logger);
return new FrameLocator($this->transport, $this->pageId, $newSelector, $this->logger, $this->page);
}

public function owner(): LocatorInterface
Expand All @@ -142,7 +147,7 @@ public function owner(): LocatorInterface
'frameSelector' => $this->frameSelector,
]);

return new Locator($this->transport, $this->pageId, $this->frameSelector, null, $this->logger);
return new Locator($this->transport, $this->pageId, $this->frameSelector, null, $this->logger, [], $this->page);
}

public function content(): string
Expand Down Expand Up @@ -259,6 +264,32 @@ public function evaluate(string $expression, mixed $arg = null): mixed
return $response['result'] ?? null;
}

public function evaluateHandle(string $expression, mixed $arg = null): JSHandleInterface
{
$response = $this->sendCommand('frame.evaluateHandle', [
'expression' => self::normalizeForPage($expression),
'arg' => $arg,
]);

return $this->createHandle($response, 'frame.evaluateHandle');
}

public function frameElement(): JSHandleInterface
{
$response = $this->sendCommand('frame.frameElement');

return $this->createHandle($response, 'frame.frameElement');
}

public function page(): PageInterface
{
if (null === $this->page) {
throw new RuntimeException('This frame was not created from a page.');
}

return $this->page;
}

public function name(): string
{
$response = $this->sendCommand('frame.name');
Expand Down Expand Up @@ -315,7 +346,7 @@ public function parentFrame(): ?FrameInterface
$selector = $response['selector'] ?? null;

return is_string($selector)
? new Frame($this->transport, $this->pageId, $selector, $this->logger)
? new Frame($this->transport, $this->pageId, $selector, $this->logger, $this->page)
: null;
}

Expand All @@ -333,7 +364,7 @@ public function childFrames(): array
$result = [];
foreach ($frames as $frameData) {
if (is_array($frameData) && isset($frameData['selector']) && is_string($frameData['selector'])) {
$result[] = new Frame($this->transport, $this->pageId, $frameData['selector'], $this->logger);
$result[] = new Frame($this->transport, $this->pageId, $frameData['selector'], $this->logger, $this->page);
}
}

Expand Down Expand Up @@ -364,6 +395,19 @@ private function sendCommand(string $action, array $params = []): array
return $response;
}

/**
* @param array<string, mixed> $response
*/
private function createHandle(array $response, string $action): JSHandleInterface
{
$handleId = $response['handleId'] ?? null;
if (!is_string($handleId)) {
throw new ProtocolErrorException(\sprintf('Invalid %s response', $action), 0);
}

return new JSHandle($this->transport, $handleId);
}

private function createResponse(mixed $data): ?ResponseInterface
{
if (null === $data) {
Expand Down
28 changes: 28 additions & 0 deletions src/Frame/FrameInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace Playwright\Frame;

use Playwright\JSHandle\JSHandleInterface;
use Playwright\Locator\LocatorInterface;
use Playwright\Network\ResponseInterface;
use Playwright\Page\Options\DragAndDropOptions;
Expand All @@ -24,6 +25,7 @@
use Playwright\Page\Options\WaitForFunctionOptions;
use Playwright\Page\Options\WaitForNavigationOptions;
use Playwright\Page\Options\WaitForUrlOptions;
use Playwright\Page\PageInterface;

interface FrameInterface
{
Expand Down Expand Up @@ -129,6 +131,32 @@ public function addStyleTag(array|StyleTagOptions $options = []): self;
*/
public function evaluate(string $expression, mixed $arg = null): mixed;

/**
* Returns a handle to the result instead of a serialized copy, so a DOM node
* or a live object survives the round trip.
*
* Dispose the handle once done with it, otherwise it pins its target until
* the page closes.
*/
public function evaluateHandle(string $expression, mixed $arg = null): JSHandleInterface;

/**
* A handle to the <iframe> element embedding this frame, resolved from the
* parent document.
*
* The main frame has no embedding element and reports an error instead.
*/
public function frameElement(): JSHandleInterface;

/**
* The page this frame belongs to, main frame and nested frames alike.
*
* @throws \Playwright\Exception\RuntimeException if the frame was built
* without a page, which only
* happens when constructed by hand
*/
public function page(): PageInterface;

/**
* The frame's name attribute.
*/
Expand Down
Loading
Loading