From 718bda5426f281be6241f55b0a518fcc4340f9f9 Mon Sep 17 00:00:00 2001 From: Filip Weiss Date: Wed, 28 Oct 2026 16:15:25 +0100 Subject: [PATCH] Upgrade Playwright to 1.64.1 --- CHANGELOG.md | 4 ++ package.json | 8 +-- pnpm-lock.yaml | 34 +++++------ src/frame.test.ts | 4 +- src/frame.ts | 20 ++++++- src/locator.ts | 29 ++++++++++ src/page.test.ts | 4 +- src/page.ts | 34 ++++++++++- src/playwright-api.ts | 6 ++ src/tracing.ts | 4 +- src/webmcp.ts | 132 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 src/webmcp.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f27825f..63eb394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`, 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` 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 diff --git a/package.json b/package.json index 4d6c6c7..0eaedd0 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45bd611..6579303 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: playwright-core: - specifier: ^1.63.0 - version: 1.63.0 + specifier: ^1.64.1 + version: 1.64.1 devDependencies: '@biomejs/biome': specifier: 2.5.13 @@ -22,8 +22,8 @@ importers: specifier: ^4.0.0 version: 4.0.0(effect@4.0.0)(vitest@5.0.1(@types/node@24.13.4)(vite@8.3.0(@types/node@24.13.4)(esbuild@0.28.2)(tsx@4.23.13)(yaml@2.9.0))) '@playwright/test': - specifier: ^1.63.0 - version: 1.63.0 + specifier: ^1.64.1 + version: 1.64.1 '@types/node': specifier: ^24.13.4 version: 24.13.4 @@ -34,8 +34,8 @@ importers: specifier: ^0.0.88 version: 0.0.88 playwright: - specifier: ^1.63.0 - version: 1.63.0 + specifier: ^1.64.1 + version: 1.64.1 ts-morph: specifier: ^28.0.0 version: 28.0.0 @@ -302,8 +302,8 @@ packages: '@oxc-project/types@0.149.0': resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} - '@playwright/test@1.63.0': - resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + '@playwright/test@1.64.1': + resolution: {integrity: sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==} engines: {node: '>=20'} hasBin: true @@ -812,13 +812,13 @@ packages: resolution: {integrity: sha512-Xc6PMJ2gher0WZP+rtjefFk26hIb7V1PTLL30bmy1Z2vsRmSvqiss7Ag1XUdyplTGYzIlFNJp4L3vMNmK44N6g==} hasBin: true - playwright-core@1.63.0: - resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + playwright-core@1.64.1: + resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} engines: {node: '>=20'} hasBin: true - playwright@1.63.0: - resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + playwright@1.64.1: + resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==} engines: {node: '>=20'} hasBin: true @@ -1206,9 +1206,9 @@ snapshots: '@oxc-project/types@0.149.0': {} - '@playwright/test@1.63.0': + '@playwright/test@1.64.1': dependencies: - playwright: 1.63.0 + playwright: 1.64.1 '@quansync/fs@1.0.0': dependencies: @@ -1561,11 +1561,11 @@ snapshots: pkg-pr-new@0.0.88: {} - playwright-core@1.63.0: {} + playwright-core@1.64.1: {} - playwright@1.63.0: + playwright@1.64.1: dependencies: - playwright-core: 1.63.0 + playwright-core: 1.64.1 postcss@8.5.28: dependencies: diff --git a/src/frame.test.ts b/src/frame.test.ts index 96d7db5..4ddcd55 100644 --- a/src/frame.test.ts +++ b/src/frame.test.ts @@ -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 @@ -123,7 +123,7 @@ layer(PlaywrightSpawner.layer(chromium))("Frame", (it) => { // Test setContent yield* frame.setContent("

New Content

"); - const newContent = yield* frame.content; + const newContent = yield* frame.content(); assert.isTrue(newContent.includes("New Content")); }).pipe(PlaywrightSpawner.withBrowser), ); diff --git a/src/frame.ts b/src/frame.ts index 0c4fba6..3e7b2e2 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -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 @@ -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; + readonly content: ( + options?: Parameters[0], + ) => Effect.Effect; + + /** + * 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. @@ -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)), diff --git a/src/locator.ts b/src/locator.ts index d9357ef..28539c5 100644 --- a/src/locator.ts +++ b/src/locator.ts @@ -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. * @@ -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( diff --git a/src/page.test.ts b/src/page.test.ts index 2c66df3..b9b280a 100644 --- a/src/page.test.ts +++ b/src/page.test.ts @@ -31,7 +31,7 @@ layer(PlaywrightSpawner.layer(chromium))("Page", (it) => { const page = yield* browser.newPage(); yield* page.setContent("

Hello World

"); - const content = yield* page.content; + const content = yield* page.content(); assert(content.includes("

Hello World

")); }).pipe(PlaywrightSpawner.withBrowser), ); @@ -55,7 +55,7 @@ layer(PlaywrightSpawner.layer(chromium))("Page", (it) => { yield* page.goto( "data:text/html,Content

Hello

", ); - const content = yield* page.content; + const content = yield* page.content(); assert(content.includes("

Hello

")); }).pipe(PlaywrightSpawner.withBrowser), ); diff --git a/src/page.ts b/src/page.ts index 96270b9..090aec6 100644 --- a/src/page.ts +++ b/src/page.ts @@ -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; @@ -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 @@ -486,10 +498,16 @@ export interface Page { readonly title: Effect.Effect; /** * 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; + readonly content: ( + options?: Parameters[0], + ) => Effect.Effect; /** * Runs an asynchronous operation against the underlying Playwright `Page`. * @@ -583,6 +601,16 @@ export interface Page { readonly getByTestId: ( testId: Parameters[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. * @@ -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), @@ -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: ( fn: PageFunction, arg?: Arg, @@ -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) => diff --git a/src/playwright-api.ts b/src/playwright-api.ts index 3ab322b..dc1ca68 100644 --- a/src/playwright-api.ts +++ b/src/playwright-api.ts @@ -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"; diff --git a/src/tracing.ts b/src/tracing.ts index f3b2fea..31e1b5c 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -90,8 +90,8 @@ export const Tracing = Context.Service( 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(() => {})), diff --git a/src/webmcp.ts b/src/webmcp.ts new file mode 100644 index 0000000..da30c75 --- /dev/null +++ b/src/webmcp.ts @@ -0,0 +1,132 @@ +/** + * Effect service wrapper for tools registered through the WebMCP browser API. + * + * @since 0.9.0 + */ + +import { Context, type Effect } from "effect"; +import type { WebMCP as CoreWebMCP } from "playwright-core"; +import type { PlaywrightError } from "./errors"; +import { useHelper } from "./utils"; + +/** + * Hints a page provides about a WebMCP tool. + * + * @category models + * @since 0.9.0 + */ +export interface WebMCPToolAnnotations { + /** The tool does not modify any state. */ + readonly readOnly?: boolean; + /** The tool output may contain third-party content. */ + readonly untrustedContent?: boolean; + /** The tool takes a consequential action, such as placing an order. */ + readonly consequential?: boolean; +} + +/** + * A tool registered by a frame through the WebMCP browser API. + * + * @see {@link CoreWebMCP.tools} + * @category models + * @since 0.9.0 + */ +export interface WebMCPTool { + /** Tool name, unique within the frame. */ + readonly name: string; + /** Tool description. */ + readonly description: string; + /** JSON Schema of the tool input, when the page provides one. */ + readonly inputSchema?: unknown; + /** Hints the page provides about the tool. */ + readonly annotations?: WebMCPToolAnnotations; +} + +/** + * Tools that a frame registers through the experimental + * [WebMCP](https://webmachinelearning.github.io/webmcp/) browser API. + * + * **Gotchas** + * + * WebMCP must be enabled when the browser launches. In Chromium, pass + * `--enable-features=WebMCP`; WebKit does not support it. `Page.webmcp` only + * covers the main frame, so child frames list their tools through their own + * `Frame.webmcp`. + * + * @example + * ```ts + * import { Effect } from "effect"; + * import { Playwright } from "effect-playwright"; + * import { chromium } from "playwright-core"; + * + * const program = Effect.gen(function* () { + * const playwright = yield* Playwright.Playwright; + * const browser = yield* playwright.launchScoped(chromium, { + * args: ["--enable-features=WebMCP"], + * }); + * const page = yield* browser.newPage(); + * yield* page.goto("https://example.com"); + * + * const tools = yield* page.webmcp.tools(); + * const result = yield* page.webmcp.callTool("add", { a: 2, b: 40 }); + * return { tools, result }; + * }).pipe(Effect.scoped, Effect.provide(Playwright.layer)); + * ``` + * + * @see {@link CoreWebMCP} + * @category models + * @since 0.9.0 + */ +export interface WebMCP { + /** + * Calls a tool registered by the frame and returns its result. + * + * The result is whatever the tool's `execute` function resolved to. A result + * with `isError: true` succeeds as is; the effect fails when the tool is not + * registered or its `execute` function throws. + * + * @see {@link CoreWebMCP.callTool} + * @since 0.9.0 + */ + readonly callTool: ( + name: string, + input?: unknown, + options?: Parameters[2], + ) => Effect.Effect; + + /** + * Returns the tools currently registered by the frame. + * + * Fails if the browser was launched without WebMCP support. + * + * @see {@link CoreWebMCP.tools} + * @since 0.9.0 + */ + readonly tools: ( + options?: Parameters[0], + ) => Effect.Effect, PlaywrightError>; +} + +/** + * @category services + * @since 0.9.0 + */ +export const WebMCP = Context.Service( + "effect-playwright/webmcp/WebMCP", +); + +/** + * Creates `WebMCP` from a Playwright `WebMCP` instance. + * + * @param webmcp - The Playwright `WebMCP` instance to wrap. + * @category constructors + * @since 0.9.0 + */ +export const makeWebMCP = (webmcp: CoreWebMCP): WebMCP => { + const use = useHelper(webmcp); + return WebMCP.of({ + callTool: (name, input, options) => + use((w) => w.callTool(name, input, options)), + tools: (options) => use((w) => w.tools(options)), + }); +};