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
24 changes: 3 additions & 21 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <A>(
f: (frame: Frame) => Promise<A>,
) => Effect.Effect<A, PlaywrightError>;
}> {
static make(frame: Frame): PlaywrightFrame {
const use = useHelper(frame);

return new PlaywrightFrame({ use });
}
}

/**
* @category model
* @since 0.1.2
Expand All @@ -42,7 +24,7 @@ export class PlaywrightRequest extends Data.TaggedClass("PlaywrightRequest")<{
PlaywrightError
>;
failure: () => Option.Option<NonNullable<ReturnType<Request["failure"]>>>;
frame: Effect.Effect<PlaywrightFrame>;
frame: Effect.Effect<PlaywrightFrameService>;
headerValue: (
name: string,
) => Effect.Effect<Option.Option<string>, PlaywrightError>;
Expand Down Expand Up @@ -128,7 +110,7 @@ export class PlaywrightResponse extends Data.TaggedClass("PlaywrightResponse")<{
Awaited<ReturnType<Response["finished"]>>,
PlaywrightError
>;
frame: Effect.Effect<PlaywrightFrame>;
frame: Effect.Effect<PlaywrightFrameService>;
fromServiceWorker: Effect.Effect<boolean>;
headers: Effect.Effect<ReturnType<Response["headers"]>>;
headersArray: Effect.Effect<
Expand Down
60 changes: 60 additions & 0 deletions src/frame.test.ts
Original file line number Diff line number Diff line change
@@ -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 =
"<html><head><title>Frame Title</title></head><body><div id='target'>Hello from Frame</div></body></html>";
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),
);
});
187 changes: 187 additions & 0 deletions src/frame.ts
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
StefanWerW marked this conversation as resolved.
/**
* Navigates the frame to the given URL.
*
* @see {@link Frame.goto}
* @since 0.1.3
*/
readonly goto: (
url: string,
options?: Parameters<Frame["goto"]>[1],
) => Effect.Effect<void, PlaywrightError>;
/**
* Waits for the frame to navigate to the given URL.
*
* @see {@link Frame.waitForURL}
* @since 0.1.3
*/
readonly waitForURL: (
url: Parameters<Frame["waitForURL"]>[0],
options?: Parameters<Frame["waitForURL"]>[1],
) => Effect.Effect<void, PlaywrightError>;
/**
* Evaluates a function in the context of the frame.
*
* @see {@link Frame.evaluate}
* @since 0.1.3
*/
readonly evaluate: <R, Arg = void>(
pageFunction: PageFunction<Arg, R>,
arg?: Arg,
) => Effect.Effect<R, PlaywrightError>;
/**
* Returns the frame title.
*
* @see {@link Frame.title}
* @since 0.1.3
*/
readonly title: Effect.Effect<string, PlaywrightError>;
/**
* 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: <T>(
f: (frame: Frame) => Promise<T>,
) => Effect.Effect<T, PlaywrightError>;
/**
* Returns a locator for the given selector.
*
* @see {@link Frame.locator}
* @since 0.1.3
*/
readonly locator: (
selector: string,
options?: Parameters<Frame["locator"]>[1],
) => typeof PlaywrightLocator.Service;
/**
* Returns a locator that matches the given role.
*
* @see {@link Frame.getByRole}
* @since 0.1.3
*/
readonly getByRole: (
role: Parameters<Frame["getByRole"]>[0],
options?: Parameters<Frame["getByRole"]>[1],
) => typeof PlaywrightLocator.Service;
/**
* Returns a locator that matches the given text.
*
* @see {@link Frame.getByText}
* @since 0.1.3
*/
readonly getByText: (
text: Parameters<Frame["getByText"]>[0],
options?: Parameters<Frame["getByText"]>[1],
) => typeof PlaywrightLocator.Service;
/**
* Returns a locator that matches the given label.
*
* @see {@link Frame.getByLabel}
* @since 0.1.3
*/
readonly getByLabel: (
label: Parameters<Frame["getByLabel"]>[0],
options?: Parameters<Frame["getByLabel"]>[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<Frame["getByTestId"]>[0],
) => typeof PlaywrightLocator.Service;

/**
* Returns the current URL of the frame.
*
* @see {@link Frame.url}
* @since 0.1.3
*/
readonly url: Effect.Effect<string, PlaywrightError>;

/**
* Returns the full HTML contents of the frame, including the doctype.
*
* @see {@link Frame.content}
* @since 0.1.3
*/
readonly content: Effect.Effect<string, PlaywrightError>;

/**
* Returns the frame name.
*
* @see {@link Frame.name}
* @since 0.1.3
*/
readonly name: Effect.Effect<string>;

/**
* 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<Frame["click"]>[1],
) => Effect.Effect<void, PlaywrightError>;
}

/**
* @category tag
* @since 0.1.2
*/
export class PlaywrightFrame extends Context.Tag(
"effect-playwright/PlaywrightFrame",
)<PlaywrightFrame, PlaywrightFrameService>() {
/**
* 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: <R, Arg>(f: PageFunction<Arg, R>, arg?: Arg) =>
use((frame) => frame.evaluate<R, Arg>(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)),
});
}
}
4 changes: 3 additions & 1 deletion src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down