From e2b3465a9effc69310d3669832e2df7135264cc2 Mon Sep 17 00:00:00 2001 From: Filip Weiss Date: Wed, 14 Jan 2026 21:31:47 +0100 Subject: [PATCH 1/4] add scoped cdp connect, fix docs, add tests I assumed that calling `close` on the browser connected via cdp would close it but this is not the case. see https://playwright.dev/docs/api/class-browser#browser-close I discovered this while writing tests for CDP. So i fixed the docs, added a scoped cdp connect method. --- README.md | 30 +++++++++++++ src/playwright.test.ts | 98 +++++++++++++++++++++++++++++++++++++++++- src/playwright.ts | 85 +++++++++++++++++++++++++----------- 3 files changed, 186 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 16b846b..3ba0a44 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,36 @@ const program = Effect.gen(function* () { }).pipe(Effect.scoped); ``` +## Connecting via CDP + +You can connect to an existing browser instance using the Chrome DevTools Protocol (CDP). + +```ts +const program = Effect.gen(function* () { + const playwright = yield* Playwright; + + // Use connectCDPScoped to automatically close the CONNECTION when the scope ends + // Note: This does NOT close the browser process itself, only the CDP connection. + const browser = yield* playwright.connectCDPScoped("http://localhost:9222"); + + const page = yield* browser.newPage(); + // ... +}).pipe(Effect.scoped); +``` + +If you need to manage the connection lifecycle manually, use `connectCDP`: + +```ts +const program = Effect.gen(function* () { + const playwright = yield* Playwright; + const browser = yield* playwright.connectCDP("http://localhost:9222"); + + // ... use browser ... + + yield* browser.close; +}); +``` + ## PlaywrightEnvironment (Experimental) The `PlaywrightEnvironment` simplifies setup by allowing you to configure the browser type and launch options once and reuse them across your application. diff --git a/src/playwright.test.ts b/src/playwright.test.ts index 64faf8c..7f479c1 100644 --- a/src/playwright.test.ts +++ b/src/playwright.test.ts @@ -1,4 +1,4 @@ -import { assert, expect, layer } from "@effect/vitest"; +import { assert, layer } from "@effect/vitest"; import { Effect } from "effect"; import { Playwright } from "effect-playwright"; import { chromium } from "playwright-core"; @@ -66,7 +66,101 @@ layer(Playwright.layer)("Playwright", (it) => { result._tag === "PlaywrightError", "Expected failure with timeout 0", ); - expect(result.reason).toBe("Timeout"); + assert(result.reason === "Timeout", "Expected reason to be timeout"); + }), + ); + + it.scoped( + "should connect via CDP", + Effect.fn(function* () { + const playwright = yield* Playwright; + + // 1. Launch a browser that exposes CDP + yield* playwright.launchScoped(chromium, { + args: [ + "--remote-debugging-port=9222", + "--remote-debugging-address=127.0.0.1", + ], + }); + + // 2. Connect to it via CDP + const browser = yield* playwright.connectCDP("http://127.0.0.1:9222"); + + // 3. Cleanup connection (doesn't close the browser itself, but closes the CDP connection) + yield* Effect.addFinalizer(() => browser.close.pipe(Effect.ignore)); + + const page = yield* browser.newPage(); + const content = yield* page.evaluate(() => "cdp works"); + assert(content === "cdp works", "Expected content to be cdp works"); + }), + ); + + it.scoped( + "should connect via CDP (confirm browser.close only closes CDP connection)", + Effect.fn(function* () { + const playwright = yield* Playwright; + + // 1. Launch a browser that exposes CDP + const directBrowser = yield* playwright.launchScoped(chromium, { + args: [ + "--remote-debugging-port=9222", + "--remote-debugging-address=127.0.0.1", + ], + }); + + // 2. Connect to it via CDP + const browser = yield* playwright.connectCDP("http://127.0.0.1:9222"); + + // 3. Cleanup connection now + yield* browser.close; + + assert( + (yield* directBrowser.isConnected) === true, + "Expected browser to be connected", + ); + + const page = yield* directBrowser.newPage(); + const content = yield* page.evaluate(() => "cdp works"); + assert(content === "cdp works", "Expected content to be cdp works"); + }), + ); + + it.scoped( + "should connect via CDP and close automatically with scope", + Effect.fn(function* () { + const playwright = yield* Playwright; + + // 1. Launch a browser that exposes CDP + const directBrowser = yield* playwright.launchScoped(chromium, { + args: [ + "--remote-debugging-port=9223", + "--remote-debugging-address=127.0.0.1", + ], + }); + + // 2. Connect to it via CDP using connectCDPScoped + yield* Effect.gen(function* () { + const browser = yield* playwright.connectCDPScoped( + "http://127.0.0.1:9223", + ); + const isConnected = yield* browser.isConnected; + assert(isConnected === true, "Expected connected true"); + }).pipe(Effect.scoped); + + // 3. After scope, connection should be closed + // We can't easily check the CDP browser object as it's out of scope + // but we can check if the direct browser is still connected + assert( + (yield* directBrowser.isConnected) === true, + "Expected browser to still be connected", + ); + + const page = yield* directBrowser.newPage(); + const content = yield* page.evaluate(() => "cdp works after cdp closed"); + assert( + content === "cdp works after cdp closed", + "Expected content to be correct", + ); }), ); }); diff --git a/src/playwright.ts b/src/playwright.ts index dc421c8..12fda7f 100644 --- a/src/playwright.ts +++ b/src/playwright.ts @@ -71,18 +71,19 @@ export interface PlaywrightService { /** * Connects to a browser instance via Chrome DevTools Protocol (CDP). * - * Unlike {@link launchScoped}, this method does **not** close the browser when the - * scope is closed. It is the caller's responsibility to manage the browser's + * Unlike {@link connectCDPScoped}, this method does **not** close the connection when the + * scope is closed. It is the caller's responsibility to manage the connection's * lifecycle. * - * If you want to close the browser using a scope simply add a finalizer: + * If you want to close the connection using a scope simply add a finalizer: * * ```ts * import { Effect } from "effect"; * import { Playwright } from "effect-playwright"; * * const program = Effect.gen(function* () { - * const browser = yield* Playwright.connectCDP(cdpUrl); + * const playwright = yield* Playwright; + * const browser = yield* playwright.connectCDP(cdpUrl); * yield* Effect.addFinalizer(() => browser.close.pipe(Effect.ignore)); * }); * @@ -97,6 +98,39 @@ export interface PlaywrightService { cdpUrl: string, options?: ConnectOverCDPOptions, ) => Effect.Effect; + /** + * Connects to a browser instance via Chrome DevTools Protocol (CDP) managed by a Scope. + * + * This method automatically closes the connection when the scope is closed. + * + * Note that closing a CDP connection does **not** close the browser instance itself, + * only the CDP connection. + * + * ```ts + * import { Effect } from "effect"; + * import { Playwright } from "effect-playwright"; + * + * const program = Effect.gen(function* () { + * const playwright = yield* Playwright; + * const browser = yield* playwright.connectCDPScoped(cdpUrl); + * // Connection will be closed automatically when scope closes + * }); + * + * await Effect.runPromise(program); + * ``` + * + * @param cdpUrl - The CDP URL to connect to. + * @param options - Optional options for connecting to the CDP URL. + * @since 0.1.0 + */ + connectCDPScoped: ( + cdpUrl: string, + options?: ConnectOverCDPOptions, + ) => Effect.Effect< + typeof PlaywrightBrowser.Service, + PlaywrightError, + Scope.Scope + >; } const launch: ( @@ -114,6 +148,19 @@ const launch: ( return browser; }); +const connectCDP: ( + cdpUrl: string, + options?: ConnectOverCDPOptions, +) => Effect.Effect = + Effect.fn(function* (cdpUrl: string, options?: ConnectOverCDPOptions) { + const browser = yield* Effect.tryPromise({ + try: () => chromium.connectOverCDP(cdpUrl, options), + catch: wrapError, + }); + + return PlaywrightBrowser.make(browser); + }); + export class Playwright extends Context.Tag( "effect-playwright/index/Playwright", )() { @@ -122,26 +169,14 @@ export class Playwright extends Context.Tag( */ static readonly layer = Layer.succeed(Playwright, { launch, - launchScoped: Effect.fn(function* ( - browserType: BrowserType, - options?: LaunchOptions, - ) { - const browser = yield* launch(browserType, options); - - // cleanup - yield* Effect.addFinalizer(() => browser.close.pipe(Effect.ignore)); - return browser; - }), - connectCDP: Effect.fn(function* ( - cdpUrl: string, - options?: ConnectOverCDPOptions, - ) { - const browser = yield* Effect.tryPromise({ - try: () => chromium.connectOverCDP(cdpUrl, options), - catch: wrapError, - }); - - return PlaywrightBrowser.make(browser); - }), + launchScoped: (browserType, options) => + Effect.acquireRelease(launch(browserType, options), (browser) => + browser.close.pipe(Effect.ignore), + ), + connectCDP, + connectCDPScoped: (cdpUrl, options) => + Effect.acquireRelease(connectCDP(cdpUrl, options), (browser) => + browser.close.pipe(Effect.ignore), + ), }); } From 3fed94f4a2d54c7300f26905bef4a962c32e5e39 Mon Sep 17 00:00:00 2001 From: Filip Weiss Date: Wed, 14 Jan 2026 22:45:21 +0100 Subject: [PATCH 2/4] improve test wording for cdp tests --- src/playwright.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/playwright.test.ts b/src/playwright.test.ts index 7f479c1..bc6e15f 100644 --- a/src/playwright.test.ts +++ b/src/playwright.test.ts @@ -116,12 +116,12 @@ layer(Playwright.layer)("Playwright", (it) => { assert( (yield* directBrowser.isConnected) === true, - "Expected browser to be connected", + "Expected direct browser to be still connected", ); const page = yield* directBrowser.newPage(); - const content = yield* page.evaluate(() => "cdp works"); - assert(content === "cdp works", "Expected content to be cdp works"); + const content = yield* page.evaluate(() => "eval works"); + assert(content === "eval works", "Expected content to be eval works"); }), ); @@ -156,9 +156,9 @@ layer(Playwright.layer)("Playwright", (it) => { ); const page = yield* directBrowser.newPage(); - const content = yield* page.evaluate(() => "cdp works after cdp closed"); + const content = yield* page.evaluate(() => "eval after cdp closed"); assert( - content === "cdp works after cdp closed", + content === "eval after cdp closed", "Expected content to be correct", ); }), From 8f76c7e3024117477a13f471d0c5a1ce2e18d31c Mon Sep 17 00:00:00 2001 From: Filip Weiss Date: Wed, 14 Jan 2026 22:51:35 +0100 Subject: [PATCH 3/4] remove redundant cdp test to avoid race condition --- src/playwright.test.ts | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/playwright.test.ts b/src/playwright.test.ts index bc6e15f..e9b74a0 100644 --- a/src/playwright.test.ts +++ b/src/playwright.test.ts @@ -70,31 +70,6 @@ layer(Playwright.layer)("Playwright", (it) => { }), ); - it.scoped( - "should connect via CDP", - Effect.fn(function* () { - const playwright = yield* Playwright; - - // 1. Launch a browser that exposes CDP - yield* playwright.launchScoped(chromium, { - args: [ - "--remote-debugging-port=9222", - "--remote-debugging-address=127.0.0.1", - ], - }); - - // 2. Connect to it via CDP - const browser = yield* playwright.connectCDP("http://127.0.0.1:9222"); - - // 3. Cleanup connection (doesn't close the browser itself, but closes the CDP connection) - yield* Effect.addFinalizer(() => browser.close.pipe(Effect.ignore)); - - const page = yield* browser.newPage(); - const content = yield* page.evaluate(() => "cdp works"); - assert(content === "cdp works", "Expected content to be cdp works"); - }), - ); - it.scoped( "should connect via CDP (confirm browser.close only closes CDP connection)", Effect.fn(function* () { From 948685955304020318b0c9ad7fe2d2245e003da5 Mon Sep 17 00:00:00 2001 From: Filip Weiss Date: Thu, 15 Jan 2026 11:04:19 +0100 Subject: [PATCH 4/4] correct connectCDPScoped "since" docstring --- src/playwright.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playwright.ts b/src/playwright.ts index 12fda7f..2a675c1 100644 --- a/src/playwright.ts +++ b/src/playwright.ts @@ -121,7 +121,7 @@ export interface PlaywrightService { * * @param cdpUrl - The CDP URL to connect to. * @param options - Optional options for connecting to the CDP URL. - * @since 0.1.0 + * @since 0.1.1 */ connectCDPScoped: ( cdpUrl: string,