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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ All notable changes to this project will be documented in this file.
### Breaking Changes

- **`Page.reload` is a method**: `Page.reload` changed from an `Effect` property to a method that takes Playwright's reload options, like `Page.goBack()` and `Page.goForward()`. Replace `yield* page.reload` with `yield* page.reload()`.
- **`Page.content` and `Frame.content` are methods**: They changed from `Effect` properties to methods that accept Playwright's new `includeShadow` option. Replace `yield* page.content` with `yield* page.content()`.
- **Playwright 1.64 behavior changes**: Elements inside hidden iframes are now considered hidden by actions and `Locator.isVisible()`. The `@playwright/test` peer dependency now requires `^1.64.1`.

### Features

- **Navigation responses**: `Page.goto()`, `Frame.goto()` and `Page.reload()` now return the main resource response as `Option<Playwright.Response>`, like `Page.goBack()` and `Page.goForward()`, instead of `void`. Playwright does not fail on an error status such as 404, so the response is the way to tell whether the page loaded. Code that ignores the result is unaffected, and code that needs `Effect<void, PlaywrightError>` can pipe `Effect.asVoid`.
- **Playwright 1.64.1 Upgrade**: Upgraded `playwright-core`, `playwright`, and `@playwright/test` to version `1.64.1`.
- **Playwright 1.64 APIs**: Added `Locator.within()`, `Page.getByRef()`, the `includeShadow` option of `Page.content()` and `Frame.content()`, and `Page.webmcp` and `Frame.webmcp`, backed by the new `Playwright.WebMCP` service (`tools()` and `callTool()`). New `fps` and action `style` video options pass through the existing option types.

## 0.8.0

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@
],
"packageManager": "pnpm@11.5.1",
"dependencies": {
"playwright-core": "^1.63.0"
"playwright-core": "^1.64.1"
},
"peerDependencies": {
"@playwright/test": "^1.63.0",
"@playwright/test": "^1.64.1",
"effect": "^4.0.0"
},
"peerDependenciesMeta": {
Expand All @@ -62,11 +62,11 @@
"@biomejs/biome": "2.5.13",
"@effect/language-service": "0.87.2",
"@effect/vitest": "^4.0.0",
"@playwright/test": "^1.63.0",
"@playwright/test": "^1.64.1",
"@types/node": "^24.13.4",
"effect": "^4.0.0",
"pkg-pr-new": "^0.0.88",
"playwright": "^1.63.0",
"playwright": "^1.64.1",
"ts-morph": "^28.0.0",
"tsdown": "0.23.0",
"tsx": "^4.23.13",
Expand Down
34 changes: 17 additions & 17 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/frame.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ layer(PlaywrightSpawner.layer(chromium))("Frame", (it) => {
assert.strictEqual(title, "Frame Title");

// Test content
const content = yield* frame.content;
const content = yield* frame.content();
assert.isTrue(content.includes("Hello from Frame"));

// Test evaluate
Expand Down Expand Up @@ -123,7 +123,7 @@ layer(PlaywrightSpawner.layer(chromium))("Frame", (it) => {

// Test setContent
yield* frame.setContent("<h1>New Content</h1>");
const newContent = yield* frame.content;
const newContent = yield* frame.content();
assert.isTrue(newContent.includes("New Content"));
}).pipe(PlaywrightSpawner.withBrowser),
);
Expand Down
20 changes: 18 additions & 2 deletions src/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { type Locator, makeLocator } from "./locator";
import { makePage, type Page } from "./page";
import type { PageFunction } from "./playwright-types";
import { useHelper } from "./utils";
import { makeWebMCP, type WebMCP } from "./webmcp";

/**
* @category models
Expand Down Expand Up @@ -243,10 +244,24 @@ export interface Frame {
/**
* Returns the full HTML contents of the frame, including the doctype.
*
* Pass `includeShadow: true` to serialize open shadow roots as declarative
* shadow DOM.
*
* @see {@link CoreFrame.content}
* @since 0.1.3
*/
readonly content: Effect.Effect<string, PlaywrightError>;
readonly content: (
options?: Parameters<CoreFrame["content"]>[0],
) => Effect.Effect<string, PlaywrightError>;

/**
* Access the tools that the frame registers through the experimental
* WebMCP browser API.
*
* @see {@link CoreFrame.webmcp}
* @since 0.9.0
*/
readonly webmcp: WebMCP;

/**
* Returns the owner iframe element for the frame.
Expand Down Expand Up @@ -337,7 +352,8 @@ export const makeFrame = (frame: CoreFrame): Frame => {
waitForTimeout: (timeout) => use((f) => f.waitForTimeout(timeout)),
setContent: (html, options) => use((f) => f.setContent(html, options)),
url: () => frame.url(),
content: use((f) => f.content()),
content: (options) => use((f) => f.content(options)),
webmcp: makeWebMCP(frame.webmcp),
frameElement: use((f) => f.frameElement()),
name: () => frame.name(),
click: (selector, options) => use((f) => f.click(selector, options)),
Expand Down
29 changes: 29 additions & 0 deletions src/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,33 @@ export interface Locator {
* @since 0.4.1
*/
readonly or: (locator: Locator | CoreLocator) => Locator;
/**
* Returns a locator that matches this locator's elements inside each element
* matched by `locator`.
*
* Relative locators such as {@link Locator.nth} or {@link Locator.first} are
* resolved separately inside each matched parent.
*
* @example
* ```ts
* import { Effect } from "effect";
* import { Playwright } from "effect-playwright";
*
* const program = Effect.gen(function* () {
* const page = yield* Playwright.Page;
* // The third cell of every row, not the third cell in the table.
* const thirdColumn = page
* .getByRole("cell")
* .nth(2)
* .within(page.getByRole("row"));
* return yield* thirdColumn.allTextContents();
* });
* ```
*
* @see {@link CoreLocator.within}
* @since 0.9.0
*/
readonly within: (locator: Locator | CoreLocator) => Locator;
/**
* A page this locator belongs to.
*
Expand Down Expand Up @@ -824,6 +851,8 @@ export const makeLocator = (locator: CoreLocator): Locator => {
frameLocator: (selector) =>
makeFrameLocator(locator.frameLocator(selector)),
or: (locatorOrService) => makeLocator(locator.or(unwrap(locatorOrService))),
within: (locatorOrService) =>
makeLocator(locator.within(unwrap(locatorOrService))),
page: () => makePage(locator.page()),
locator: (selectorOrLocator, options) =>
makeLocator(
Expand Down
4 changes: 2 additions & 2 deletions src/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ layer(PlaywrightSpawner.layer(chromium))("Page", (it) => {
const page = yield* browser.newPage();

yield* page.setContent("<h1>Hello World</h1>");
const content = yield* page.content;
const content = yield* page.content();
assert(content.includes("<h1>Hello World</h1>"));
}).pipe(PlaywrightSpawner.withBrowser),
);
Expand All @@ -55,7 +55,7 @@ layer(PlaywrightSpawner.layer(chromium))("Page", (it) => {
yield* page.goto(
"data:text/html,<html><head><title>Content</title></head><body><h1>Hello</h1></body></html>",
);
const content = yield* page.content;
const content = yield* page.content();
assert(content.includes("<h1>Hello</h1>"));
}).pipe(PlaywrightSpawner.withBrowser),
);
Expand Down
34 changes: 32 additions & 2 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { makeScreencast, type Screencast } from "./screencast";
import { makeTouchscreen, type Touchscreen } from "./touchscreen";
import { useHelper } from "./utils";
import { makeWebStorage, type WebStorage } from "./web-storage";
import { makeWebMCP, type WebMCP } from "./webmcp";

interface CorePageEventMap {
close: CorePage;
Expand Down Expand Up @@ -199,6 +200,17 @@ export interface Page {
* @since 0.5.1
*/
readonly sessionStorage: WebStorage;
/**
* Access the tools that the main frame registers through the experimental
* WebMCP browser API.
*
* Shortcut for `page.mainFrame().webmcp`. Child frames expose their tools
* through their own {@link Frame.webmcp}.
*
* @see {@link CorePage.webmcp}
* @since 0.9.0
*/
readonly webmcp: WebMCP;
/**
* Navigates the page to the given URL and returns the main resource
* response, or `None` when there is none, such as for `about:blank` or a
Expand Down Expand Up @@ -486,10 +498,16 @@ export interface Page {
readonly title: Effect.Effect<string, PlaywrightError>;
/**
* Returns the full HTML contents of the page, including the doctype.
*
* Pass `includeShadow: true` to serialize open shadow roots as declarative
* shadow DOM.
*
* @see {@link CorePage.content}
* @since 0.3.0
*/
readonly content: Effect.Effect<string, PlaywrightError>;
readonly content: (
options?: Parameters<CorePage["content"]>[0],
) => Effect.Effect<string, PlaywrightError>;
/**
* Runs an asynchronous operation against the underlying Playwright `Page`.
*
Expand Down Expand Up @@ -583,6 +601,16 @@ export interface Page {
readonly getByTestId: (
testId: Parameters<CorePage["getByTestId"]>[0],
) => Locator;
/**
* Returns a locator for the element with the given aria ref, such as `e2`.
*
* Refs are reported by {@link Page.ariaSnapshot} in `"ai"` mode and resolve
* against the latest snapshot taken in the element's frame.
*
* @see {@link CorePage.getByRef}
* @since 0.9.0
*/
readonly getByRef: (ref: string) => Locator;
/**
* Returns a locator that matches the given alt text.
*
Expand Down Expand Up @@ -939,6 +967,7 @@ export const makePage = (page: CorePage): Page => {
touchscreen: makeTouchscreen(page.touchscreen),
screencast: makeScreencast(page.screencast),
sessionStorage: makeWebStorage(page.sessionStorage),
webmcp: makeWebMCP(page.webmcp),
goto: (url, options) =>
use((page) => page.goto(url, options)).pipe(
Effect.map(Option.fromNullishOr),
Expand All @@ -959,7 +988,7 @@ export const makePage = (page: CorePage): Page => {
waitForLoadState: (state, options) =>
use((page) => page.waitForLoadState(state, options)),
title: use((page) => page.title()),
content: use((page) => page.content()),
content: (options) => use((page) => page.content(options)),
evaluate: <R, Arg>(
fn: PageFunction<Arg, R>,
arg?: Arg,
Expand Down Expand Up @@ -1015,6 +1044,7 @@ export const makePage = (page: CorePage): Page => {
getByLabel: (label, options) =>
makeLocator(page.getByLabel(label, options)),
getByTestId: (testId) => makeLocator(page.getByTestId(testId)),
getByRef: (ref) => makeLocator(page.getByRef(ref)),
getByAltText: (text, options) =>
makeLocator(page.getByAltText(text, options)),
getByPlaceholder: (text, options) =>
Expand Down
6 changes: 6 additions & 0 deletions src/playwright-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ export { makeScreencast, Screencast } from "./screencast";
export { makeTouchscreen, Touchscreen } from "./touchscreen";
export { makeTracing, Tracing } from "./tracing";
export { makeWebStorage, WebStorage } from "./web-storage";
export {
makeWebMCP,
WebMCP,
type WebMCPTool,
type WebMCPToolAnnotations,
} from "./webmcp";
4 changes: 2 additions & 2 deletions src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ export const Tracing = Context.Service<Tracing>(
export const makeTracing = (tracing: CoreTracing): Tracing => {
const use = useHelper(tracing);
return Tracing.of({
start: (options) => use((t) => t.start(options)),
startChunk: (options) => use((t) => t.startChunk(options)),
start: (options) => use((t) => t.start(options).then(() => {})),
startChunk: (options) => use((t) => t.startChunk(options).then(() => {})),
stopChunk: (options) => use((t) => t.stopChunk(options)),
stop: (options) => use((t) => t.stop(options)),
startHar: (options) => use((t) => t.startHar(options).then(() => {})),
Expand Down
Loading
Loading