Skip to content

Commit 41a32a7

Browse files
committed
implement frame service & methods
1 parent 6b63a5e commit 41a32a7

File tree

4 files changed

+253
-22
lines changed

4 files changed

+253
-22
lines changed

src/common.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,16 @@ import type {
44
Download,
55
ElementHandle,
66
FileChooser,
7-
Frame,
87
Request,
98
Response,
109
Worker,
1110
} from "playwright-core";
1211
import type { PlaywrightError } from "./errors";
12+
import { PlaywrightFrame, type PlaywrightFrameService } from "./frame";
1313
import { PlaywrightPage, type PlaywrightPageService } from "./page";
1414
import type { PageFunction } from "./playwright-types";
1515
import { useHelper } from "./utils";
1616

17-
// TODO: wrap frame methods
18-
19-
/**
20-
* @category model
21-
* @since 0.1.2
22-
*/
23-
export class PlaywrightFrame extends Data.TaggedClass("PlaywrightFrame")<{
24-
use: <A>(
25-
f: (frame: Frame) => Promise<A>,
26-
) => Effect.Effect<A, PlaywrightError>;
27-
}> {
28-
static make(frame: Frame): PlaywrightFrame {
29-
const use = useHelper(frame);
30-
31-
return new PlaywrightFrame({ use });
32-
}
33-
}
34-
3517
/**
3618
* @category model
3719
* @since 0.1.2
@@ -42,7 +24,7 @@ export class PlaywrightRequest extends Data.TaggedClass("PlaywrightRequest")<{
4224
PlaywrightError
4325
>;
4426
failure: () => Option.Option<NonNullable<ReturnType<Request["failure"]>>>;
45-
frame: Effect.Effect<PlaywrightFrame>;
27+
frame: Effect.Effect<PlaywrightFrameService>;
4628
headerValue: (
4729
name: string,
4830
) => Effect.Effect<Option.Option<string>, PlaywrightError>;
@@ -128,7 +110,7 @@ export class PlaywrightResponse extends Data.TaggedClass("PlaywrightResponse")<{
128110
Awaited<ReturnType<Response["finished"]>>,
129111
PlaywrightError
130112
>;
131-
frame: Effect.Effect<PlaywrightFrame>;
113+
frame: Effect.Effect<PlaywrightFrameService>;
132114
fromServiceWorker: Effect.Effect<boolean>;
133115
headers: Effect.Effect<ReturnType<Response["headers"]>>;
134116
headersArray: Effect.Effect<

src/frame.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { assert, layer } from "@effect/vitest";
2+
import { Effect } from "effect";
3+
import { chromium } from "playwright-core";
4+
import { PlaywrightBrowser } from "./browser";
5+
import { PlaywrightEnvironment } from "./experimental";
6+
import { PlaywrightFrame } from "./frame";
7+
8+
layer(PlaywrightEnvironment.layer(chromium))("PlaywrightFrame", (it) => {
9+
it.scoped("should wrap frame methods", () =>
10+
Effect.gen(function* () {
11+
const browser = yield* PlaywrightBrowser;
12+
const page = yield* browser.newPage();
13+
14+
// Setup a page with an iframe
15+
yield* page.evaluate(() => {
16+
const iframe = document.createElement("iframe");
17+
iframe.name = "test-frame";
18+
iframe.srcdoc =
19+
"<html><head><title>Frame Title</title></head><body><div id='target'>Hello from Frame</div></body></html>";
20+
document.body.appendChild(iframe);
21+
});
22+
23+
// Get the frame
24+
const frameService = yield* page.use(async (p) => {
25+
let frame = p.frames().find((f) => f.name() === "test-frame");
26+
if (!frame) {
27+
// Wait a bit if not found immediately (though srcdoc is sync-ish, iframe loading is async)
28+
await p.waitForLoadState("networkidle");
29+
frame = p.frames().find((f) => f.name() === "test-frame");
30+
}
31+
if (!frame) throw new Error("Frame not found");
32+
return PlaywrightFrame.make(frame);
33+
});
34+
35+
// Test title
36+
const title = yield* frameService.title;
37+
assert.strictEqual(title, "Frame Title");
38+
39+
// Test content
40+
const content = yield* frameService.content;
41+
assert.isTrue(content.includes("Hello from Frame"));
42+
43+
// Test evaluate
44+
const result = yield* frameService.evaluate(() => 1 + 1);
45+
assert.strictEqual(result, 2);
46+
47+
// Test locator
48+
const text = yield* frameService.locator("#target").textContent();
49+
assert.strictEqual(text, "Hello from Frame");
50+
51+
// Test getByText
52+
const byText = yield* frameService.getByText("Hello from Frame").count;
53+
assert.strictEqual(byText, 1);
54+
55+
// Test name
56+
const name = yield* frameService.name;
57+
assert.strictEqual(name, "test-frame");
58+
}).pipe(PlaywrightEnvironment.withBrowser),
59+
);
60+
});

src/frame.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { Context, Effect } from "effect";
2+
import type { Frame } from "playwright-core";
3+
import type { PlaywrightError } from "./errors";
4+
import { PlaywrightLocator } from "./locator";
5+
import type { PageFunction } from "./playwright-types";
6+
import { useHelper } from "./utils";
7+
8+
/**
9+
* @category model
10+
* @since 0.1.2
11+
*/
12+
export interface PlaywrightFrameService {
13+
/**
14+
* Navigates the frame to the given URL.
15+
*
16+
* @see {@link Frame.goto}
17+
* @since 0.1.3
18+
*/
19+
readonly goto: (
20+
url: string,
21+
options?: Parameters<Frame["goto"]>[1],
22+
) => Effect.Effect<void, PlaywrightError>;
23+
/**
24+
* Waits for the frame to navigate to the given URL.
25+
*
26+
* @see {@link Frame.waitForURL}
27+
* @since 0.1.3
28+
*/
29+
readonly waitForURL: (
30+
url: Parameters<Frame["waitForURL"]>[0],
31+
options?: Parameters<Frame["waitForURL"]>[1],
32+
) => Effect.Effect<void, PlaywrightError>;
33+
/**
34+
* Evaluates a function in the context of the frame.
35+
*
36+
* @see {@link Frame.evaluate}
37+
* @since 0.1.3
38+
*/
39+
readonly evaluate: <R, Arg = void>(
40+
pageFunction: PageFunction<Arg, R>,
41+
arg?: Arg,
42+
) => Effect.Effect<R, PlaywrightError>;
43+
/**
44+
* Returns the frame title.
45+
*
46+
* @see {@link Frame.title}
47+
* @since 0.1.3
48+
*/
49+
readonly title: Effect.Effect<string, PlaywrightError>;
50+
/**
51+
* A generic utility to execute any promise-based method on the underlying Playwright `Frame`.
52+
* Can be used to access any Frame functionality not directly exposed by this service.
53+
*
54+
* @see {@link Frame}
55+
* @since 0.1.2
56+
*/
57+
readonly use: <T>(
58+
f: (frame: Frame) => Promise<T>,
59+
) => Effect.Effect<T, PlaywrightError>;
60+
/**
61+
* Returns a locator for the given selector.
62+
*
63+
* @see {@link Frame.locator}
64+
* @since 0.1.3
65+
*/
66+
readonly locator: (
67+
selector: string,
68+
options?: Parameters<Frame["locator"]>[1],
69+
) => typeof PlaywrightLocator.Service;
70+
/**
71+
* Returns a locator that matches the given role.
72+
*
73+
* @see {@link Frame.getByRole}
74+
* @since 0.1.3
75+
*/
76+
readonly getByRole: (
77+
role: Parameters<Frame["getByRole"]>[0],
78+
options?: Parameters<Frame["getByRole"]>[1],
79+
) => typeof PlaywrightLocator.Service;
80+
/**
81+
* Returns a locator that matches the given text.
82+
*
83+
* @see {@link Frame.getByText}
84+
* @since 0.1.3
85+
*/
86+
readonly getByText: (
87+
text: Parameters<Frame["getByText"]>[0],
88+
options?: Parameters<Frame["getByText"]>[1],
89+
) => typeof PlaywrightLocator.Service;
90+
/**
91+
* Returns a locator that matches the given label.
92+
*
93+
* @see {@link Frame.getByLabel}
94+
* @since 0.1.3
95+
*/
96+
readonly getByLabel: (
97+
label: Parameters<Frame["getByLabel"]>[0],
98+
options?: Parameters<Frame["getByLabel"]>[1],
99+
) => typeof PlaywrightLocator.Service;
100+
/**
101+
* Returns a locator that matches the given test id.
102+
*
103+
* @see {@link Frame.getByTestId}
104+
* @since 0.1.3
105+
*/
106+
readonly getByTestId: (
107+
testId: Parameters<Frame["getByTestId"]>[0],
108+
) => typeof PlaywrightLocator.Service;
109+
110+
/**
111+
* Returns the current URL of the frame.
112+
*
113+
* @see {@link Frame.url}
114+
* @since 0.1.3
115+
*/
116+
readonly url: Effect.Effect<string, PlaywrightError>;
117+
118+
/**
119+
* Returns the full HTML contents of the frame, including the doctype.
120+
*
121+
* @see {@link Frame.content}
122+
* @since 0.1.3
123+
*/
124+
readonly content: Effect.Effect<string, PlaywrightError>;
125+
126+
/**
127+
* Returns the frame name.
128+
*
129+
* @see {@link Frame.name}
130+
* @since 0.1.3
131+
*/
132+
readonly name: Effect.Effect<string>;
133+
134+
/**
135+
* Clicks an element matching the given selector.
136+
*
137+
* @deprecated Use {@link PlaywrightFrameService.locator} to create a locator and then call `click` on it instead.
138+
* @see {@link Frame.click}
139+
* @since 0.1.3
140+
* @category deprecated
141+
*/
142+
readonly click: (
143+
selector: string,
144+
options?: Parameters<Frame["click"]>[1],
145+
) => Effect.Effect<void, PlaywrightError>;
146+
}
147+
148+
/**
149+
* @category tag
150+
* @since 0.1.2
151+
*/
152+
export class PlaywrightFrame extends Context.Tag(
153+
"effect-playwright/PlaywrightFrame",
154+
)<PlaywrightFrame, PlaywrightFrameService>() {
155+
/**
156+
* Creates a `PlaywrightFrame` from a Playwright `Frame` instance.
157+
*
158+
* @param frame - The Playwright `Frame` instance to wrap.
159+
* @since 0.1.2
160+
*/
161+
static make(frame: Frame): PlaywrightFrameService {
162+
const use = useHelper(frame);
163+
164+
return PlaywrightFrame.of({
165+
goto: (url, options) => use((f) => f.goto(url, options)),
166+
waitForURL: (url, options) => use((f) => f.waitForURL(url, options)),
167+
evaluate: <R, Arg>(f: PageFunction<Arg, R>, arg?: Arg) =>
168+
use((frame) => frame.evaluate<R, Arg>(f, arg as Arg)),
169+
title: use((f) => f.title()),
170+
use,
171+
locator: (selector, options) =>
172+
PlaywrightLocator.make(frame.locator(selector, options)),
173+
getByRole: (role, options) =>
174+
PlaywrightLocator.make(frame.getByRole(role, options)),
175+
getByText: (text, options) =>
176+
PlaywrightLocator.make(frame.getByText(text, options)),
177+
getByLabel: (label, options) =>
178+
PlaywrightLocator.make(frame.getByLabel(label, options)),
179+
getByTestId: (testId) =>
180+
PlaywrightLocator.make(frame.getByTestId(testId)),
181+
url: Effect.sync(() => frame.url()),
182+
content: use((f) => f.content()),
183+
name: Effect.sync(() => frame.name()),
184+
click: (selector, options) => use((f) => f.click(selector, options)),
185+
});
186+
}
187+
}

src/page.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ import type {
1111
WebSocket,
1212
Worker,
1313
} from "playwright-core";
14+
1415
import {
1516
PlaywrightDialog,
1617
PlaywrightDownload,
1718
PlaywrightFileChooser,
18-
PlaywrightFrame,
1919
PlaywrightRequest,
2020
PlaywrightResponse,
2121
PlaywrightWorker,
2222
} from "./common";
23+
2324
import type { PlaywrightError } from "./errors";
25+
import { PlaywrightFrame } from "./frame";
2426
import { PlaywrightLocator } from "./locator";
2527
import type { PageFunction, PatchedEvents } from "./playwright-types";
2628
import { useHelper } from "./utils";

0 commit comments

Comments
 (0)