Skip to content

Commit 9961935

Browse files
authored
Add route interception and context-level request handling (#6)
1 parent 896e0cf commit 9961935

File tree

7 files changed

+493
-54
lines changed

7 files changed

+493
-54
lines changed

README.md

Lines changed: 128 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,44 @@
1010

1111
# Playwright for PHP
1212

13-
Modern, PHP-native browser automation on top of Microsoft Playwright.
13+
Modern, PHPnative browser automation powered by Microsoft Playwright.
1414

15-
## Highlights
15+
## About
1616

17-
- Powerful: Drive Chromium, Firefox, and WebKit with one API
18-
- Reliable: Auto-waits and locator model reduce flakiness
19-
- Expressive: Jest-style `expect()` assertions for pages and locators
20-
- Test-ready: PHPUnit integration and convenient test helpers
17+
Playwright for PHP lets you launch real browsers (Chromium, Firefox, WebKit), drive pages and locators, and write reliable end‑to‑end tests — all from PHP.
2118

22-
## Requirements
19+
- Familiar model: browser → context → page → locator
20+
- Auto‑waiting interactions reduce flakiness
21+
- PHPUnit integration with a base test case and fluent `expect()` assertions
22+
- Cross‑browser: Chromium, Firefox, and WebKit supported
2323

24+
Requirements:
2425
- PHP 8.3+
25-
- Node.js 20+ (used by the Playwright server and browser binaries)
26+
- Node.js 20+ (used by the bundled Playwright server and browsers)
2627

2728
## Install
2829

30+
Add the library to your project:
31+
2932
```
3033
composer require playwright-php/playwright
3134
```
3235

33-
Optionnally, install the Playwright browsers:
36+
Install the Playwright browsers (Chromium, Firefox, WebKit):
3437

3538
```
36-
bin/playwright-install
39+
composer run install-browsers
40+
# or, if your environment needs extra OS deps
41+
composer run install-browsers-with-deps
3742
```
3843

39-
This installs the Node server dependencies under `bin/` and fetches Playwright browsers.
44+
The PHP library installs and manages a lightweight Node server under the hood; no manual server process is required.
45+
46+
## Usage
47+
48+
### Quick start
4049

41-
## Quick Start
50+
Open a page and print its title:
4251

4352
```php
4453
<?php
@@ -47,68 +56,136 @@ require __DIR__.'/vendor/autoload.php';
4756

4857
use PlaywrightPHP\Playwright;
4958

50-
// Launch Chromium and get a context
5159
$context = Playwright::chromium(['headless' => true]);
52-
53-
// Create a new page and navigate to a website
5460
$page = $context->newPage();
5561
$page->goto('https://example.com');
5662

57-
// Print the page title
58-
echo $page->title().PHP_EOL; // "Example Domain"
63+
echo $page->title().PHP_EOL; // Example Domain
5964

6065
$context->close();
6166
```
6267

63-
Tip: Debug with Inspector by running headed and pausing:
64-
- Env: `PWDEBUG=1 php your_script.php`
65-
- Builder: `$playwright->chromium()->withHeadless(false)->withInspector()->launch();` then `$page->pause();`
68+
### Browser
6669

67-
Minimal `expect()` example:
70+
- Choose a browser: `Playwright::chromium()`, `Playwright::firefox()`, or `Playwright::webkit()`.
71+
- `Playwright::safari()` is an alias of `webkit()`.
72+
- Common launch options: `headless` (bool), `slowMo` (ms), `args` (array of CLI args), and an optional `context` array.
6873

6974
```php
70-
<?php
71-
require __DIR__.'/vendor/autoload.php';
75+
$context = Playwright::webkit([
76+
'headless' => false,
77+
'slowMo' => 200,
78+
'args' => ['--no-sandbox'],
79+
]);
80+
```
7281

73-
use PlaywrightPHP\Playwright;
74-
use function PlaywrightPHP\Testing\expect;
82+
### Page
7583

76-
$context = Playwright::chromium();
84+
Create pages, navigate, evaluate scripts, and take screenshots:
85+
86+
```php
7787
$page = $context->newPage();
7888
$page->goto('https://example.com');
7989

80-
expect($page->locator('h1'))->toHaveText('Example Domain');
81-
expect($page->locator('h1 ~ p'))->toHaveCount(2);
90+
$html = $page->content();
91+
$title = $page->title();
92+
$path = $page->screenshot(__DIR__.'/screenshot.png');
8293

83-
$context->close();
94+
$answer = $page->evaluate('() => 6 * 7'); // 42
8495
```
8596

86-
You can also run the ready-made example:
87-
- php docs/examples/example_expect.php
97+
### Locator
98+
99+
Work with auto‑waiting locators for reliable interactions and assertions:
100+
101+
```php
102+
use function PlaywrightPHP\Testing\expect;
103+
104+
$h1 = $page->locator('h1');
105+
expect($h1)->toHaveText('Example Domain');
106+
107+
$search = $page->locator('#q');
108+
$search->fill('playwright php');
109+
$page->locator('form')->submit();
110+
111+
// Compose and filter
112+
$items = $page->locator('.result-item');
113+
expect($items)->toHaveCount(10);
114+
```
115+
116+
### Server
117+
118+
- A lightweight Node.js Playwright server is installed under `bin/` and started automatically by the PHP library.
119+
- Install browsers with: `composer run install-browsers` (or `install-browsers-with-deps`).
120+
- Requires Node.js 20+ in your environment (local and CI).
88121

89-
## Scripts
122+
### Inspector (debugging)
90123

91-
- composer test — runs the full test suite
92-
- composer analyse — static analysis (PHPStan)
93-
- composer cs-check — code style check
94-
- composer cs-fix — code style fix
95-
- composer run install-browsers — installs server deps and browsers (in `bin/`)
124+
- Run headed by setting `headless => false`.
125+
- Export `PWDEBUG=1` to open the Inspector: `PWDEBUG=1 php your_script.php`.
126+
- You can also call `$page->pause()` to break into the Inspector during a run.
127+
128+
More examples: see `docs/examples/` (e.g., `php docs/examples/expect.php`).
129+
130+
### Route interception
131+
132+
Intercept and control network requests at page or context scope.
133+
134+
- Page-level block (e.g., images):
135+
136+
```php
137+
$page->route('**/*.png', function ($route) {
138+
$route->abort(); // block images
139+
});
140+
```
141+
142+
- Context-level stub API and pass-through others:
143+
144+
```php
145+
$context->route('**/api/todos', function ($route) {
146+
$route->fulfill([
147+
'status' => 200,
148+
'contentType' => 'application/json',
149+
'body' => json_encode(['items' => []]),
150+
]);
151+
});
152+
153+
$context->route('*', fn ($route) => $route->continue());
154+
```
155+
156+
See runnable examples:
157+
- `php docs/examples/route_block_images.php`
158+
- `php docs/examples/route_stub_api.php`
159+
160+
## Testing
161+
162+
Integrate with PHPUnit using the provided base class:
163+
164+
```php
165+
<?php
166+
167+
use PlaywrightPHP\Testing\PlaywrightTestCase;
168+
use function PlaywrightPHP\Testing\expect;
169+
170+
final class MyE2ETest extends PlaywrightTestCase
171+
{
172+
public function test_homepage(): void
173+
{
174+
$this->page->goto('https://example.com');
175+
expect($this->page->locator('h1'))->toHaveText('Example Domain');
176+
}
177+
}
178+
```
96179

97-
## Notes on the Server
180+
Tips:
181+
- On failure, screenshots are saved under `test-failures/`.
182+
- Enable tracing with `PW_TRACE=1` to capture a `trace.zip` for Playwright Trace Viewer.
183+
- Guides: `docs/guide/getting-started.md`, `docs/guide/testing-with-phpunit.md`, `docs/guide/assertions-reference.md`.
98184

99-
- The Node-based Playwright server and its dependencies live under `bin/`.
100-
- Composer’s `install-browsers` script installs dependencies in `bin/` and runs `npx playwright install` there.
101-
- The PHP transport auto-starts the Node server as needed; no manual server process is required.
185+
## Contributing
102186

103-
## Debugging with Playwright Inspector
187+
Contributions are welcome. Please use Conventional Commits, include tests for behavior changes, and ensure docs/examples are updated when relevant. See `docs/contributing/testing.md` for local workflow.
104188

105-
- Enable Inspector via builder: call `withInspector()` and run headed.
106-
- Example: `$browser = $playwright->chromium()->withHeadless(false)->withInspector()->launch();`
107-
- Pause at a point to open Inspector: `$page->pause();`
108-
- Alternatively, export `PWDEBUG` when running your script to enable Inspector globally:
109-
- macOS/Linux: `PWDEBUG=1 php your_script.php`
110-
- Windows (PowerShell): `$env:PWDEBUG='1'; php your_script.php`
189+
## Licence
111190

112-
Notes:
113-
- Inspector opens from the Node server; `PWDEBUG` is forwarded automatically.
114-
- A headed browser (`headless: false`) makes it easier to see UI interactions while debugging.
191+
MIT — see `LICENSE` for details.

bin/playwright-server.js

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,39 @@ class PlaywrightServer {
289289
case 'stopTracing':
290290
await context.tracing.stop({ path: command.path });
291291
return {success: true};
292+
case 'route':
293+
await context.route(command.url, async (route) => {
294+
const routeId = `route_${++this.routeCounter}`;
295+
this.routes.set(routeId, route);
296+
297+
const req = route.request();
298+
let postData = null;
299+
try {
300+
postData = req.postData();
301+
} catch (e) {
302+
postData = null;
303+
}
304+
debugLogger.info('CTX ROUTE', { url: req.url(), method: req.method() });
305+
const event = {
306+
objectId: command.contextId,
307+
event: 'route',
308+
params: {
309+
routeId,
310+
request: {
311+
url: req.url(),
312+
method: req.method(),
313+
headers: req.headers(),
314+
postData: postData ?? null,
315+
resourceType: req.resourceType ? req.resourceType() : 'document'
316+
}
317+
}
318+
};
319+
sendFramedResponse(event);
320+
});
321+
return {success: true};
322+
case 'unroute':
323+
await context.unroute(command.url);
324+
return {success: true};
292325
case 'newPage':
293326
const page = await context.newPage(command.options);
294327
const pageId = `page_${++this.pageCounter}`;
@@ -327,8 +360,36 @@ class PlaywrightServer {
327360
const gotoResponse = await page.goto(command.url, command.options);
328361
return {response: this.serializeResponse(gotoResponse)};
329362
case 'evaluate':
330-
const result = await page.evaluate(command.expression, command.arg);
331-
return {result};
363+
try {
364+
// Attempt function-like evaluation first to support strings like '() => 42' or 'async () => {...}'
365+
const attempt = await page.evaluate(async ({expression, arg}) => {
366+
try {
367+
const func = eval(`(${expression})`);
368+
if (typeof func === 'function') {
369+
const value = await func(arg);
370+
return { ok: true, value };
371+
}
372+
return { ok: false, reason: 'not-a-function' };
373+
} catch (e) {
374+
return { ok: false, reason: e.message };
375+
}
376+
}, {expression: command.expression, arg: command.arg});
377+
378+
if (attempt && attempt.ok === true) {
379+
const value = attempt.value === undefined ? null : attempt.value;
380+
debugLogger.info('PAGE EVALUATE(FUNC) OK', { type: typeof value });
381+
return {result: value};
382+
}
383+
384+
// Fallback: treat expression as direct JS expression (e.g., 'window.foo')
385+
const result = await page.evaluate(command.expression, command.arg);
386+
const value = result === undefined ? null : result;
387+
debugLogger.info('PAGE EVALUATE(EXPR) OK', { type: typeof value });
388+
return {result: value};
389+
} catch (error) {
390+
debugLogger.error('PAGE EVALUATE ERROR', { message: error.message });
391+
throw error;
392+
}
332393
case 'waitForResponse':
333394
const jsAction = command.jsAction;
334395
const [response] = await Promise.all([
@@ -390,6 +451,7 @@ class PlaywrightServer {
390451
} catch (e) {
391452
postData = null;
392453
}
454+
debugLogger.info('PAGE ROUTE', { url: req.url(), method: req.method() });
393455
const event = {
394456
objectId: command.pageId,
395457
event: 'route',
@@ -539,11 +601,17 @@ class PlaywrightServer {
539601

540602
switch (method) {
541603
case 'fulfill':
604+
debugLogger.info('ROUTE FULFILL', { routeId: command.routeId });
542605
await route.fulfill(command.options);
543606
break;
544607
case 'abort':
608+
debugLogger.info('ROUTE ABORT', { routeId: command.routeId, errorCode: command.errorCode });
545609
await route.abort(command.errorCode);
546610
break;
611+
case 'continue':
612+
debugLogger.info('ROUTE CONTINUE', { routeId: command.routeId });
613+
await route.continue(command.options || undefined);
614+
break;
547615
default:
548616
throw new Error(`Unknown route action: ${method}`);
549617
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
require_once __DIR__.'/../../vendor/autoload.php';
12+
13+
use PlaywrightPHP\Playwright;
14+
15+
$context = Playwright::chromium([
16+
'headless' => true,
17+
]);
18+
$page = $context->newPage();
19+
20+
// Block PNG images on this page
21+
$page->route('**/*.png', function ($route): void {
22+
$route->abort();
23+
});
24+
25+
$page->goto('https://example.com');
26+
echo 'Loaded example.com with images blocked'.PHP_EOL;
27+
28+
$context->close();

0 commit comments

Comments
 (0)