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
56 changes: 55 additions & 1 deletion bin/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ class PageHandler extends BaseHandler {
frame: () => this.getFrame(page, command),
waitForPopup: () => this.waitForPopup(page, command),
bringToFront: () => page.bringToFront(),
ariaSnapshot: () => PromiseUtils.wrapValue(page.ariaSnapshot(command.options)),
hideHighlight: () => page.hideHighlight(),
video: () => this.getVideo(page),
});

return await ErrorHandler.safeExecute(() => this.executeWithRegistry(registry, method), { method, pageId: command.pageId });
Expand All @@ -325,6 +328,16 @@ class PageHandler extends BaseHandler {
if (page) { await page.close(); this.pages.delete(pageId); }
}

async getVideo(page) {
const video = page.video();
if (!video) return { video: null };
const videoId = this.generateId('video');
this.videos.set(videoId, video);
// path() resolves right away with the target file, which Playwright only
// fills in once the page closes.
return { video: { videoId, path: await video.path() } };
}

async goto(page, command) {
const gotoResponse = await this.followNavigationRedirects(
command.pageId,
Expand Down Expand Up @@ -646,6 +659,9 @@ class LocatorHandler extends BaseHandler {
dispatchEvent: () => locator.dispatchEvent(command.type, command.eventInit, command.options),
evaluateAll: () => this.evaluateAll(locator, command),
highlight: () => locator.highlight(),
hideHighlight: () => locator.hideHighlight(),
drop: () => locator.drop(this.decodeDropPayload(command.payload), command.options),
normalize: () => this.normalize(locator),
selectText: () => locator.selectText(command.options),
setChecked: () => locator.setChecked(command.checked, command.options),
tap: () => locator.tap(command.options),
Expand Down Expand Up @@ -681,6 +697,26 @@ class LocatorHandler extends BaseHandler {
return page.locator(command.selector);
}

// File buffers arrive base64 encoded because the PHP transport is JSON.
decodeDropPayload(payload) {
const source = payload || {};
const decoded = {};
if (source.data) decoded.data = source.data;
if (source.files) {
decoded.files = source.files.map(file => typeof file === 'string'
? file
: { name: file.name, mimeType: file.mimeType, buffer: Buffer.from(file.buffer, 'base64') });
}
return decoded;
}

// The resolved selector is the only part of the normalized locator PHP can use,
// and Playwright exposes it nowhere else.
async normalize(locator) {
const normalized = await locator.normalize();
return this.createValueResult(normalized._selector);
}

async evaluateLocator(locator, command) {
try {
const count = await locator.count();
Expand Down Expand Up @@ -960,4 +996,22 @@ class SelectorsHandler extends BaseHandler {
}
}

module.exports = { ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, JSHandleHandler, SelectorsHandler };
class VideoHandler extends BaseHandler {
async handle(command, method) {
const video = this.validateResource(this.videos, command.videoId, 'Video');

const registry = CommandRegistry.create({
// Both calls wait for the recording to be flushed, which happens on page close.
saveAs: () => video.saveAs(command.path),
delete: async () => {
await video.delete();
this.videos.delete(command.videoId);
}
});

const result = await ErrorHandler.safeExecute(() => this.executeWithRegistry(registry, method), { method, videoId: command.videoId });
return this.wrapResult(result);
}
}

module.exports = { ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, JSHandleHandler, SelectorsHandler, VideoHandler };
9 changes: 6 additions & 3 deletions 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, JSHandleHandler, SelectorsHandler } = require('./lib/handlers');
const { ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, JSHandleHandler, SelectorsHandler, VideoHandler } = require('./lib/handlers');
const { globalCoordinator } = require('./lib/coordination');

class PlaywrightServer extends BaseHandler {
Expand All @@ -22,14 +22,15 @@ class PlaywrightServer extends BaseHandler {
this.contextThrottling = new Map();
this.navigationRedirects = new Map();
this.servers = new Map();
this.counters = { browser: 0, context: 0, page: 0, response: 0, route: 0, element: 0, server: 0 };
this.videos = new Map();
this.counters = { browser: 0, context: 0, page: 0, response: 0, route: 0, element: 0, server: 0, video: 0 };
}

initHandlers() {
const deps = {
contexts: this.contexts, contextThrottling: this.contextThrottling, pages: this.pages,
pageContexts: this.pageContexts, dialogs: this.dialogs, elementHandles: this.elementHandles,
responses: this.responses, routes: this.routes, generateId: this.generateId.bind(this),
responses: this.responses, routes: this.routes, videos: this.videos, generateId: this.generateId.bind(this),
navigationRedirects: this.navigationRedirects,
extractRequestData: this.extractRequestData.bind(this), serializeResponse: this.serializeResponse.bind(this),
serializeConsoleMessage: this.serializeConsoleMessage.bind(this),
Expand All @@ -44,6 +45,7 @@ class PlaywrightServer extends BaseHandler {
this.frameHandler = new FrameHandler(deps);
this.jsHandleHandler = new JSHandleHandler(deps);
this.selectorsHandler = new SelectorsHandler(deps);
this.videoHandler = new VideoHandler(deps);
}

async handleCommand(command) {
Expand Down Expand Up @@ -83,6 +85,7 @@ class PlaywrightServer extends BaseHandler {
webStorage: () => this.pageHandler.handleWebStorage(command, actionMethod),
credentials: () => this.contextHandler.handleCredentials(command, actionMethod),
screencast: () => this.pageHandler.handleScreencast(command, actionMethod),
video: () => this.videoHandler.handle(command, actionMethod),
// Tracing actions are flat names (no dot), sent by the PHP Tracing class
tracingStart: () => this.contextHandler.handleTracing(command, 'start'),
tracingStartChunk: () => this.contextHandler.handleTracing(command, 'startChunk'),
Expand Down
33 changes: 33 additions & 0 deletions src/Locator/Locator.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
use Playwright\Locator\Options\DblClickOptions;
use Playwright\Locator\Options\DispatchEventOptions;
use Playwright\Locator\Options\DragToOptions;
use Playwright\Locator\Options\DropOptions;
use Playwright\Locator\Options\DropPayload;
use Playwright\Locator\Options\FillOptions;
use Playwright\Locator\Options\FilterOptions;
use Playwright\Locator\Options\GetAttributeOptions;
Expand Down Expand Up @@ -584,6 +586,21 @@ public function dragTo(LocatorInterface $target, array|DragToOptions $options =
$this->transport->processEvents();
}

/**
* @param array<string, mixed>|DropPayload $payload
* @param array<string, mixed>|DropOptions $options
*/
public function drop(array|DropPayload $payload, array|DropOptions $options = []): void
{
$payload = DropPayload::from($payload);
$options = DropOptions::from($options);

$this->sendCommand('locator.drop', [
'payload' => $payload->toArray(),
'options' => $options->toArray(),
]);
}

/**
* @param array<string, mixed>|TextContentOptions $options
*/
Expand Down Expand Up @@ -708,6 +725,11 @@ public function highlight(): void
$this->sendCommand('locator.highlight');
}

public function hideHighlight(): void
{
$this->sendCommand('locator.hideHighlight');
}

/**
* @param array<string, mixed>|SelectTextOptions $options
*/
Expand Down Expand Up @@ -965,6 +987,17 @@ public function describe(string $description): self
return $this;
}

public function normalize(): self
{
$response = $this->sendCommand('locator.normalize');
$value = $response['value'] ?? null;
if (!is_string($value)) {
throw new ProtocolErrorException('Invalid normalize response', 0);
}

return new self($this->transport, $this->pageId, $value, $this->frameSelector, $this->logger);
}

public function contentFrame(): FrameLocatorInterface
{
return new FrameLocator($this->transport, $this->pageId, $this->selectorChain->toString(), $this->logger, $this->page);
Expand Down
26 changes: 26 additions & 0 deletions src/Locator/LocatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
use Playwright\Locator\Options\DblClickOptions;
use Playwright\Locator\Options\DispatchEventOptions;
use Playwright\Locator\Options\DragToOptions;
use Playwright\Locator\Options\DropOptions;
use Playwright\Locator\Options\DropPayload;
use Playwright\Locator\Options\FillOptions;
use Playwright\Locator\Options\FilterOptions;
use Playwright\Locator\Options\GetAttributeOptions;
Expand Down Expand Up @@ -96,6 +98,17 @@ public function hover(array|HoverOptions $options = []): void;
*/
public function dragTo(LocatorInterface $target, array|DragToOptions $options = []): void;

/**
* Drop files or clipboard-like data onto this element, as an external application would.
*
* The target must accept the drop by calling preventDefault() in its dragover
* handler, otherwise the drop is rejected and this method throws.
*
* @param array<string, mixed>|DropPayload $payload
* @param array<string, mixed>|DropOptions $options
*/
public function drop(array|DropPayload $payload, array|DropOptions $options = []): void;

/**
* @param array<string, mixed>|DblClickOptions $options
*/
Expand Down Expand Up @@ -288,6 +301,11 @@ public function evaluateAll(string $expression, mixed $arg = null): mixed;
*/
public function highlight(): void;

/**
* Removes the highlight painted by highlight().
*/
public function hideHighlight(): void;

/**
* @param array<string, mixed>|SelectTextOptions $options
*/
Expand Down Expand Up @@ -318,5 +336,13 @@ public function or(LocatorInterface $locator): self;

public function describe(string $description): self;

/**
* Resolves the current match into a locator built from test ids and aria roles.
*
* Turns an implementation-detail selector into a user-facing one, so it needs a
* live match and hits the browser.
*/
public function normalize(): self;

public function contentFrame(): FrameLocatorInterface;
}
60 changes: 60 additions & 0 deletions src/Locator/Options/DropOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?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\Locator\Options;

final readonly class DropOptions
{
/**
* @param array{x: float, y: float}|null $position
*/
public function __construct(
public ?array $position = null,
public ?float $timeout = null,
) {
}

/**
* @return array<string, mixed>
*/
public function toArray(): array
{
$options = [];
if (null !== $this->position) {
$options['position'] = $this->position;
}
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 array{x: float, y: float}|null $position */
$position = $options['position'] ?? null;
/** @var float|null $timeout */
$timeout = $options['timeout'] ?? null;

return new self($position, $timeout);
}
}
81 changes: 81 additions & 0 deletions src/Locator/Options/DropPayload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?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\Locator\Options;

/**
* What a drop carries: files and/or clipboard-like entries keyed by mime type.
*/
final readonly class DropPayload
{
/**
* A file entry is either a path on disk or an in-memory file whose buffer holds
* raw bytes. Data entries are keyed by mime type, such as text/plain.
*
* @param list<string|array{name: string, mimeType: string, buffer: string}> $files
* @param array<string, string> $data
*/
public function __construct(
public array $files = [],
public array $data = [],
) {
}

/**
* Buffers travel base64 encoded because the wire format is JSON.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
$payload = [];
if ([] !== $this->files) {
$payload['files'] = array_map(
/**
* @param string|array{name: string, mimeType: string, buffer: string} $file
*
* @return string|array{name: string, mimeType: string, buffer: string}
*/
static fn (string|array $file): string|array => is_string($file) ? $file : [
'name' => $file['name'],
'mimeType' => $file['mimeType'],
'buffer' => base64_encode($file['buffer']),
],
$this->files,
);
}
if ([] !== $this->data) {
$payload['data'] = $this->data;
}

return $payload;
}

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

/** @var string|list<string|array{name: string, mimeType: string, buffer: string}> $files */
$files = $payload['files'] ?? [];
/** @var array<string, string> $data */
$data = $payload['data'] ?? [];

return new self(is_string($files) ? [$files] : $files, $data);
}
}
Loading
Loading