Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
39f6d09
implement clock
fiws Feb 20, 2026
0109e61
add page.addInitScript
fiws Feb 20, 2026
7159514
implement Page.keyboard
fiws Feb 20, 2026
222cfe4
improve implement skill
fiws Feb 20, 2026
547cc50
lint fixes
fiws Feb 20, 2026
e8b8f76
implement Page.screenshot
fiws Feb 20, 2026
8990a57
implement Page.addScriptTag
fiws Feb 21, 2026
e1c813d
implement Page.addStyleTag
fiws Feb 21, 2026
dda6c66
implement Page.bringToFront
fiws Feb 21, 2026
d9d88af
fix @since docstrings
fiws Feb 21, 2026
74644bf
implement Page.consoleMessages
fiws Feb 21, 2026
3ee03bd
implement Page.content
fiws Feb 21, 2026
687314c
implement Page.context
fiws Feb 21, 2026
37236fb
implement Page.dragAndDrop
fiws Feb 21, 2026
97cb523
implement Page.emulateMedia
fiws Feb 21, 2026
c200070
implement Page.exposeFunction
fiws Feb 21, 2026
f6f69c0
implement Page.frame
fiws Feb 21, 2026
f3aedcb
implement missing Page.getBy* methods
fiws Feb 21, 2026
6a66cb6
coverage: categorize soft deprecated
fiws Feb 21, 2026
91c49cc
implement Page.goBack and Page.goForward
fiws Feb 21, 2026
3e9efac
implement Page.mouse
fiws Feb 21, 2026
b10aa02
improve agent instructions a bit
fiws Feb 21, 2026
0b0236f
implement Page.touchscreen
fiws Feb 21, 2026
3c7a864
implement Page.isClosed
fiws Feb 21, 2026
6d76bf2
implement Page.mainFrame and Page.opener
fiws Feb 21, 2026
e35e61d
implement Page.pageErrors
fiws Feb 21, 2026
04c1b8c
implement Page.pause
fiws Feb 21, 2026
4550e27
implement Page.pdf
fiws Feb 21, 2026
26bc3cb
implement page.requestGC
fiws Feb 21, 2026
3853270
implement page.setContent
fiws Feb 21, 2026
ea418b0
implement remaining Page.set* methods
fiws Feb 21, 2026
e7d7629
implement remaining Page.viewpowerSize
fiws Feb 21, 2026
7dc0ed3
implement remaining Page.workers
fiws Feb 21, 2026
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
41 changes: 36 additions & 5 deletions .agents/skills/implement-playwright-method/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ First, find the implementation of the method in the Playwright codebase to under

### 2. Analyze the Method

Determine if the method can throw and what it returns. **Do not blindly follow existing patterns in `effect-playwright` if they wrap safe synchronous methods in Effects.**
Determine if the method can throw and what it returns. **Do not blindly follow existing patterns in `effect-playwright`. Always analyze the original Playwright source code to determine behavior.**

#### Can it throw?

Expand All @@ -43,7 +43,38 @@ Determine if the method can throw and what it returns. **Do not blindly follow e
- **`T | null`** -> `Option<T>` (if sync) or `Effect<Option<T>, PlaywrightError>` (if async)
- **Playwright Object (e.g., `Page`)** -> **Wrapped Object (e.g., `PlaywrightPage`)**

### 3. Define the Interface
### 3. Handle Sub-APIs / Nested Properties

Some Playwright interfaces expose other classes as properties (e.g., `Page.keyboard`, `Page.mouse`, `BrowserContext.tracing`).

1. **Create a new Wrapper**: Create a new file, Service, and Tag for the sub-API (e.g., `PlaywrightKeyboardService` wrapping `Keyboard`).
2. **Expose as a Sync Property**: Expose it as a direct, read-only property on the parent service. Do not wrap property access in an `Effect`.

**Example (Interface in Parent):**

```typescript
export interface PlaywrightPageService {
/**
* Access the keyboard.
* @see {@link Page.keyboard}
*/
readonly keyboard: PlaywrightKeyboardService;
}
```

**Example (Implementation in Parent's `make`):**

```typescript
static make(page: Page): PlaywrightPageService {
return PlaywrightPage.of({
// Initialize the sub-API wrapper synchronously
keyboard: PlaywrightKeyboard.make(page.keyboard),
// ...
});
}
```

### 4. Define the Interface

Add the method to the Service interface in the corresponding `src/X.ts` file (e.g., `PlaywrightPageService` in `src/page.ts`).

Expand Down Expand Up @@ -93,7 +124,7 @@ readonly url: () => string;
readonly textContent: Effect.Effect<Option.Option<string>, PlaywrightError>;
```

### 4. Implement the Method
### 5. Implement the Method

Implement the method in the `make` function of the implementation class (e.g., `PlaywrightPage.make`).

Expand Down Expand Up @@ -135,7 +166,7 @@ Implement the method in the `make` function of the implementation class (e.g., `
),
```

### 5. Verify
### 6. Verify

- Ensure types match `PlaywrightXService`.
- Run `npm run typecheck` (or equivalent) to verify implementation.
- Run `pnpm type-check` and `pnpm test` to verify implementation.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Use `pnpm` for all package management tasks.
- **Run All Tests:** `pnpm test` (uses `vitest`)
- **Run Single Test File:** `pnpm test src/path/to/test.ts`
- **Type Check:** `pnpm type-check` (runs `tsc --noEmit`)
- **Format:** `pnpm format-fix` (uses `biome format --fix`)
- **Format:** `pnpm format` (uses `biome format --fix`)
- **Generate Docs:** `pnpm generate-docs`

## 2. Code Style & Conventions
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"type-check": "tsc --noEmit",
"coverage": "tsx scripts/coverage.ts",
"generate-docs": "typedoc",
"format-fix": "biome format --fix"
"format": "biome format --fix"
},
"keywords": [
"effect",
Expand Down
16 changes: 11 additions & 5 deletions scripts/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const MAPPINGS = [
{ pw: "Dialog", ep: "PlaywrightDialog", type: "class" as const },
{ pw: "FileChooser", ep: "PlaywrightFileChooser", type: "class" as const },
{ pw: "Download", ep: "PlaywrightDownload", type: "class" as const },
{ pw: "Clock", ep: "PlaywrightClockService", type: "interface" as const },
];

const EXCLUDED_METHODS = new Set([
Expand Down Expand Up @@ -61,11 +62,16 @@ function isRelevantProperty(name: string) {
}

function isDeprecated(node: JSDocableNode): boolean {
return node
.getJsDocs()
.some((doc) =>
doc.getTags().some((tag) => tag.getTagName() === "deprecated"),
);
return node.getJsDocs().some((doc) => {
const hasDeprecatedTag = doc
.getTags()
.some((tag) => tag.getTagName() === "deprecated");
const docText = doc.getText();

// some methods are "soft-deprecated", i.e. they are still available but discouraged
const hasLocatorNote = docText.includes("**NOTE** Use locator-based");
return hasDeprecatedTag || hasLocatorNote;
});
}

const runCoverage = Effect.gen(function* () {
Expand Down
39 changes: 39 additions & 0 deletions src/browser-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { assert, layer } from "@effect/vitest";
import { Effect } from "effect";
import { chromium } from "playwright-core";
import { PlaywrightBrowser } from "./browser";
import { PlaywrightEnvironment } from "./experimental";

type TestWindow = Window & {
magicValue?: number;
};

layer(PlaywrightEnvironment.layer(chromium))(
"PlaywrightBrowserContext",
(it) => {
it.scoped("addInitScript should execute script in all new pages", () =>
Effect.gen(function* () {
const browser = yield* PlaywrightBrowser;
const context = yield* browser.newContext();

yield* context.addInitScript(() => {
(window as TestWindow).magicValue = 84;
});

const page1 = yield* context.newPage;
yield* page1.goto("about:blank");
const magicValue1 = yield* page1.evaluate(
() => (window as TestWindow).magicValue,
);
assert.strictEqual(magicValue1, 84);

const page2 = yield* context.newPage;
yield* page2.goto("about:blank");
const magicValue2 = yield* page2.evaluate(
() => (window as TestWindow).magicValue,
);
assert.strictEqual(magicValue2, 84);
}).pipe(PlaywrightEnvironment.withBrowser),
);
},
);
19 changes: 19 additions & 0 deletions src/browser-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
WebError,
Worker,
} from "playwright-core";
import { PlaywrightClock, type PlaywrightClockService } from "./clock";
import {
PlaywrightDialog,
PlaywrightRequest,
Expand Down Expand Up @@ -58,6 +59,10 @@ type BrowserContextWithPatchedEvents = PatchedEvents<
* @since 0.1.0
*/
export interface PlaywrightBrowserContextService {
/**
* Access the clock.
*/
readonly clock: PlaywrightClockService;
/**
* Returns the list of all open pages in the browser context.
*
Expand Down Expand Up @@ -87,6 +92,18 @@ export interface PlaywrightBrowserContextService {
* @since 0.1.0
*/
readonly close: Effect.Effect<void, PlaywrightError>;
/**
* Adds a script which would be evaluated in one of the following scenarios:
* - Whenever a page is created in the browser context or is navigated.
* - Whenever a child frame is attached or navigated. In this case, the script is evaluated in the context of the newly attached frame.
*
* @see {@link BrowserContext.addInitScript}
* @since 0.2.0
*/
readonly addInitScript: (
script: Parameters<BrowserContext["addInitScript"]>[0],
arg?: Parameters<BrowserContext["addInitScript"]>[1],
) => Effect.Effect<void, PlaywrightError>;

/**
* Creates a stream of the given event from the browser context.
Expand Down Expand Up @@ -122,9 +139,11 @@ export class PlaywrightBrowserContext extends Context.Tag(
): PlaywrightBrowserContextService {
const use = useHelper(context);
return PlaywrightBrowserContext.of({
clock: PlaywrightClock.make(context.clock),
pages: Effect.sync(() => context.pages().map(PlaywrightPage.make)),
newPage: use((c) => c.newPage().then(PlaywrightPage.make)),
close: use((c) => c.close()),
addInitScript: (script, arg) => use((c) => c.addInitScript(script, arg)),
eventStream: <K extends keyof BrowserContextEvents>(event: K) =>
Stream.asyncPush<BrowserContextEvents[K]>((emit) =>
Effect.acquireRelease(
Expand Down
124 changes: 124 additions & 0 deletions src/clock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Context, type Effect } from "effect";
import type { Clock } from "playwright-core";
import type { PlaywrightError } from "./errors";
import { useHelper } from "./utils";

/**
* Interface for a Playwright clock.
* @category model
*/
export interface PlaywrightClockService {
/**
* Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user
* closing the laptop lid for a while and reopening it later, after given time.
*
* @see {@link Clock.fastForward}
* @since 0.1.0
*/
readonly fastForward: (
ticks: number | string,
) => Effect.Effect<void, PlaywrightError>;

/**
* Install fake implementations for time-related functions.
*
* @see {@link Clock.install}
* @since 0.1.0
*/
readonly install: (options?: {
time?: number | string | Date;
}) => Effect.Effect<void, PlaywrightError>;

/**
* Advance the clock by jumping forward in time and pause the time.
*
* @see {@link Clock.pauseAt}
* @since 0.1.0
*/
readonly pauseAt: (
time: number | string | Date,
) => Effect.Effect<void, PlaywrightError>;

/**
* Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
*
* @see {@link Clock.resume}
* @since 0.1.0
*/
readonly resume: Effect.Effect<void, PlaywrightError>;

/**
* Advance the clock, firing all the time-related callbacks.
*
* @see {@link Clock.runFor}
* @since 0.1.0
*/
readonly runFor: (
ticks: number | string,
) => Effect.Effect<void, PlaywrightError>;

/**
* Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running.
*
* @see {@link Clock.setFixedTime}
* @since 0.1.0
*/
readonly setFixedTime: (
time: number | string | Date,
) => Effect.Effect<void, PlaywrightError>;

/**
* Sets system time, but does not trigger any timers.
*
* @see {@link Clock.setSystemTime}
* @since 0.1.0
*/
readonly setSystemTime: (
time: number | string | Date,
) => Effect.Effect<void, PlaywrightError>;

/**
* A generic utility to execute any promise-based method on the underlying Playwright `Clock`.
* Can be used to access any Clock functionality not directly exposed by this service.
*
* @param f - A function that takes the Playwright `Clock` and returns a `Promise`.
* @returns An effect that wraps the promise and returns its result.
* @see {@link Clock}
* @since 0.1.0
*/
readonly use: <T>(
f: (clock: Clock) => Promise<T>,
) => Effect.Effect<T, PlaywrightError>;
}

/**
* A service that provides a `PlaywrightClock` instance.
*
* @since 0.1.0
* @category tag
*/
export class PlaywrightClock extends Context.Tag(
"effect-playwright/PlaywrightClock",
)<PlaywrightClock, PlaywrightClockService>() {
/**
* Creates a `PlaywrightClock` from a Playwright `Clock` instance.
*
* @param clock - The Playwright `Clock` instance to wrap.
* @since 0.1.0
* @category constructor
*/
static make(clock: Clock): typeof PlaywrightClock.Service {
const use = useHelper(clock);

return PlaywrightClock.of({
fastForward: (ticks) => use((c) => c.fastForward(ticks)),
install: (options) => use((c) => c.install(options)),
pauseAt: (time) => use((c) => c.pauseAt(time)),
resume: use((c) => c.resume()),
runFor: (ticks) => use((c) => c.runFor(ticks)),
setFixedTime: (time) => use((c) => c.setFixedTime(time)),
setSystemTime: (time) => use((c) => c.setSystemTime(time)),
use,
});
}
}
18 changes: 6 additions & 12 deletions src/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightCommon", (it) => {

const requestFiber = yield* page
.eventStream("request")
.pipe(Stream.runHead)
.pipe(Effect.fork);
.pipe(Stream.runHead, Effect.fork);

const responseFiber = yield* page
.eventStream("response")
.pipe(Stream.runHead)
.pipe(Effect.fork);
.pipe(Stream.runHead, Effect.fork);

yield* page.goto("http://example.com");

Expand Down Expand Up @@ -52,8 +50,7 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightCommon", (it) => {

const workerFiber = yield* page
.eventStream("worker")
.pipe(Stream.runHead)
.pipe(Effect.fork);
.pipe(Stream.runHead, Effect.fork);

yield* page.evaluate(() => {
const blob = new Blob(['console.log("worker")'], {
Expand All @@ -77,8 +74,7 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightCommon", (it) => {

const dialogFiber = yield* page
.eventStream("dialog")
.pipe(Stream.runHead)
.pipe(Effect.fork);
.pipe(Stream.runHead, Effect.fork);

yield* page.evaluate(() => {
setTimeout(() => alert("hello world"), 10);
Expand All @@ -104,8 +100,7 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightCommon", (it) => {

const fileChooserFiber = yield* page
.eventStream("filechooser")
.pipe(Stream.runHead)
.pipe(Effect.fork);
.pipe(Stream.runHead, Effect.fork);

yield* page.locator("#fileinput").click();

Expand All @@ -130,8 +125,7 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightCommon", (it) => {

const downloadFiber = yield* page
.eventStream("download")
.pipe(Stream.runHead)
.pipe(Effect.fork);
.pipe(Stream.runHead, Effect.fork);

yield* page.locator("#download").click();

Expand Down
3 changes: 2 additions & 1 deletion src/experimental/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export const layer = (browser: BrowserType, launchOptions?: LaunchOptions) => {
browser: playwright.launchScoped(browser, launchOptions),
});
}),
).pipe(Effect.provide(Playwright.layer)),
Effect.provide(Playwright.layer),
),
);
};

Expand Down
Loading