diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 1aa4392cb5..5e1a9b67e3 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -11,6 +11,6 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.38.0" + "@playwright/test": "^1.54.1" } } diff --git a/examples/todomvc/playwright.config.ts b/examples/todomvc/playwright.config.ts index 831aa90355..693899e25c 100644 --- a/examples/todomvc/playwright.config.ts +++ b/examples/todomvc/playwright.config.ts @@ -73,6 +73,18 @@ export default defineConfig({ }, }, + { + name: 'worker', + + /* Project-specific settings. */ + use: { + ...devices['Desktop Chrome'], + connectOptions: { + wsEndpoint: 'ws://localhost:8001/v1/playwright?session_id=12345', + }, + }, + }, + /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', diff --git a/packages/playwright-cloudflare/.npmignore b/packages/playwright-cloudflare/.npmignore index 9cdcd4b421..b642248a6e 100644 --- a/packages/playwright-cloudflare/.npmignore +++ b/packages/playwright-cloudflare/.npmignore @@ -9,6 +9,7 @@ !types/**/*.d.ts # Include playwright core and test entry points +!client.d.ts !index.d.ts !internal.d.ts !test.d.ts diff --git a/packages/playwright-cloudflare/client.d.ts b/packages/playwright-cloudflare/client.d.ts new file mode 100644 index 0000000000..5af286e67e --- /dev/null +++ b/packages/playwright-cloudflare/client.d.ts @@ -0,0 +1,127 @@ +import * as FS from 'fs'; +import type { Browser } from './types/types'; +import { chromium, request, selectors, devices } from './types/types'; +import { env } from 'cloudflare:workers'; + +export * from './types/types'; + +declare module './types/types' { + interface Browser { + /** + * Get the BISO session ID associated with this browser + * + * @public + */ + sessionId(): string; + } +} + +/** + * @public + */ +export interface BrowserWorker { + fetch: typeof fetch; +} + +export type BrowserEndpoint = BrowserWorker | string | URL; + +/** + * @public + */ +export interface AcquireResponse { + sessionId: string; +} + +/** + * @public + */ +export interface ActiveSession { + sessionId: string; + startTime: number; // timestamp + // connection info, if present means there's a connection established + // from a worker to that session + connectionId?: string; + connectionStartTime?: string; +} + +/** + * @public + */ +export interface ClosedSession extends ActiveSession { + endTime: number; // timestamp + closeReason: number; // close reason code + closeReasonText: string; // close reason description +} + +export interface AcquireResponse { + sessionId: string; +} + +/** + * @public + */ +export interface SessionsResponse { + sessions: ActiveSession[]; +} + +/** + * @public + */ +export interface HistoryResponse { + history: ClosedSession[]; +} + +/** + * @public + */ +export interface LimitsResponse { + activeSessions: Array<{id: string}>; + maxConcurrentSessions: number; + allowedBrowserAcquisitions: number; // 1 if allowed, 0 otherwise + timeUntilNextAllowedBrowserAcquisition: number; +} + +/** + * @public + */ +export interface WorkersLaunchOptions { + keep_alive?: number; // milliseconds to keep browser alive even if it has no activity (from 10_000ms to 600_000ms, default is 60_000) +} + +// Extracts the keys whose values match a specified type `ValueType` +type KeysByValue = { + [K in keyof T]: T[K] extends ValueType ? K : never; +}[keyof T]; + +export type BrowserBindingKey = KeysByValue; + +export function connect(endpoint: BrowserWorker, options: { sessionId: string }): Promise; + +export function acquire(endpoint: BrowserEndpoint, options?: WorkersLaunchOptions): Promise; + +/** + * Returns active sessions + * + * @remarks + * Sessions with a connnectionId already have a worker connection established + * + * @param endpoint - Cloudflare worker binding + * @returns List of active sessions + */ +export function sessions(endpoint: BrowserEndpoint): Promise; + +/** + * Returns recent sessions (active and closed) + * + * @param endpoint - Cloudflare worker binding + * @returns List of recent sessions (active and closed) + */ +export function history(endpoint: BrowserEndpoint): Promise; + +/** + * Returns current limits + * + * @param endpoint - Cloudflare worker binding + * @returns current limits + */ +export function limits(endpoint: BrowserEndpoint): Promise; diff --git a/packages/playwright-cloudflare/examples/todomvc/package-lock.json b/packages/playwright-cloudflare/examples/todomvc/package-lock.json index 1a705c5277..2dc80092bc 100644 --- a/packages/playwright-cloudflare/examples/todomvc/package-lock.json +++ b/packages/playwright-cloudflare/examples/todomvc/package-lock.json @@ -9,13 +9,21 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@cloudflare/playwright": "^0.0.11" + "@cloudflare/playwright": "file:../.." }, "devDependencies": { "typescript": "^5.8.3", "wrangler": "^4.26.0" } }, + "../..": { + "name": "@cloudflare/playwright", + "version": "0.0.1-next", + "license": "Apache-2.0", + "devDependencies": { + "vite": "^6.1.0" + } + }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", @@ -30,10 +38,8 @@ } }, "node_modules/@cloudflare/playwright": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@cloudflare/playwright/-/playwright-0.0.11.tgz", - "integrity": "sha512-5meAHlST5K3MKYA7TyL6Ns1XQhYB615qYZxDSn0UnSX14MFgRe+pqlq482dXbgMBappnvvsjZl+Ea3/FQcdbvQ==", - "license": "Apache-2.0" + "resolved": "../..", + "link": true }, "node_modules/@cloudflare/unenv-preset": { "version": "2.4.1", diff --git a/packages/playwright-cloudflare/examples/todomvc/package.json b/packages/playwright-cloudflare/examples/todomvc/package.json index 9796f3b54e..7b4c489660 100644 --- a/packages/playwright-cloudflare/examples/todomvc/package.json +++ b/packages/playwright-cloudflare/examples/todomvc/package.json @@ -14,6 +14,6 @@ "wrangler": "^4.26.0" }, "dependencies": { - "@cloudflare/playwright": "^0.0.11" + "@cloudflare/playwright": "file:../.." } } diff --git a/packages/playwright-cloudflare/examples/todomvc/src/index.ts b/packages/playwright-cloudflare/examples/todomvc/src/index.ts index 33b954e7b1..ebfe866570 100644 --- a/packages/playwright-cloudflare/examples/todomvc/src/index.ts +++ b/packages/playwright-cloudflare/examples/todomvc/src/index.ts @@ -1,22 +1,43 @@ -import { launch } from '@cloudflare/playwright'; -import { expect } from '@cloudflare/playwright/test'; -import fs from '@cloudflare/playwright/fs'; +import { connect, acquire, BrowserWorker } from '@cloudflare/playwright/client'; +import { debug } from '@cloudflare/playwright/internal'; + +// eslint-disable-next-line no-console +const log = console.log; + +export function localBrowserSim(baseUrl: string) { + // hack to allow for local only dev, which calls a local chrome + return { + async fetch(request: string, requestInit?: RequestInit | Request): Promise { + // The puppeteer fork calls the binding with a fake host + const u = request.replace('http://fake.host', ''); + log(`LOCAL ${baseUrl}${u}`); + return fetch(`${baseUrl}${u}`, requestInit).catch(err => { + log(err); + throw new Error('Unable to create new browser: code: 429: message: Too Many Requests. Local sim'); + }); + }, + }; +} + +function getBrowserConnection(isLocalEnv = true): BrowserWorker { + return localBrowserSim('http://localhost:3000') as BrowserWorker; +} export default { async fetch(request: Request, env: Env) { - const { searchParams } = new URL(request.url); - const todos = searchParams.getAll('todo'); - const trace = searchParams.has('trace'); + debug.enable('pw:*'); + const binding = getBrowserConnection(); + const { sessionId } = await acquire(binding); + log(`Acquired session ID: ${sessionId}`); + const browser = await connect(binding, { sessionId }); - const browser = await launch(env.MYBROWSER); - const page = await browser.newPage(); + log(`Connected to browser with session ID: ${sessionId}`); - if (trace) - await page.context().tracing.start({ screenshots: true, snapshots: true }); + const page = await browser.newPage(); await page.goto('https://demo.playwright.dev/todomvc'); - const TODO_ITEMS = todos.length > 0 ? todos : [ + const TODO_ITEMS = [ 'buy some cheese', 'feed the cat', 'book a doctors appointment' @@ -28,32 +49,13 @@ export default { await newTodo.press('Enter'); } - await expect(page.getByTestId('todo-title')).toHaveCount(TODO_ITEMS.length); - - await Promise.all(TODO_ITEMS.map( - (value, index) => expect(page.getByTestId('todo-title').nth(index)).toHaveText(value) - )); + const img = await page.screenshot(); + await browser.close(); - if (trace) { - await page.context().tracing.stop({ path: 'trace.zip' }); - await browser.close(); - const file = await fs.promises.readFile('trace.zip'); - - return new Response(file, { - status: 200, - headers: { - 'Content-Type': 'application/zip', - }, - }); - } else { - const img = await page.screenshot(); - await browser.close(); - - return new Response(img, { - headers: { - 'Content-Type': 'image/png', - }, - }); - } + return new Response(img, { + headers: { + 'Content-Type': 'image/png', + }, + }); }, }; diff --git a/packages/playwright-cloudflare/examples/todomvc/wrangler.toml b/packages/playwright-cloudflare/examples/todomvc/wrangler.toml index 84e5ba1180..41871cd2c0 100644 --- a/packages/playwright-cloudflare/examples/todomvc/wrangler.toml +++ b/packages/playwright-cloudflare/examples/todomvc/wrangler.toml @@ -5,5 +5,6 @@ compatibility_flags = ["nodejs_compat"] compatibility_date = "2025-03-05" upload_source_maps = true -[browser] -binding = "MYBROWSER" +unsafe.bindings = [ + {name = "MYBROWSER", type = "browser", internal_env = "https://core-staging.rendering.cfdata.org/"}, +] diff --git a/packages/playwright-cloudflare/package.json b/packages/playwright-cloudflare/package.json index 3ff54c9112..3cbe999ab0 100644 --- a/packages/playwright-cloudflare/package.json +++ b/packages/playwright-cloudflare/package.json @@ -28,6 +28,12 @@ "import": "./lib/esm/bundles/fs.js", "require": "./lib/cjs/bundles/fs.js", "default": "./lib/esm/bundles/fs.js" + }, + "./client": { + "types": "./client.d.ts", + "import": "./lib/esm/client.js", + "require": "./lib/cjs/client.js", + "default": "./lib/esm/client.js" } }, "scripts": { diff --git a/packages/playwright-cloudflare/src/client.ts b/packages/playwright-cloudflare/src/client.ts new file mode 100644 index 0000000000..320defc46d --- /dev/null +++ b/packages/playwright-cloudflare/src/client.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Connection } from 'playwright-core/lib/client/connection'; +import { nodePlatform } from 'playwright-core/lib/utils'; + +import type { Browser, BrowserWorker } from '..'; + +export { acquire, sessions, limits, history } from './session-management'; + +export type Options = { + headless?: boolean; +}; + +export async function connect(endpoint: BrowserWorker, options: { sessionId: string }): Promise { + const response = await endpoint.fetch(`http://fake.host/v1/playwright?browser_session=${options.sessionId}`, { + headers: { + 'Upgrade': 'websocket', + }, + }); + const ws = response.webSocket; + + if (!ws) + throw new Error('WebSocket connection not established'); + + ws.accept(); + + const connection = new Connection({ + ...nodePlatform, + log(name: 'api' | 'channel', message: string | Error | object) { + // eslint-disable-next-line no-console + console.debug(name, message); + }, + }); + connection.onmessage = message => ws.send(JSON.stringify(message)); + ws.addEventListener('message', message => { + const data = message.data instanceof ArrayBuffer ? Buffer.from(message.data).toString() : message.data; + connection.dispatch(JSON.parse(data)); + }); + ws.addEventListener('close', () => connection.close()); + + const playwright = await connection.initializePlaywright(); + const browser = playwright._preLaunchedBrowser() as unknown as Browser; + // TODO Hack to ensure the browser is closed (I still think websockets are not being closed properly) + (browser as any)._closedPromise = Promise.resolve(); + browser.sessionId = () => options.sessionId; + + return browser; +} diff --git a/packages/playwright-cloudflare/src/index.ts b/packages/playwright-cloudflare/src/index.ts index 51330adfe5..caf8f737dc 100644 --- a/packages/playwright-cloudflare/src/index.ts +++ b/packages/playwright-cloudflare/src/index.ts @@ -9,10 +9,13 @@ import { transportZone, WebSocketTransport } from './cloudflare/webSocketTranspo import { wrapClientApis } from './cloudflare/wrapClientApis'; import { unsupportedOperations } from './cloudflare/unsupportedOperations'; import * as packageJson from '../package.json'; +import { acquire, extractOptions, getBrowserBinding, limits, sessions, history, HTTP_FAKE_HOST, WS_FAKE_HOST } from './session-management'; import type { ProtocolRequest } from 'playwright-core/lib/server/transport'; import type { CRBrowser } from 'playwright-core/lib/server/chromium/crBrowser'; -import type { AcquireResponse, ActiveSession, Browser, BrowserBindingKey, BrowserEndpoint, BrowserWorker, ClosedSession, ConnectOverCDPOptions, HistoryResponse, LimitsResponse, SessionsResponse, WorkersLaunchOptions } from '..'; +import type { Browser, BrowserBindingKey, BrowserEndpoint, BrowserWorker, ConnectOverCDPOptions, WorkersLaunchOptions } from '..'; + +export { sessions, history, acquire, limits } from './session-management'; function resetMonotonicTime() { // performance.timeOrigin is always 0 in Cloudflare Workers. Besides, Date.now() is 0 in global scope, @@ -26,9 +29,6 @@ const playwright = createInProcessPlaywright(); unsupportedOperations(playwright); wrapClientApis(); -const HTTP_FAKE_HOST = 'http://fake.host'; -const WS_FAKE_HOST = 'ws://fake.host'; - const originalConnectOverCDP = playwright.chromium.connectOverCDP; // playwright-mcp uses playwright.chromium.connectOverCDP if a CDP endpoint is passed, // so we need to override it to use our own connectOverCDP implementation @@ -62,17 +62,6 @@ async function connectDevtools(endpoint: BrowserEndpoint, options: { sessionId: return webSocket; } -function extractOptions(endpoint: BrowserEndpoint): { sessionId?: string, keep_alive?: number, persistent?: boolean } { - if (typeof endpoint === 'string' || endpoint instanceof URL) { - const url = endpoint instanceof URL ? endpoint : new URL(endpoint); - const sessionId = url.searchParams.get('browser_session') ?? undefined; - const keepAlive = url.searchParams.has('keep_alive') ? parseInt(url.searchParams.get('keep_alive')!, 10) : undefined; - const persistent = url.searchParams.has('persistent'); - return { sessionId, keep_alive: keepAlive, persistent }; - } - return {}; -} - export function endpointURLString(binding: BrowserWorker | BrowserBindingKey, options?: { sessionId?: string, persistent?: boolean, keepAlive?: number }): string { const bindingKey = typeof binding === 'string' ? binding : Object.keys(env).find(key => (env as any)[key] === binding); if (!bindingKey || !(bindingKey in env)) @@ -100,17 +89,6 @@ async function createBrowser(transport: WebSocketTransport, options?: { persiste }); } -function getBrowserBinding(endpoint: BrowserEndpoint): BrowserWorker { - if (typeof endpoint === 'string' || endpoint instanceof URL) { - const url = endpoint instanceof URL ? endpoint : new URL(endpoint); - const binding = url.searchParams.get('browser_binding') as BrowserBindingKey; - if (!binding || !(binding in env)) - throw new Error(`No binding found for ${binding}`); - return env[binding]; - } - return endpoint; -} - export async function connect(endpoint: string | URL): Promise; export async function connect(endpoint: BrowserWorker, sessionIdOrOptions: string | { sessionId: string, persistent?: boolean }): Promise; export async function connect(endpoint: BrowserEndpoint, sessionIdOrOptions?: string | { sessionId: string, persistent?: boolean }): Promise { @@ -143,64 +121,6 @@ export async function launch(endpoint: BrowserEndpoint, launchOptions?: WorkersL return browser; } -export async function acquire(endpoint: BrowserEndpoint, options?: WorkersLaunchOptions): Promise { - options = { ...extractOptions(endpoint), ...options }; - let acquireUrl = `${HTTP_FAKE_HOST}/v1/acquire`; - if (options?.keep_alive) - acquireUrl = `${acquireUrl}?keep_alive=${options.keep_alive}`; - - const res = await getBrowserBinding(endpoint).fetch(acquireUrl); - const status = res.status; - const text = await res.text(); - if (status !== 200) { - throw new Error( - `Unable to create new browser: code: ${status}: message: ${text}` - ); - } - // Got a 200, so response text is actually an AcquireResponse - const response: AcquireResponse = JSON.parse(text); - return response; -} - -export async function sessions(endpoint: BrowserEndpoint): Promise { - const res = await getBrowserBinding(endpoint).fetch(`${HTTP_FAKE_HOST}/v1/sessions`); - const status = res.status; - const text = await res.text(); - if (status !== 200) { - throw new Error( - `Unable to fetch new sessions: code: ${status}: message: ${text}` - ); - } - const data: SessionsResponse = JSON.parse(text); - return data.sessions; -} - -export async function history(endpoint: BrowserEndpoint): Promise { - const res = await getBrowserBinding(endpoint).fetch(`${HTTP_FAKE_HOST}/v1/history`); - const status = res.status; - const text = await res.text(); - if (status !== 200) { - throw new Error( - `Unable to fetch account history: code: ${status}: message: ${text}` - ); - } - const data: HistoryResponse = JSON.parse(text); - return data.history; -} - -export async function limits(endpoint: BrowserEndpoint): Promise { - const res = await getBrowserBinding(endpoint).fetch(`${HTTP_FAKE_HOST}/v1/limits`); - const status = res.status; - const text = await res.text(); - if (status !== 200) { - throw new Error( - `Unable to fetch account limits: code: ${status}: message: ${text}` - ); - } - const data: LimitsResponse = JSON.parse(text); - return data; -} - export const chromium = playwright.chromium; export const selectors = playwright.selectors; export const devices = playwright.devices; diff --git a/packages/playwright-cloudflare/src/session-management.ts b/packages/playwright-cloudflare/src/session-management.ts new file mode 100644 index 0000000000..187955da84 --- /dev/null +++ b/packages/playwright-cloudflare/src/session-management.ts @@ -0,0 +1,87 @@ +import { env } from 'cloudflare:workers'; + +import type { AcquireResponse, ActiveSession, BrowserBindingKey, BrowserEndpoint, BrowserWorker, ClosedSession, HistoryResponse, LimitsResponse, SessionsResponse, WorkersLaunchOptions } from '..'; + + +export const HTTP_FAKE_HOST = 'http://fake.host'; +export const WS_FAKE_HOST = 'ws://fake.host'; + +export function getBrowserBinding(endpoint: BrowserEndpoint): BrowserWorker { + if (typeof endpoint === 'string' || endpoint instanceof URL) { + const url = endpoint instanceof URL ? endpoint : new URL(endpoint); + const binding = url.searchParams.get('browser_binding') as BrowserBindingKey; + if (!binding || !(binding in env)) + throw new Error(`No binding found for ${binding}`); + return env[binding]; + } + return endpoint; +} + +export function extractOptions(endpoint: BrowserEndpoint): { sessionId?: string, keep_alive?: number, persistent?: boolean } { + if (typeof endpoint === 'string' || endpoint instanceof URL) { + const url = endpoint instanceof URL ? endpoint : new URL(endpoint); + const sessionId = url.searchParams.get('browser_session') ?? undefined; + const keepAlive = url.searchParams.has('keep_alive') ? parseInt(url.searchParams.get('keep_alive')!, 10) : undefined; + const persistent = url.searchParams.has('persistent'); + return { sessionId, keep_alive: keepAlive, persistent }; + } + return {}; +} + +export async function acquire(endpoint: BrowserEndpoint, options?: WorkersLaunchOptions): Promise { + options = { ...extractOptions(endpoint), ...options }; + let acquireUrl = `${HTTP_FAKE_HOST}/v1/acquire`; + if (options?.keep_alive) + acquireUrl = `${acquireUrl}?keep_alive=${options.keep_alive}`; + + const res = await getBrowserBinding(endpoint).fetch(acquireUrl); + const status = res.status; + const text = await res.text(); + if (status !== 200) { + throw new Error( + `Unable to create new browser: code: ${status}: message: ${text}` + ); + } + // Got a 200, so response text is actually an AcquireResponse + const response: AcquireResponse = JSON.parse(text); + return response; +} + +export async function sessions(endpoint: BrowserEndpoint): Promise { + const res = await getBrowserBinding(endpoint).fetch(`${HTTP_FAKE_HOST}/v1/sessions`); + const status = res.status; + const text = await res.text(); + if (status !== 200) { + throw new Error( + `Unable to fetch new sessions: code: ${status}: message: ${text}` + ); + } + const data: SessionsResponse = JSON.parse(text); + return data.sessions; +} + +export async function history(endpoint: BrowserEndpoint): Promise { + const res = await getBrowserBinding(endpoint).fetch(`${HTTP_FAKE_HOST}/v1/history`); + const status = res.status; + const text = await res.text(); + if (status !== 200) { + throw new Error( + `Unable to fetch account history: code: ${status}: message: ${text}` + ); + } + const data: HistoryResponse = JSON.parse(text); + return data.history; +} + +export async function limits(endpoint: BrowserEndpoint): Promise { + const res = await getBrowserBinding(endpoint).fetch(`${HTTP_FAKE_HOST}/v1/limits`); + const status = res.status; + const text = await res.text(); + if (status !== 200) { + throw new Error( + `Unable to fetch account limits: code: ${status}: message: ${text}` + ); + } + const data: LimitsResponse = JSON.parse(text); + return data; +} diff --git a/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts b/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts index 82e2a19a29..27daacd393 100644 --- a/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts +++ b/packages/playwright-cloudflare/tests/src/server/workerFixtures.ts @@ -1,5 +1,6 @@ import { _baseTest, currentTestContext, runWithExpectApiListener } from '@cloudflare/playwright/internal'; import playwright, { connect } from '@cloudflare/playwright'; +import { connect as pwsConnect } from '@cloudflare/playwright/client'; import { env } from 'cloudflare:workers'; import fs from '@cloudflare/playwright/fs'; import { expect as baseExpect } from '@cloudflare/playwright/test'; @@ -19,9 +20,10 @@ export type WorkersWorkerFixtures = { sessionId: string; bindingName: BrowserBindingName; binding: BrowserWorker; + usePlaywrightServer: boolean; cdnTraces: { - worker: CdnTrace; - browser: CdnTrace; + worker?: CdnTrace; + browser?: CdnTrace; }; }; @@ -177,10 +179,11 @@ export type TestModeTestFixtures = { }; function parseTrace(trace: string) { - return Object.fromEntries(trace.split('\n').filter(line => line).map(line => { + const parsedCdnTrace = Object.fromEntries(trace.split('\n').filter(line => line).map(line => { const [key, value] = line.split('='); return [key, value]; - })) as { loc: string, colo: string }; + })); + return 'loc' in parsedCdnTrace && 'colo' in parsedCdnTrace ? parsedCdnTrace as CdnTrace : undefined; } export const test = platformTest.extend({ @@ -213,16 +216,20 @@ export const test = platformTest.extend { + await run(bindingName === 'BROWSER_BRAPI_STAGING'); + }, { scope: 'worker' }], + cdnTraces: [async ({ sessionId, browser }, run, workerInfo) => { - const worker = parseTrace(await fetch('https://1.1.1.1/cdn-cgi/trace').then(resp => resp.text())); + const worker = parseTrace(await fetch('https://1.1.1.1/cdn-cgi/trace').then(resp => resp.text()).catch(() => '')); const context = await browser.newContext(); const page = await context.newPage(); - const browserCdnTrace = parseTrace(await page.goto('https://1.1.1.1/cdn-cgi/trace').then(resp => resp!.text())); + const browserCdnTrace = parseTrace(await page.goto('https://1.1.1.1/cdn-cgi/trace').then(resp => resp!.text()).catch(() => '')); await page.close(); await context.close(); // eslint-disable-next-line no-console - console.log(`ℹ️ Session ID: ${sessionId}, Worker: ${worker.colo}, Browser: ${browserCdnTrace.colo}`); + console.log(`ℹ️ Session ID: ${sessionId}, Worker: ${worker?.colo}, Browser: ${browserCdnTrace?.colo}`); await run({ worker, browser: browserCdnTrace }); }, { scope: 'worker' }], @@ -252,8 +259,8 @@ export const test = platformTest.extend run(playwright), { scope: 'worker' }], - browser: [async ({ binding, sessionId }, run) => { - const browser = await connect(binding, sessionId); + browser: [async ({ usePlaywrightServer, binding, sessionId }, run) => { + const browser = await (usePlaywrightServer ? pwsConnect : connect)(binding, { sessionId }); await run(browser); await browser.close(); }, { scope: 'worker' }], @@ -389,13 +396,19 @@ export const test = platformTest.extend