Skip to content

Commit 2a41e16

Browse files
authored
Enhance Playwright integration with new response handling and file upload features (#7)
1 parent 9961935 commit 2a41e16

10 files changed

Lines changed: 269 additions & 37 deletions

File tree

‎bin/playwright-server.js‎

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,16 @@ class PlaywrightServer {
153153
this.browsers = new Map();
154154
this.contexts = new Map();
155155
this.pages = new Map();
156+
this.pageContexts = new Map();
156157
this.responses = new Map();
157158
this.browserCounter = 0;
158159
this.contextCounter = 0;
159160
this.pageCounter = 0;
160161
this.responseCounter = 0;
161162
this.routes = new Map();
162163
this.routeCounter = 0;
164+
this.contextThrottling = new Map();
165+
this.dialogs = new Map();
163166
this.elementHandleCounter = 0;
164167
this.elementHandles = new Map();
165168
}
@@ -222,6 +225,9 @@ class PlaywrightServer {
222225
if (actionPrefix === 'route') {
223226
return await this.handleRouteCommand(command, actionMethod);
224227
}
228+
if (actionPrefix === 'response') {
229+
return await this.handleResponseCommand(command, actionMethod);
230+
}
225231
if (actionPrefix === 'mouse') {
226232
return await this.handleMouseCommand(command, actionMethod);
227233
}
@@ -289,10 +295,22 @@ class PlaywrightServer {
289295
case 'stopTracing':
290296
await context.tracing.stop({ path: command.path });
291297
return {success: true};
298+
case 'waitForEvent':
299+
await context.waitForEvent(command.event, { timeout: command.timeout });
300+
return {success: true};
301+
case 'setNetworkThrottling':
302+
if (command.throttling && typeof command.throttling === 'object') {
303+
this.contextThrottling.set(command.contextId, {
304+
latency: Number(command.throttling.latency) || 0,
305+
downloadThroughput: Number(command.throttling.downloadThroughput) || 0,
306+
uploadThroughput: Number(command.throttling.uploadThroughput) || 0,
307+
});
308+
}
309+
return {success: true};
292310
case 'route':
293311
await context.route(command.url, async (route) => {
294312
const routeId = `route_${++this.routeCounter}`;
295-
this.routes.set(routeId, route);
313+
this.routes.set(routeId, { route, contextId: command.contextId });
296314

297315
const req = route.request();
298316
let postData = null;
@@ -326,6 +344,62 @@ class PlaywrightServer {
326344
const page = await context.newPage(command.options);
327345
const pageId = `page_${++this.pageCounter}`;
328346
this.pages.set(pageId, page);
347+
this.pageContexts.set(pageId, command.contextId);
348+
349+
// Forward important page events
350+
page.on('console', (msg) => {
351+
const params = {
352+
type: msg.type(),
353+
text: msg.text(),
354+
args: [],
355+
location: msg.location ? msg.location() : {},
356+
};
357+
sendFramedResponse({ objectId: pageId, event: 'console', params });
358+
});
359+
360+
page.on('dialog', (dialog) => {
361+
const dialogId = `dialog_${Date.now()}_${Math.floor(Math.random()*1000)}`;
362+
this.dialogs.set(dialogId, dialog);
363+
const params = {
364+
dialogId,
365+
type: dialog.type(),
366+
message: dialog.message(),
367+
defaultValue: dialog.defaultValue ? dialog.defaultValue() : null,
368+
};
369+
sendFramedResponse({ objectId: pageId, event: 'dialog', params });
370+
});
371+
372+
page.on('request', (req) => {
373+
const params = {
374+
request: {
375+
url: req.url(),
376+
method: req.method(),
377+
headers: req.headers(),
378+
postData: (() => { try { return req.postData(); } catch { return null; } })(),
379+
resourceType: req.resourceType ? req.resourceType() : 'document',
380+
}
381+
};
382+
sendFramedResponse({ objectId: pageId, event: 'request', params });
383+
});
384+
385+
page.on('response', (res) => {
386+
const params = { response: this.serializeResponse(res) };
387+
sendFramedResponse({ objectId: pageId, event: 'response', params });
388+
});
389+
390+
page.on('requestfailed', (req) => {
391+
const params = {
392+
request: {
393+
url: req.url(),
394+
method: req.method(),
395+
headers: req.headers(),
396+
postData: (() => { try { return req.postData(); } catch { return null; } })(),
397+
resourceType: req.resourceType ? req.resourceType() : 'document',
398+
}
399+
};
400+
sendFramedResponse({ objectId: pageId, event: 'requestfailed', params });
401+
});
402+
329403
return {pageId: pageId};
330404
case 'cookies':
331405
return {cookies: await context.cookies(command.urls)};
@@ -439,10 +513,22 @@ class PlaywrightServer {
439513
case 'addStyleTag':
440514
await page.addStyleTag(command.options);
441515
return {success: true};
516+
case 'handleDialog':
517+
const dialog = this.dialogs.get(command.dialogId);
518+
if (dialog) {
519+
if (command.accept) {
520+
await dialog.accept(command.promptText);
521+
} else {
522+
await dialog.dismiss();
523+
}
524+
this.dialogs.delete(command.dialogId);
525+
}
526+
return {success: true};
442527
case 'route':
443528
await page.route(command.url, async (route) => {
444529
const routeId = `route_${++this.routeCounter}`;
445-
this.routes.set(routeId, route);
530+
const contextId = this.pageContexts.get(command.pageId);
531+
this.routes.set(routeId, { route, contextId });
446532

447533
const req = route.request();
448534
let postData = null;
@@ -547,6 +633,9 @@ class PlaywrightServer {
547633
case 'screenshot':
548634
const buffer = await locator.screenshot(command.options);
549635
return {binary: buffer.toString('base64')};
636+
case 'setInputFiles':
637+
await locator.setInputFiles(command.files, command.options);
638+
return;
550639
case 'evaluate':
551640
try {
552641
const count = await locator.count();
@@ -596,8 +685,9 @@ class PlaywrightServer {
596685
}
597686

598687
async handleRouteCommand(command, method) {
599-
const route = this.routes.get(command.routeId);
600-
if (!route) throw new Error(`Route not found: ${command.routeId}`);
688+
const info = this.routes.get(command.routeId);
689+
if (!info) throw new Error(`Route not found: ${command.routeId}`);
690+
const route = info.route || info;
601691

602692
switch (method) {
603693
case 'fulfill':
@@ -610,6 +700,13 @@ class PlaywrightServer {
610700
break;
611701
case 'continue':
612702
debugLogger.info('ROUTE CONTINUE', { routeId: command.routeId });
703+
try {
704+
const ctxId = info.contextId;
705+
const throttle = ctxId ? this.contextThrottling.get(ctxId) : null;
706+
if (throttle && throttle.latency && throttle.latency > 0) {
707+
await new Promise(r => setTimeout(r, throttle.latency));
708+
}
709+
} catch {}
613710
await route.continue(command.options || undefined);
614711
break;
615712
default:
@@ -618,6 +715,18 @@ class PlaywrightServer {
618715
this.routes.delete(command.routeId);
619716
}
620717

718+
async handleResponseCommand(command, method) {
719+
switch (method) {
720+
case 'body':
721+
const res = this.responses.get(command.responseId);
722+
if (!res) throw new Error(`Response not found: ${command.responseId}`);
723+
const buf = await res.body();
724+
return { binary: Buffer.from(buf).toString('base64') };
725+
default:
726+
throw new Error(`Unknown response action: ${method}`);
727+
}
728+
}
729+
621730
async handleMouseCommand(command, method) {
622731
const page = this.pages.get(command.pageId);
623732
if (!page) throw new Error(`Page not found: ${command.pageId}`);

‎src/Browser/BrowserType.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
enum BrowserType: string
1717
{
18-
case Chrome = 'chrome';
19-
case Firefox = 'firefox';
20-
case Safari = 'safari';
18+
case CHROMIUM = 'chromium';
19+
case FIREFOX = 'firefox';
20+
case WEBKIT = 'webkit';
2121
}

‎src/Configuration/BrowserType.php‎

Lines changed: 0 additions & 23 deletions
This file was deleted.

‎src/Configuration/PlaywrightConfig.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace PlaywrightPHP\Configuration;
1212

13+
use PlaywrightPHP\Browser\BrowserType;
1314
use Psr\Log\LoggerInterface;
1415

1516
/**
@@ -31,7 +32,7 @@ final class PlaywrightConfig
3132
*/
3233
public function __construct(
3334
public readonly ?string $nodePath = null,
34-
public readonly string $minNodeVersion = '18.0.0',
35+
public readonly string $minNodeVersion = '20.0.0',
3536

3637
public readonly BrowserType $browser = BrowserType::CHROMIUM,
3738
public readonly ?string $channel = null,

‎src/Configuration/PlaywrightConfigBuilder.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace PlaywrightPHP\Configuration;
1212

13+
use PlaywrightPHP\Browser\BrowserType;
1314
use Psr\Log\LoggerInterface;
1415

1516
/**
@@ -19,14 +20,14 @@
1920
* - Browser: chromium
2021
* - Headless: true
2122
* - Timeout: 30s
22-
* - Node min version: 18.0.0
23+
* - Node min version: 20.0.0
2324
*
2425
* @author Simon André <smn.andre@gmail.com>
2526
*/
2627
final class PlaywrightConfigBuilder
2728
{
2829
private ?string $nodePath = null;
29-
private string $minNodeVersion = '18.0.0';
30+
private string $minNodeVersion = '20.0.0';
3031

3132
private BrowserType $browser = BrowserType::CHROMIUM;
3233
private ?string $channel = null;

‎src/Node/NodeBinaryResolver.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*/
2424
final class NodeBinaryResolver implements NodeBinaryResolverInterface
2525
{
26-
private const string DEFAULT_MIN_VERSION = '18.0.0';
26+
private const string DEFAULT_MIN_VERSION = '20.0.0';
2727

2828
private ?string $cached = null;
2929

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the playwright-php/playwright package.
7+
* For the full copyright and license information, please view
8+
* the LICENSE file that was distributed with this source code.
9+
*/
10+
11+
namespace PlaywrightPHP\Tests\Integration\Input;
12+
13+
use PHPUnit\Framework\Attributes\CoversClass;
14+
use PHPUnit\Framework\Attributes\Test;
15+
use PHPUnit\Framework\TestCase;
16+
use PlaywrightPHP\Locator\Locator;
17+
use PlaywrightPHP\Page\Page;
18+
use PlaywrightPHP\Testing\PlaywrightTestCaseTrait;
19+
use PlaywrightPHP\Tests\Support\RouteServerTestTrait;
20+
21+
#[CoversClass(Page::class)]
22+
#[CoversClass(Locator::class)]
23+
final class FileUploadIntegrationTest extends TestCase
24+
{
25+
use PlaywrightTestCaseTrait;
26+
use RouteServerTestTrait;
27+
28+
private string $tempFile = '';
29+
30+
public function setUp(): void
31+
{
32+
$this->setUpPlaywright();
33+
34+
// Create a small temporary file to upload
35+
$this->tempFile = tempnam(sys_get_temp_dir(), 'pwphp_upload_') ?: sys_get_temp_dir().'/pwphp_upload.txt';
36+
file_put_contents($this->tempFile, 'upload-content');
37+
38+
$this->installRouteServer($this->page, [
39+
'/index.html' => <<<'HTML'
40+
<h1>Upload Test</h1>
41+
<input type="file" id="file" />
42+
<script>
43+
const input = document.getElementById('file');
44+
input.addEventListener('change', (e) => {
45+
const file = e.target.files && e.target.files[0];
46+
document.body.dataset.filename = file ? file.name : '';
47+
});
48+
</script>
49+
HTML,
50+
]);
51+
$this->page->goto($this->routeUrl('/index.html'));
52+
}
53+
54+
public function tearDown(): void
55+
{
56+
if ($this->tempFile && file_exists($this->tempFile)) {
57+
@unlink($this->tempFile);
58+
}
59+
$this->tearDownPlaywright();
60+
}
61+
62+
#[Test]
63+
public function itUploadsAFileWithPageHelper(): void
64+
{
65+
$this->page->setInputFiles('#file', [$this->tempFile]);
66+
usleep(100000);
67+
68+
$name = $this->page->evaluate('() => document.body.dataset.filename');
69+
$this->assertSame(basename($this->tempFile), $name);
70+
}
71+
72+
#[Test]
73+
public function itUploadsAFileWithLocator(): void
74+
{
75+
$this->page->locator('#file')->setInputFiles([$this->tempFile]);
76+
usleep(100000);
77+
78+
$name = $this->page->evaluate('() => document.body.dataset.filename');
79+
$this->assertSame(basename($this->tempFile), $name);
80+
}
81+
}

0 commit comments

Comments
 (0)