Skip to content
Open
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
7 changes: 7 additions & 0 deletions samples/cdp-tests/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ PLAYWRIGHT_SERVICE_ACCESS_TOKEN=
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_API_VERSION=

# Authenticated forward proxy (Required only if you switch the entry point in
# connectOverCDPScript.js / puppeteerScript.js / cdpUseScript.py to the
# `mainWithProxy()` / `main_with_proxy()` variant)
PROXY_SERVER=
PROXY_USERNAME=
PROXY_PASSWORD=
25 changes: 23 additions & 2 deletions samples/cdp-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ Samples for connecting to Microsoft Playwright Service via CDP (Chrome DevTools
|------|----------|----------|-------------|
| `playwright_service_client.py` | Python | Core Module | Shared Python client for all samples |
| `playwrightServiceClient.js` | JavaScript | Core Module | Shared JavaScript client |
| `connectOverCDPScript.py` | Python | **Manual** | Simple connect_over_cdp example |
| `connectOverCDPScript.js` | JavaScript | **Manual** | Simple connectOverCDP example |
| `connectOverCDPScript.py` | Python | **Manual** | Playwright `connect_over_cdp` example |
| `connectOverCDPScript.js` | JavaScript | **Manual** | Playwright `connectOverCDP` example |
| `puppeteerScript.js` | JavaScript | **Manual** | Puppeteer over CDP (proxy variant in same file) |
| `cdpUseScript.py` | Python | **Manual** | Raw CDP via `cdp-use` (proxy variant in same file) |
| `test_runner.py` | Python | **Testing** | Test runner with helpers |
| `Browser-Use-Remote.py` | Python | **AI Agent** | Browser-Use + Azure OpenAI |

Expand Down Expand Up @@ -107,8 +109,27 @@ PLAYWRIGHT_SERVICE_ACCESS_TOKEN=your_access_token
AZURE_OPENAI_API_KEY=your_api_key
AZURE_OPENAI_ENDPOINT=https://<resource>.openai.azure.com/
AZURE_OPENAI_API_VERSION=2023-07-01-preview

# For the opt-in proxy snippets only
PROXY_SERVER=http://<your-proxy>:8080
PROXY_USERNAME=<user>
PROXY_PASSWORD=<password>
```

## 🌐 Optional: Authenticated HTTP proxy

Each manual sample includes a separate proxy entry-point function alongside
the normal `main()`. It isn't run by default — to use it, change the call at
the bottom of the file from `main()` to the proxy variant and set
`PROXY_SERVER` / `PROXY_USERNAME` / `PROXY_PASSWORD` in your env.

- [connectOverCDPScript.js](./connectOverCDPScript.js) — `mainWithProxy()` uses `newContext({ proxy })`
- [puppeteerScript.js](./puppeteerScript.js) — `mainWithProxy()` uses `createBrowserContext({ proxyServer })` + `page.authenticate()`
- [cdpUseScript.py](./cdpUseScript.py) — `main_with_proxy()` uses `Target.createBrowserContext({ proxyServer })` + manual `Fetch.continueWithAuth`

Playwright and Puppeteer answer the proxy 407 challenge for you; with
`cdp-use` the function shows the full dance.

## 📚 Resources

- [Microsoft Playwright Service](https://learn.microsoft.com/azure/playwright-testing/)
Expand Down
149 changes: 149 additions & 0 deletions samples/cdp-tests/cdpUseScript.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
cdp-use over PWW

Drives a remote Chromium on Microsoft Playwright Service using the low-level
`cdp-use` Python CDP client.

----------------------------------------
Install
----------------------------------------
pip install cdp-use python-dotenv aiohttp

----------------------------------------
Required env vars
----------------------------------------
PLAYWRIGHT_SERVICE_URL
PLAYWRIGHT_SERVICE_ACCESS_TOKEN

----------------------------------------
Run
----------------------------------------
python cdpUseScript.py
"""

import asyncio
import os
from typing import Optional

from cdp_use.client import CDPClient
from dotenv import load_dotenv

from playwright_service_client import get_cdp_endpoint

load_dotenv()


async def main():
cdp_url = await get_cdp_endpoint()

async with CDPClient(cdp_url) as client:
ctx = await client.send.Target.createBrowserContext()
target = await client.send.Target.createTarget(
params={"url": "about:blank", "browserContextId": ctx["browserContextId"]}
)
session = await client.send.Target.attachToTarget(
params={"targetId": target["targetId"], "flatten": True}
)
session_id = session["sessionId"]

load_event = asyncio.Event()

def on_load(event, sid: Optional[str]) -> None:
load_event.set()

client.register.Page.loadEventFired(on_load)

await client.send.Page.enable(session_id=session_id)
await client.send.Runtime.enable(session_id=session_id)

await client.send.Page.navigate(
params={"url": "https://example.com"}, session_id=session_id
)
await load_event.wait()

result = await client.send.Runtime.evaluate(
params={"expression": "document.title"}, session_id=session_id
)
print("Page title:", result["result"]["value"])


# Opt-in proxy variant. Not invoked by default — change the entry point at
# the bottom of this file to `main_with_proxy()` to use it. Requires
# PROXY_SERVER / PROXY_USERNAME / PROXY_PASSWORD in your env.
#
# cdp-use does not abstract proxy auth, so we enable Fetch interception
# and answer Fetch.authRequired ourselves with Fetch.continueWithAuth.
async def main_with_proxy():
cdp_url = await get_cdp_endpoint()

async with CDPClient(cdp_url) as client:
ctx = await client.send.Target.createBrowserContext(
params={"proxyServer": os.environ["PROXY_SERVER"]}
)
target = await client.send.Target.createTarget(
params={"url": "about:blank", "browserContextId": ctx["browserContextId"]}
)
session = await client.send.Target.attachToTarget(
params={"targetId": target["targetId"], "flatten": True}
)
session_id = session["sessionId"]

load_event = asyncio.Event()

async def on_auth(event, sid: Optional[str]) -> None:
if event["authChallenge"]["source"] == "Proxy":
await client.send.Fetch.continueWithAuth(
params={
"requestId": event["requestId"],
"authChallengeResponse": {
"response": "ProvideCredentials",
"username": os.environ["PROXY_USERNAME"],
"password": os.environ["PROXY_PASSWORD"],
},
},
session_id=sid,
)
else: # never leak proxy creds to origin servers
await client.send.Fetch.continueWithAuth(
params={
"requestId": event["requestId"],
"authChallengeResponse": {"response": "CancelAuth"},
},
session_id=sid,
)

async def on_paused(event, sid: Optional[str]) -> None:
await client.send.Fetch.continueRequest(
params={"requestId": event["requestId"]}, session_id=sid
)

def on_load(event, sid: Optional[str]) -> None:
load_event.set()

client.register.Fetch.authRequired(on_auth)
client.register.Fetch.requestPaused(on_paused)
client.register.Page.loadEventFired(on_load)

await client.send.Page.enable(session_id=session_id)
await client.send.Runtime.enable(session_id=session_id)
await client.send.Fetch.enable(
params={
"handleAuthRequests": True,
"patterns": [{"urlPattern": "*"}],
},
session_id=session_id,
)

await client.send.Page.navigate(
params={"url": "https://example.com"}, session_id=session_id
)
await load_event.wait()

result = await client.send.Runtime.evaluate(
params={"expression": "document.title"}, session_id=session_id
)
print("Page title (via proxy):", result["result"]["value"])


if __name__ == "__main__":
asyncio.run(main())
27 changes: 27 additions & 0 deletions samples/cdp-tests/connectOverCDPScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ async function main() {
console.log('✅ Done!');
}

// Opt-in proxy variant. Not invoked by default — change the entry point at
// the bottom of this file to `mainWithProxy()` to use it. Requires
// PROXY_SERVER / PROXY_USERNAME / PROXY_PASSWORD in your env. Playwright
// answers the 407 challenge for you.
async function mainWithProxy() {
const cdpUrl = await getCdpEndpoint();
const browser = await chromium.connectOverCDP(
cdpUrl,
{ headers: { 'User-Agent': 'Chrome-DevTools-Protocol/1.3' } }
);

const context = await browser.newContext({
proxy: {
server: process.env.PROXY_SERVER,
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
},
});
const page = await context.newPage();

await page.goto('https://example.com');
console.log(`📌 Page title (via proxy): ${await page.title()}`);

await context.close();
await browser.close();
}

main().catch(error => {
console.error('❌ Error:', error.message);
process.exit(1);
Expand Down
68 changes: 68 additions & 0 deletions samples/cdp-tests/puppeteerScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Puppeteer over CDP - Microsoft Playwright Service
*
* Connects puppeteer-core to a remote Chromium on PWW over CDP.
*
* Install:
* npm install puppeteer-core
*
* Environment Variables:
* PLAYWRIGHT_SERVICE_URL=wss://<region>.api.playwright.microsoft.com/playwrightworkspaces/<workspaceId>/browsers
* PLAYWRIGHT_SERVICE_ACCESS_TOKEN=your_access_token
*
* Usage:
* node puppeteerScript.js
*/

import puppeteer from 'puppeteer-core';
import { getCdpEndpoint } from './playwrightServiceClient.js';

async function main() {
const cdpUrl = await getCdpEndpoint();

const browser = await puppeteer.connect({
browserWSEndpoint: cdpUrl,
defaultViewport: null,
});

const context = await browser.createBrowserContext();
const page = await context.newPage();

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Page title:', await page.title());

await context.close();
await browser.disconnect();
}

// Opt-in proxy variant. Not invoked by default — change the entry point at
// the bottom of this file to `mainWithProxy()` to use it. Requires
// PROXY_SERVER / PROXY_USERNAME / PROXY_PASSWORD in your env.
async function mainWithProxy() {
const cdpUrl = await getCdpEndpoint();

const browser = await puppeteer.connect({
browserWSEndpoint: cdpUrl,
defaultViewport: null,
});

const context = await browser.createBrowserContext({
proxyServer: process.env.PROXY_SERVER,
});
const page = await context.newPage();
await page.authenticate({
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD,
});

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log('Page title (via proxy):', await page.title());

await context.close();
await browser.disconnect();
}

main().catch((err) => {
console.error('Error:', err);
process.exit(1);
});
3 changes: 3 additions & 0 deletions samples/cdp-tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ pytest-asyncio>=0.21.0
# For browser_use_remote.py (AI agent scenario)
pydantic>=2.0.0
browser-use>=0.1.0

# For cdpUseProxyScript.py (opt-in proxy sample)
cdp-use>=0.3.0
19 changes: 18 additions & 1 deletion samples/playwright-lib/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,21 @@ $env:PLAYWRIGHT_RUN_ID="your_guid"
npx ts-node src/example.ts
```

- Test Runs get updated at 5 min interval, so check current test run details after 5 min of running script.
- Test Runs get updated at 5 min interval, so check current test run details after 5 min of running script.

## Optional: route the run through an authenticated HTTP proxy

The default [`src/example.ts`](./src/example.ts) talks to PWW directly. If you
need every BrowserContext to go through an authenticated forward proxy, use
the opt-in [`src/example-proxy.ts`](./src/example-proxy.ts) instead. It adds a
`proxy` option to `browser.newContext()`; Playwright handles the 407
challenge for you.

```powershell
$env:PROXY_SERVER = "http://<your-proxy>:8080"
$env:PROXY_USERNAME = "<user>"
$env:PROXY_PASSWORD = "<password>"
$env:PROXY_ONLY_URL = "http://intranet.example/healthcheck"

npx ts-node src/example-proxy.ts
```
54 changes: 54 additions & 0 deletions samples/playwright-lib/src/example-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { chromium, devices } from 'playwright';
import { randomUUID } from 'crypto';

/**
* Opt-in proxy variant of example.ts.
*
* Same `chromium.connect()` flow against PWW, but every BrowserContext is
* created with a `proxy:` option so all traffic from this run is routed
* through your authenticated HTTP forward proxy. Playwright transparently
* answers the 407 challenge with the supplied credentials.
*
* Required env vars (in addition to PLAYWRIGHT_SERVICE_URL +
* PLAYWRIGHT_SERVICE_ACCESS_TOKEN):
* PROXY_SERVER e.g. http://<your-proxy>:8080
* PROXY_USERNAME
* PROXY_PASSWORD
* PROXY_ONLY_URL the URL to fetch through the proxy
*
* Run:
* npx ts-node src/example-proxy.ts
*/
const runId = process.env['PLAYWRIGHT_RUN_ID'] || randomUUID();
const os = 'linux';
const apiVersion = '2025-09-01';

const wsEndpoint =
`${process.env['PLAYWRIGHT_SERVICE_URL']}` +
`?runId=${encodeURIComponent(runId)}&os=${os}&api-version=${apiVersion}`;

const connectOptions = {
headers: { Authorization: `Bearer ${process.env['PLAYWRIGHT_SERVICE_ACCESS_TOKEN'] || ''}` },
timeout: 3 * 60 * 1000,
exposeNetwork: '<loopback>',
};

const proxy = {
server: process.env['PROXY_SERVER']!,
username: process.env['PROXY_USERNAME'],
password: process.env['PROXY_PASSWORD'],
};

(async () => {
const browser = await chromium.connect(wsEndpoint, connectOptions);
const context = await browser.newContext({ ...devices['Desktop Chrome'], proxy });
const page = await context.newPage();

const target = process.env['PROXY_ONLY_URL']!;
const response = await page.goto(target);
console.log(`status: ${response?.status()}`);
console.log('title :', await page.title());

await context.close();
await browser.close();
})();
Loading