Skip to content

Commit e737930

Browse files
committed
chore: playwright server poc
1 parent 0bbe648 commit e737930

File tree

13 files changed

+326
-132
lines changed

13 files changed

+326
-132
lines changed

examples/todomvc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
"author": "",
1212
"license": "ISC",
1313
"devDependencies": {
14-
"@playwright/test": "^1.38.0"
14+
"@playwright/test": "^1.54.1"
1515
}
1616
}

examples/todomvc/playwright.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ export default defineConfig({
7373
},
7474
},
7575

76+
{
77+
name: 'worker',
78+
79+
/* Project-specific settings. */
80+
use: {
81+
...devices['Desktop Chrome'],
82+
connectOptions: {
83+
wsEndpoint: 'ws://localhost:8001/v1/playwright?session_id=12345',
84+
},
85+
},
86+
},
87+
7688
/* Test against mobile viewports. */
7789
// {
7890
// name: 'Mobile Chrome',

packages/playwright-cloudflare/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
!types/**/*.d.ts
1010

1111
# Include playwright core and test entry points
12+
!client.d.ts
1213
!index.d.ts
1314
!internal.d.ts
1415
!test.d.ts
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import * as FS from 'fs';
2+
import type { Browser } from './types/types';
3+
import { chromium, request, selectors, devices } from './types/types';
4+
import { env } from 'cloudflare:workers';
5+
6+
export * from './types/types';
7+
8+
declare module './types/types' {
9+
interface Browser {
10+
/**
11+
* Get the BISO session ID associated with this browser
12+
*
13+
* @public
14+
*/
15+
sessionId(): string;
16+
}
17+
}
18+
19+
/**
20+
* @public
21+
*/
22+
export interface BrowserWorker {
23+
fetch: typeof fetch;
24+
}
25+
26+
export type BrowserEndpoint = BrowserWorker | string | URL;
27+
28+
/**
29+
* @public
30+
*/
31+
export interface AcquireResponse {
32+
sessionId: string;
33+
}
34+
35+
/**
36+
* @public
37+
*/
38+
export interface ActiveSession {
39+
sessionId: string;
40+
startTime: number; // timestamp
41+
// connection info, if present means there's a connection established
42+
// from a worker to that session
43+
connectionId?: string;
44+
connectionStartTime?: string;
45+
}
46+
47+
/**
48+
* @public
49+
*/
50+
export interface ClosedSession extends ActiveSession {
51+
endTime: number; // timestamp
52+
closeReason: number; // close reason code
53+
closeReasonText: string; // close reason description
54+
}
55+
56+
export interface AcquireResponse {
57+
sessionId: string;
58+
}
59+
60+
/**
61+
* @public
62+
*/
63+
export interface SessionsResponse {
64+
sessions: ActiveSession[];
65+
}
66+
67+
/**
68+
* @public
69+
*/
70+
export interface HistoryResponse {
71+
history: ClosedSession[];
72+
}
73+
74+
/**
75+
* @public
76+
*/
77+
export interface LimitsResponse {
78+
activeSessions: Array<{id: string}>;
79+
maxConcurrentSessions: number;
80+
allowedBrowserAcquisitions: number; // 1 if allowed, 0 otherwise
81+
timeUntilNextAllowedBrowserAcquisition: number;
82+
}
83+
84+
/**
85+
* @public
86+
*/
87+
export interface WorkersLaunchOptions {
88+
keep_alive?: number; // milliseconds to keep browser alive even if it has no activity (from 10_000ms to 600_000ms, default is 60_000)
89+
}
90+
91+
// Extracts the keys whose values match a specified type `ValueType`
92+
type KeysByValue<T, ValueType> = {
93+
[K in keyof T]: T[K] extends ValueType ? K : never;
94+
}[keyof T];
95+
96+
export type BrowserBindingKey = KeysByValue<typeof env, BrowserWorker>;
97+
98+
export function connect(endpoint: BrowserWorker, options: { sessionId: string }): Promise<Browser>;
99+
100+
export function acquire(endpoint: BrowserEndpoint, options?: WorkersLaunchOptions): Promise<AcquireResponse>;
101+
102+
/**
103+
* Returns active sessions
104+
*
105+
* @remarks
106+
* Sessions with a connnectionId already have a worker connection established
107+
*
108+
* @param endpoint - Cloudflare worker binding
109+
* @returns List of active sessions
110+
*/
111+
export function sessions(endpoint: BrowserEndpoint): Promise<ActiveSession[]>;
112+
113+
/**
114+
* Returns recent sessions (active and closed)
115+
*
116+
* @param endpoint - Cloudflare worker binding
117+
* @returns List of recent sessions (active and closed)
118+
*/
119+
export function history(endpoint: BrowserEndpoint): Promise<ClosedSession[]>;
120+
121+
/**
122+
* Returns current limits
123+
*
124+
* @param endpoint - Cloudflare worker binding
125+
* @returns current limits
126+
*/
127+
export function limits(endpoint: BrowserEndpoint): Promise<LimitsResponse>;

packages/playwright-cloudflare/examples/todomvc/package-lock.json

Lines changed: 11 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/playwright-cloudflare/examples/todomvc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
"wrangler": "^4.26.0"
1515
},
1616
"dependencies": {
17-
"@cloudflare/playwright": "^0.0.11"
17+
"@cloudflare/playwright": "file:../.."
1818
}
1919
}
Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
1-
import { launch } from '@cloudflare/playwright';
2-
import { expect } from '@cloudflare/playwright/test';
3-
import fs from '@cloudflare/playwright/fs';
1+
import { connect, acquire } from '@cloudflare/playwright/client';
2+
3+
// eslint-disable-next-line no-console
4+
const log = console.log;
45

56
export default {
67
async fetch(request: Request, env: Env) {
7-
const { searchParams } = new URL(request.url);
8-
const todos = searchParams.getAll('todo');
9-
const trace = searchParams.has('trace');
8+
const { sessionId } = await acquire(env.MYBROWSER);
9+
const browser = await connect(env.MYBROWSER, { sessionId });
1010

11-
const browser = await launch(env.MYBROWSER);
12-
const page = await browser.newPage();
11+
log(`Connected to browser with session ID: ${sessionId}`);
1312

14-
if (trace)
15-
await page.context().tracing.start({ screenshots: true, snapshots: true });
13+
const page = await browser.newPage();
1614

1715
await page.goto('https://demo.playwright.dev/todomvc');
1816

19-
const TODO_ITEMS = todos.length > 0 ? todos : [
17+
const TODO_ITEMS = [
2018
'buy some cheese',
2119
'feed the cat',
2220
'book a doctors appointment'
@@ -28,32 +26,13 @@ export default {
2826
await newTodo.press('Enter');
2927
}
3028

31-
await expect(page.getByTestId('todo-title')).toHaveCount(TODO_ITEMS.length);
32-
33-
await Promise.all(TODO_ITEMS.map(
34-
(value, index) => expect(page.getByTestId('todo-title').nth(index)).toHaveText(value)
35-
));
36-
37-
if (trace) {
38-
await page.context().tracing.stop({ path: 'trace.zip' });
39-
await browser.close();
40-
const file = await fs.promises.readFile('trace.zip');
41-
42-
return new Response(file, {
43-
status: 200,
44-
headers: {
45-
'Content-Type': 'application/zip',
46-
},
47-
});
48-
} else {
49-
const img = await page.screenshot();
50-
await browser.close();
51-
52-
return new Response(img, {
53-
headers: {
54-
'Content-Type': 'image/png',
55-
},
56-
});
57-
}
29+
const img = await page.screenshot();
30+
await browser.close();
31+
32+
return new Response(img, {
33+
headers: {
34+
'Content-Type': 'image/png',
35+
},
36+
});
5837
},
5938
};

packages/playwright-cloudflare/examples/todomvc/wrangler.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ compatibility_flags = ["nodejs_compat"]
55
compatibility_date = "2025-03-05"
66
upload_source_maps = true
77

8-
[browser]
9-
binding = "MYBROWSER"
8+
unsafe.bindings = [
9+
{name = "MYBROWSER", type = "browser", internal_env = "https://core-staging.rendering.cfdata.org/"},
10+
]

packages/playwright-cloudflare/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
"import": "./lib/esm/bundles/fs.js",
2929
"require": "./lib/cjs/bundles/fs.js",
3030
"default": "./lib/esm/bundles/fs.js"
31+
},
32+
"./client": {
33+
"types": "./client.d.ts",
34+
"import": "./lib/esm/client.js",
35+
"require": "./lib/cjs/client.js",
36+
"default": "./lib/esm/client.js"
3137
}
3238
},
3339
"scripts": {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the 'License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Connection } from 'playwright-core/lib/client/connection';
18+
import { nodePlatform } from 'playwright-core/lib/utils';
19+
import { debug } from 'playwright-core/lib/utilsBundle';
20+
21+
import type { Browser, BrowserWorker } from '..';
22+
23+
export { acquire, sessions, limits, history } from './session-management';
24+
25+
export type Options = {
26+
headless?: boolean;
27+
};
28+
29+
export async function connect(endpoint: BrowserWorker, options: { sessionId: string }): Promise<Browser> {
30+
debug.enable('pw:*');
31+
const response = await endpoint.fetch(`http://fake.host/v1/playwright?browser_session=${options.sessionId}`, {
32+
headers: {
33+
'Upgrade': 'websocket',
34+
},
35+
});
36+
const ws = response.webSocket;
37+
38+
if (!ws)
39+
throw new Error('WebSocket connection not established');
40+
41+
ws.accept();
42+
await new Promise((f, r) => {
43+
ws.addEventListener('open', f);
44+
ws.addEventListener('error', r);
45+
});
46+
47+
const connection = new Connection(nodePlatform);
48+
connection.onmessage = message => ws.send(JSON.stringify(message));
49+
ws.addEventListener('message', message => connection.dispatch(JSON.parse(message.data)));
50+
ws.addEventListener('close', () => connection.close());
51+
52+
const playwright = await connection.initializePlaywright();
53+
const browser = playwright._preLaunchedBrowser() as unknown as Browser;
54+
browser.sessionId = () => options.sessionId;
55+
return browser;
56+
}

0 commit comments

Comments
 (0)