Skip to content
Merged
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
73 changes: 71 additions & 2 deletions src/playwright.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -66,7 +66,76 @@ 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 (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 direct browser to be still connected",
);

const page = yield* directBrowser.newPage();
const content = yield* page.evaluate(() => "eval works");
assert(content === "eval works", "Expected content to be eval 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(() => "eval after cdp closed");
assert(
content === "eval after cdp closed",
"Expected content to be correct",
);
}),
);
});
85 changes: 60 additions & 25 deletions src/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
* });
*
Expand All @@ -97,6 +98,39 @@ export interface PlaywrightService {
cdpUrl: string,
options?: ConnectOverCDPOptions,
) => Effect.Effect<typeof PlaywrightBrowser.Service, PlaywrightError>;
/**
* 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.1
*/
connectCDPScoped: (
cdpUrl: string,
options?: ConnectOverCDPOptions,
) => Effect.Effect<
typeof PlaywrightBrowser.Service,
PlaywrightError,
Scope.Scope
>;
}

const launch: (
Expand All @@ -114,6 +148,19 @@ const launch: (
return browser;
});

const connectCDP: (
cdpUrl: string,
options?: ConnectOverCDPOptions,
) => Effect.Effect<typeof PlaywrightBrowser.Service, PlaywrightError> =
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",
)<Playwright, PlaywrightService>() {
Expand All @@ -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));
Comment thread
fiws marked this conversation as resolved.
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),
),
});
}