From 41a32a7f51ff7f2c8458d40c8751cd0b4d0ff9ff Mon Sep 17 00:00:00 2001 From: Filip Weiss Date: Mon, 19 Jan 2026 20:50:59 +0100 Subject: [PATCH] implement frame service & methods --- src/common.ts | 24 +----- src/frame.test.ts | 60 +++++++++++++++ src/frame.ts | 187 ++++++++++++++++++++++++++++++++++++++++++++++ src/page.ts | 4 +- 4 files changed, 253 insertions(+), 22 deletions(-) create mode 100644 src/frame.test.ts create mode 100644 src/frame.ts 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 Title
Hello 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";