diff --git a/src/common.ts b/src/common.ts
index ce141a7..f4345e7 100644
--- a/src/common.ts
+++ b/src/common.ts
@@ -4,34 +4,16 @@ import type {
Download,
ElementHandle,
FileChooser,
- Frame,
Request,
Response,
Worker,
} from "playwright-core";
import type { PlaywrightError } from "./errors";
+import { PlaywrightFrame, type PlaywrightFrameService } from "./frame";
import { PlaywrightPage, type PlaywrightPageService } from "./page";
import type { PageFunction } from "./playwright-types";
import { useHelper } from "./utils";
-// TODO: wrap frame methods
-
-/**
- * @category model
- * @since 0.1.2
- */
-export class PlaywrightFrame extends Data.TaggedClass("PlaywrightFrame")<{
- use: (
- f: (frame: Frame) => Promise,
- ) => Effect.Effect;
-}> {
- static make(frame: Frame): PlaywrightFrame {
- const use = useHelper(frame);
-
- return new PlaywrightFrame({ use });
- }
-}
-
/**
* @category model
* @since 0.1.2
@@ -42,7 +24,7 @@ export class PlaywrightRequest extends Data.TaggedClass("PlaywrightRequest")<{
PlaywrightError
>;
failure: () => Option.Option>>;
- frame: Effect.Effect;
+ frame: Effect.Effect;
headerValue: (
name: string,
) => Effect.Effect, PlaywrightError>;
@@ -128,7 +110,7 @@ export class PlaywrightResponse extends Data.TaggedClass("PlaywrightResponse")<{
Awaited>,
PlaywrightError
>;
- frame: Effect.Effect;
+ frame: Effect.Effect;
fromServiceWorker: Effect.Effect;
headers: Effect.Effect>;
headersArray: Effect.Effect<
diff --git a/src/frame.test.ts b/src/frame.test.ts
new file mode 100644
index 0000000..59624f1
--- /dev/null
+++ b/src/frame.test.ts
@@ -0,0 +1,60 @@
+import { assert, layer } from "@effect/vitest";
+import { Effect } from "effect";
+import { chromium } from "playwright-core";
+import { PlaywrightBrowser } from "./browser";
+import { PlaywrightEnvironment } from "./experimental";
+import { PlaywrightFrame } from "./frame";
+
+layer(PlaywrightEnvironment.layer(chromium))("PlaywrightFrame", (it) => {
+ it.scoped("should wrap frame methods", () =>
+ Effect.gen(function* () {
+ const browser = yield* PlaywrightBrowser;
+ const page = yield* browser.newPage();
+
+ // Setup a page with an iframe
+ yield* page.evaluate(() => {
+ const iframe = document.createElement("iframe");
+ iframe.name = "test-frame";
+ iframe.srcdoc =
+ "Frame TitleHello from Frame
";
+ document.body.appendChild(iframe);
+ });
+
+ // Get the frame
+ const frameService = yield* page.use(async (p) => {
+ let frame = p.frames().find((f) => f.name() === "test-frame");
+ if (!frame) {
+ // Wait a bit if not found immediately (though srcdoc is sync-ish, iframe loading is async)
+ await p.waitForLoadState("networkidle");
+ frame = p.frames().find((f) => f.name() === "test-frame");
+ }
+ if (!frame) throw new Error("Frame not found");
+ return PlaywrightFrame.make(frame);
+ });
+
+ // Test title
+ const title = yield* frameService.title;
+ assert.strictEqual(title, "Frame Title");
+
+ // Test content
+ const content = yield* frameService.content;
+ assert.isTrue(content.includes("Hello from Frame"));
+
+ // Test evaluate
+ const result = yield* frameService.evaluate(() => 1 + 1);
+ assert.strictEqual(result, 2);
+
+ // Test locator
+ const text = yield* frameService.locator("#target").textContent();
+ assert.strictEqual(text, "Hello from Frame");
+
+ // Test getByText
+ const byText = yield* frameService.getByText("Hello from Frame").count;
+ assert.strictEqual(byText, 1);
+
+ // Test name
+ const name = yield* frameService.name;
+ assert.strictEqual(name, "test-frame");
+ }).pipe(PlaywrightEnvironment.withBrowser),
+ );
+});
diff --git a/src/frame.ts b/src/frame.ts
new file mode 100644
index 0000000..6d36b40
--- /dev/null
+++ b/src/frame.ts
@@ -0,0 +1,187 @@
+import { Context, Effect } from "effect";
+import type { Frame } from "playwright-core";
+import type { PlaywrightError } from "./errors";
+import { PlaywrightLocator } from "./locator";
+import type { PageFunction } from "./playwright-types";
+import { useHelper } from "./utils";
+
+/**
+ * @category model
+ * @since 0.1.2
+ */
+export interface PlaywrightFrameService {
+ /**
+ * Navigates the frame to the given URL.
+ *
+ * @see {@link Frame.goto}
+ * @since 0.1.3
+ */
+ readonly goto: (
+ url: string,
+ options?: Parameters[1],
+ ) => Effect.Effect;
+ /**
+ * Waits for the frame to navigate to the given URL.
+ *
+ * @see {@link Frame.waitForURL}
+ * @since 0.1.3
+ */
+ readonly waitForURL: (
+ url: Parameters[0],
+ options?: Parameters[1],
+ ) => Effect.Effect;
+ /**
+ * Evaluates a function in the context of the frame.
+ *
+ * @see {@link Frame.evaluate}
+ * @since 0.1.3
+ */
+ readonly evaluate: (
+ pageFunction: PageFunction,
+ arg?: Arg,
+ ) => Effect.Effect;
+ /**
+ * Returns the frame title.
+ *
+ * @see {@link Frame.title}
+ * @since 0.1.3
+ */
+ readonly title: Effect.Effect;
+ /**
+ * A generic utility to execute any promise-based method on the underlying Playwright `Frame`.
+ * Can be used to access any Frame functionality not directly exposed by this service.
+ *
+ * @see {@link Frame}
+ * @since 0.1.2
+ */
+ readonly use: (
+ f: (frame: Frame) => Promise,
+ ) => Effect.Effect;
+ /**
+ * Returns a locator for the given selector.
+ *
+ * @see {@link Frame.locator}
+ * @since 0.1.3
+ */
+ readonly locator: (
+ selector: string,
+ options?: Parameters[1],
+ ) => typeof PlaywrightLocator.Service;
+ /**
+ * Returns a locator that matches the given role.
+ *
+ * @see {@link Frame.getByRole}
+ * @since 0.1.3
+ */
+ readonly getByRole: (
+ role: Parameters[0],
+ options?: Parameters[1],
+ ) => typeof PlaywrightLocator.Service;
+ /**
+ * Returns a locator that matches the given text.
+ *
+ * @see {@link Frame.getByText}
+ * @since 0.1.3
+ */
+ readonly getByText: (
+ text: Parameters[0],
+ options?: Parameters[1],
+ ) => typeof PlaywrightLocator.Service;
+ /**
+ * Returns a locator that matches the given label.
+ *
+ * @see {@link Frame.getByLabel}
+ * @since 0.1.3
+ */
+ readonly getByLabel: (
+ label: Parameters[0],
+ options?: Parameters[1],
+ ) => typeof PlaywrightLocator.Service;
+ /**
+ * Returns a locator that matches the given test id.
+ *
+ * @see {@link Frame.getByTestId}
+ * @since 0.1.3
+ */
+ readonly getByTestId: (
+ testId: Parameters[0],
+ ) => typeof PlaywrightLocator.Service;
+
+ /**
+ * Returns the current URL of the frame.
+ *
+ * @see {@link Frame.url}
+ * @since 0.1.3
+ */
+ readonly url: Effect.Effect;
+
+ /**
+ * Returns the full HTML contents of the frame, including the doctype.
+ *
+ * @see {@link Frame.content}
+ * @since 0.1.3
+ */
+ readonly content: Effect.Effect;
+
+ /**
+ * Returns the frame name.
+ *
+ * @see {@link Frame.name}
+ * @since 0.1.3
+ */
+ readonly name: Effect.Effect;
+
+ /**
+ * Clicks an element matching the given selector.
+ *
+ * @deprecated Use {@link PlaywrightFrameService.locator} to create a locator and then call `click` on it instead.
+ * @see {@link Frame.click}
+ * @since 0.1.3
+ * @category deprecated
+ */
+ readonly click: (
+ selector: string,
+ options?: Parameters[1],
+ ) => Effect.Effect;
+}
+
+/**
+ * @category tag
+ * @since 0.1.2
+ */
+export class PlaywrightFrame extends Context.Tag(
+ "effect-playwright/PlaywrightFrame",
+)() {
+ /**
+ * Creates a `PlaywrightFrame` from a Playwright `Frame` instance.
+ *
+ * @param frame - The Playwright `Frame` instance to wrap.
+ * @since 0.1.2
+ */
+ static make(frame: Frame): PlaywrightFrameService {
+ const use = useHelper(frame);
+
+ return PlaywrightFrame.of({
+ goto: (url, options) => use((f) => f.goto(url, options)),
+ waitForURL: (url, options) => use((f) => f.waitForURL(url, options)),
+ evaluate: (f: PageFunction, arg?: Arg) =>
+ use((frame) => frame.evaluate(f, arg as Arg)),
+ title: use((f) => f.title()),
+ use,
+ locator: (selector, options) =>
+ PlaywrightLocator.make(frame.locator(selector, options)),
+ getByRole: (role, options) =>
+ PlaywrightLocator.make(frame.getByRole(role, options)),
+ getByText: (text, options) =>
+ PlaywrightLocator.make(frame.getByText(text, options)),
+ getByLabel: (label, options) =>
+ PlaywrightLocator.make(frame.getByLabel(label, options)),
+ getByTestId: (testId) =>
+ PlaywrightLocator.make(frame.getByTestId(testId)),
+ url: Effect.sync(() => frame.url()),
+ content: use((f) => f.content()),
+ name: Effect.sync(() => frame.name()),
+ click: (selector, options) => use((f) => f.click(selector, options)),
+ });
+ }
+}
diff --git a/src/page.ts b/src/page.ts
index d279ab5..e6b466f 100644
--- a/src/page.ts
+++ b/src/page.ts
@@ -11,16 +11,18 @@ import type {
WebSocket,
Worker,
} from "playwright-core";
+
import {
PlaywrightDialog,
PlaywrightDownload,
PlaywrightFileChooser,
- PlaywrightFrame,
PlaywrightRequest,
PlaywrightResponse,
PlaywrightWorker,
} from "./common";
+
import type { PlaywrightError } from "./errors";
+import { PlaywrightFrame } from "./frame";
import { PlaywrightLocator } from "./locator";
import type { PageFunction, PatchedEvents } from "./playwright-types";
import { useHelper } from "./utils";