Skip to content

Commit 93df0b7

Browse files
authored
Merge pull request #10 from AzemaViator/feat/persistent-context-launch
Feat: persistent context launch
2 parents 2856e82 + bd09bef commit 93df0b7

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

src/playwright.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, layer } from "@effect/vitest";
22
import { Effect } from "effect";
33
import { Playwright } from "effect-playwright";
44
import { chromium } from "playwright-core";
5+
import type { PlaywrightBrowserContext } from "./browser-context";
56

67
layer(Playwright.layer)("Playwright", (it) => {
78
it.scoped("should launch a browser", () =>
@@ -38,6 +39,46 @@ layer(Playwright.layer)("Playwright", (it) => {
3839
}),
3940
);
4041

42+
it.scoped("should launch a persistent context", () =>
43+
Effect.gen(function* () {
44+
const playwright = yield* Playwright;
45+
const context = yield* playwright.launchPersistentContext(chromium, "");
46+
const page = yield* context.newPage;
47+
48+
yield* page.goto("data:text/html,<title>persistent-context</title>");
49+
const title = yield* page.title;
50+
51+
assert(title === "persistent-context", "Expected title to match");
52+
yield* context.close;
53+
}),
54+
);
55+
56+
it.scoped("should launch a persistent context and close with scope", () =>
57+
Effect.gen(function* () {
58+
const playwright = yield* Playwright;
59+
let capturedContext: typeof PlaywrightBrowserContext.Service | undefined;
60+
61+
yield* Effect.gen(function* () {
62+
const context = yield* playwright.launchPersistentContextScoped(
63+
chromium,
64+
"",
65+
);
66+
capturedContext = context;
67+
68+
const page = yield* context.newPage;
69+
const content = yield* page.evaluate(() => "scoped-persistent");
70+
assert(content === "scoped-persistent", "Expected content to match");
71+
}).pipe(Effect.scoped);
72+
73+
assert(capturedContext !== undefined, "Expected captured context");
74+
const error = yield* capturedContext.newPage.pipe(Effect.flip);
75+
assert(
76+
error._tag === "PlaywrightError",
77+
"Expected failure after scoped close",
78+
);
79+
}),
80+
);
81+
4182
it.scoped("should fail to launch a browser with invalid path", () =>
4283
Effect.gen(function* () {
4384
const playwright = yield* Playwright;

src/playwright.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import {
66
} from "playwright-core";
77

88
import { type LaunchOptions, PlaywrightBrowser } from "./browser";
9+
import { PlaywrightBrowserContext } from "./browser-context";
910
import { type PlaywrightError, wrapError } from "./errors";
1011

12+
type LaunchPersistentContextOptions = Parameters<
13+
BrowserType["launchPersistentContext"]
14+
>[1];
15+
1116
/**
1217
* @category model
1318
* @since 0.1.0
@@ -72,6 +77,105 @@ export interface PlaywrightService {
7277
PlaywrightError,
7378
Scope.Scope
7479
>;
80+
/**
81+
* Launches a persistent browser context.
82+
*
83+
* Unlike {@link launchPersistentContextScoped}, this method does **not** close the
84+
* context automatically when scope is closed. You are responsible for closing it.
85+
*
86+
* This launches a browser with a persistent profile under `userDataDir` and returns
87+
* the single persistent context for that browser.
88+
*
89+
* Closing this context also closes the underlying browser process.
90+
*
91+
* ```ts
92+
* import { Effect } from "effect";
93+
* import { Playwright } from "effect-playwright";
94+
* import { chromium } from "playwright-core";
95+
*
96+
* const program = Effect.gen(function* () {
97+
* const playwright = yield* Playwright;
98+
* const context = yield* playwright.launchPersistentContext(
99+
* chromium,
100+
* "./.playwright-profile",
101+
* );
102+
*
103+
* const page = yield* context.newPage;
104+
* yield* page.goto("https://example.com");
105+
*
106+
* // Closes the persistent context and browser process.
107+
* yield* context.close;
108+
* });
109+
*
110+
* await Effect.runPromise(program);
111+
* ```
112+
*
113+
* If you call this non-scoped variant inside a scope, add a finalizer for cleanup:
114+
*
115+
* ```ts
116+
* const program = Effect.gen(function* () {
117+
* const playwright = yield* Playwright;
118+
* const context = yield* playwright.launchPersistentContext(
119+
* chromium,
120+
* "./.playwright-profile",
121+
* );
122+
*
123+
* yield* Effect.addFinalizer(() => context.close.pipe(Effect.ignore));
124+
* });
125+
*
126+
* await Effect.runPromise(program.pipe(Effect.scoped));
127+
* ```
128+
*
129+
* @param browserType - The browser type to launch (e.g. chromium, firefox, webkit).
130+
* @param userDataDir - Directory used for persistent browser profile data. Pass `""` for a temporary profile directory.
131+
* @param options - Optional persistent context launch options.
132+
* @since 0.2.4
133+
*/
134+
launchPersistentContext: (
135+
browserType: BrowserType,
136+
userDataDir: string,
137+
options?: LaunchPersistentContextOptions,
138+
) => Effect.Effect<typeof PlaywrightBrowserContext.Service, PlaywrightError>;
139+
/**
140+
* Launches a persistent browser context managed by a Scope.
141+
*
142+
* This automatically closes the persistent context (and therefore the browser process)
143+
* when the scope is closed.
144+
*
145+
* ```ts
146+
* import { Effect } from "effect";
147+
* import { Playwright } from "effect-playwright";
148+
* import { chromium } from "playwright-core";
149+
*
150+
* const program = Effect.gen(function* () {
151+
* const playwright = yield* Playwright;
152+
* const context = yield* playwright.launchPersistentContextScoped(
153+
* chromium,
154+
* "./.playwright-profile",
155+
* );
156+
*
157+
* const page = yield* context.newPage;
158+
* yield* page.goto("https://example.com");
159+
* // Context/browser cleanup is automatic when scope closes.
160+
* }).pipe(Effect.scoped);
161+
*
162+
* await Effect.runPromise(program);
163+
* ```
164+
*
165+
* @param browserType - The browser type to launch (e.g. chromium, firefox, webkit).
166+
* @param userDataDir - Directory used for persistent browser profile data. Pass `""` for a temporary profile directory.
167+
* @param options - Optional persistent context launch options.
168+
* @since 0.2.4
169+
*/
170+
launchPersistentContextScoped: (
171+
browserType: BrowserType,
172+
userDataDir: string,
173+
options?: LaunchPersistentContextOptions,
174+
) => Effect.Effect<
175+
typeof PlaywrightBrowserContext.Service,
176+
PlaywrightError,
177+
Scope.Scope
178+
>;
75179
/**
76180
* Connects to a browser instance via Chrome DevTools Protocol (CDP).
77181
*
@@ -163,6 +267,24 @@ const connectCDP: (
163267
return PlaywrightBrowser.make(browser);
164268
});
165269

270+
const launchPersistentContext: (
271+
browserType: BrowserType,
272+
userDataDir: string,
273+
options?: LaunchPersistentContextOptions,
274+
) => Effect.Effect<typeof PlaywrightBrowserContext.Service, PlaywrightError> =
275+
Effect.fn(function* (
276+
browserType: BrowserType,
277+
userDataDir: string,
278+
options?: LaunchPersistentContextOptions,
279+
) {
280+
const rawContext = yield* Effect.tryPromise({
281+
try: () => browserType.launchPersistentContext(userDataDir, options),
282+
catch: wrapError,
283+
});
284+
285+
return PlaywrightBrowserContext.make(rawContext);
286+
});
287+
166288
/**
167289
* @category tag
168290
* @since 0.1.0
@@ -179,6 +301,12 @@ export class Playwright extends Context.Tag(
179301
Effect.acquireRelease(launch(browserType, options), (browser) =>
180302
browser.close.pipe(Effect.ignore),
181303
),
304+
launchPersistentContext,
305+
launchPersistentContextScoped: (browserType, userDataDir, options) =>
306+
Effect.acquireRelease(
307+
launchPersistentContext(browserType, userDataDir, options),
308+
(context) => context.close.pipe(Effect.ignore),
309+
),
182310
connectCDP,
183311
connectCDPScoped: (cdpUrl, options) =>
184312
Effect.acquireRelease(connectCDP(cdpUrl, options), (browser) =>

0 commit comments

Comments
 (0)