From 6f73a5b919c52779668258f0209fd7c291d0eac4 Mon Sep 17 00:00:00 2001 From: Filip Weiss Date: Fri, 15 May 2026 10:58:58 +0200 Subject: [PATCH] Upgrade to playwright 1.60 --- .../implement-playwright-method/SKILL.md | 1 + .agents/skills/upgrade-playwright/SKILL.md | 1 + package.json | 4 +- pnpm-lock.yaml | 22 ++-- src/browser-context.ts | 36 ++++++ src/browser.ts | 30 ++++- src/locator.test.ts | 3 + src/locator.ts | 36 +++++- src/page.test.ts | 4 +- src/page.ts | 114 ++++++++++++++++-- src/screencast.ts | 111 +++++++++++++++++ src/tracing.ts | 90 ++++++++++++++ 12 files changed, 424 insertions(+), 28 deletions(-) create mode 100644 src/screencast.ts create mode 100644 src/tracing.ts diff --git a/.agents/skills/implement-playwright-method/SKILL.md b/.agents/skills/implement-playwright-method/SKILL.md index fa46782..9aae880 100644 --- a/.agents/skills/implement-playwright-method/SKILL.md +++ b/.agents/skills/implement-playwright-method/SKILL.md @@ -169,4 +169,5 @@ Implement the method in the `make` function of the implementation class (e.g., ` ### 6. Verify - Ensure types match `PlaywrightXService`. +- Run `pnpm exec biome check --write .` to ensure code style and lint rules are followed. - Run `pnpm type-check` and `pnpm test` to verify implementation. diff --git a/.agents/skills/upgrade-playwright/SKILL.md b/.agents/skills/upgrade-playwright/SKILL.md index 74bdb0e..3be432f 100644 --- a/.agents/skills/upgrade-playwright/SKILL.md +++ b/.agents/skills/upgrade-playwright/SKILL.md @@ -60,6 +60,7 @@ pnpm exec playwright install ### 6. Verification +- Run `pnpm exec biome check --write .` to ensure code style and lint rules are followed. - Run `pnpm type-check`. - Run `pnpm test` to ensure no regressions. - Ensure no temporary files remain diff --git a/package.json b/package.json index 0d59423..bfdb78f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ ], "packageManager": "pnpm@10.27.0", "dependencies": { - "playwright-core": "^1.59.1" + "playwright-core": "^1.60.0" }, "peerDependencies": { "@effect/platform": "^0.93.3", @@ -54,7 +54,7 @@ "@effect/vitest": "^0.29.0", "@types/node": "^25.6.1", "effect": "^3.21.2", - "playwright": "^1.59.1", + "playwright": "^1.60.0", "ts-morph": "^28.0.0", "tsdown": "0.22.0", "tsx": "^4.21.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bcd1d22..6d063d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: playwright-core: - specifier: ^1.59.1 - version: 1.59.1 + specifier: ^1.60.0 + version: 1.60.0 devDependencies: '@biomejs/biome': specifier: 2.4.14 @@ -37,8 +37,8 @@ importers: specifier: ^3.21.2 version: 3.21.2 playwright: - specifier: ^1.59.1 - version: 1.59.1 + specifier: ^1.60.0 + version: 1.60.0 ts-morph: specifier: ^28.0.0 version: 28.0.0 @@ -1174,13 +1174,13 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} hasBin: true - playwright@1.59.1: - resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} engines: {node: '>=18'} hasBin: true @@ -2322,11 +2322,11 @@ snapshots: picomatch@4.0.4: {} - playwright-core@1.59.1: {} + playwright-core@1.60.0: {} - playwright@1.59.1: + playwright@1.60.0: dependencies: - playwright-core: 1.59.1 + playwright-core: 1.60.0 optionalDependencies: fsevents: 2.3.2 diff --git a/src/browser-context.ts b/src/browser-context.ts index def8e3a..9eeb382 100644 --- a/src/browser-context.ts +++ b/src/browser-context.ts @@ -3,6 +3,8 @@ import type { BrowserContext, ConsoleMessage, Dialog, + Download, + Frame, Page, Request, Response, @@ -13,21 +15,31 @@ import { PlaywrightBrowser, type PlaywrightBrowserService } from "./browser"; import { PlaywrightClock, type PlaywrightClockService } from "./clock"; import { PlaywrightDialog, + PlaywrightDownload, PlaywrightRequest, PlaywrightResponse, PlaywrightWorker, } from "./common"; import type { PlaywrightError } from "./errors"; +import { PlaywrightFrame } from "./frame"; import { PlaywrightPage } from "./page"; import type { PatchedEvents } from "./playwright-types"; +import { PlaywrightTracing, type PlaywrightTracingService } from "./tracing"; import { useHelper } from "./utils"; interface BrowserContextEvents { + /** @deprecated Since Playwright 1.56.0. This event is no longer emitted. */ backgroundpage: Page; close: BrowserContext; console: ConsoleMessage; dialog: Dialog; + download: Download; + frameattached: Frame; + framedetached: Frame; + framenavigated: Frame; page: Page; + pageclose: Page; + pageload: Page; request: Request; requestfailed: Request; requestfinished: Request; @@ -41,7 +53,13 @@ const eventMappings = { close: (context: BrowserContext) => PlaywrightBrowserContext.make(context), console: identity, dialog: (dialog: Dialog) => PlaywrightDialog.make(dialog), + download: (download: Download) => PlaywrightDownload.make(download), + frameattached: (frame: Frame) => PlaywrightFrame.make(frame), + framedetached: (frame: Frame) => PlaywrightFrame.make(frame), + framenavigated: (frame: Frame) => PlaywrightFrame.make(frame), page: (page: Page) => PlaywrightPage.make(page), + pageclose: (page: Page) => PlaywrightPage.make(page), + pageload: (page: Page) => PlaywrightPage.make(page), request: (request: Request) => PlaywrightRequest.make(request), requestfailed: (request: Request) => PlaywrightRequest.make(request), requestfinished: (request: Request) => PlaywrightRequest.make(request), @@ -64,6 +82,12 @@ export interface PlaywrightBrowserContextService { * Access the clock. */ readonly clock: PlaywrightClockService; + /** + * Access the tracing. + * + * @since 0.5.0 + */ + readonly tracing: PlaywrightTracingService; /** * Returns the list of all open pages in the browser context. * @@ -214,6 +238,16 @@ export interface PlaywrightBrowserContextService { */ readonly setDefaultTimeout: (timeout: number) => void; + /** + * Sets the storage state for the browser context. + * + * @see {@link BrowserContext.setStorageState} + * @since 0.5.0 + */ + readonly setStorageState: ( + options: Parameters[0], + ) => Effect.Effect; + /** * Creates a stream of the given event from the browser context. * @@ -249,6 +283,7 @@ export class PlaywrightBrowserContext extends Context.Tag( const use = useHelper(context); return PlaywrightBrowserContext.of({ clock: PlaywrightClock.make(context.clock), + tracing: PlaywrightTracing.make(context.tracing), pages: () => context.pages().map(PlaywrightPage.make), newPage: use((c) => c.newPage().then(PlaywrightPage.make)), close: use((c) => c.close()), @@ -271,6 +306,7 @@ export class PlaywrightBrowserContext extends Context.Tag( setDefaultNavigationTimeout: (timeout) => context.setDefaultNavigationTimeout(timeout), setDefaultTimeout: (timeout) => context.setDefaultTimeout(timeout), + setStorageState: (options) => use((c) => c.setStorageState(options)), eventStream: (event: K) => Stream.asyncPush((emit) => Effect.acquireRelease( diff --git a/src/browser.ts b/src/browser.ts index 2f843ba..52284fb 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,6 +1,11 @@ import { Context, Effect, Stream } from "effect"; import type { Scope } from "effect/Scope"; -import type { Browser, BrowserType, chromium } from "playwright-core"; +import type { + Browser, + BrowserContext, + BrowserType, + chromium, +} from "playwright-core"; import { PlaywrightBrowserContext } from "./browser-context"; import type { PlaywrightError } from "./errors"; import { PlaywrightPage } from "./page"; @@ -13,10 +18,12 @@ export type NewContextOptions = Parameters[0]; interface BrowserEvents { disconnected: Browser; + context: BrowserContext; } const eventMappings = { disconnected: (browser: Browser) => PlaywrightBrowser.make(browser), + context: (context: BrowserContext) => PlaywrightBrowserContext.make(context), } as const; type BrowserWithPatchedEvents = PatchedEvents; @@ -94,6 +101,25 @@ export interface PlaywrightBrowserService { */ readonly isConnected: () => boolean; + /** + * Binds the browser to a title. + * + * @see {@link Browser.bind} + * @since 0.5.0 + */ + readonly bind: ( + title: string, + options?: Parameters[1], + ) => Effect.Effect<{ endpoint: string }, PlaywrightError>; + + /** + * Unbinds the browser. + * + * @see {@link Browser.unbind} + * @since 0.5.0 + */ + readonly unbind: Effect.Effect; + /** * Creates a stream of the given event from the browser. * @@ -138,6 +164,8 @@ export class PlaywrightBrowser extends Context.Tag( browserType: () => browser.browserType(), version: () => browser.version(), isConnected: () => browser.isConnected(), + bind: (title, options) => use((browser) => browser.bind(title, options)), + unbind: use((browser) => browser.unbind()), eventStream: (event: K) => Stream.asyncPush((emit) => Effect.acquireRelease( diff --git a/src/locator.test.ts b/src/locator.test.ts index de8da11..4f59808 100644 --- a/src/locator.test.ts +++ b/src/locator.test.ts @@ -237,6 +237,9 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightLocator", (it) => { // highlight yield* buttons.first().highlight(); + // hideHighlight + yield* buttons.first().hideHighlight; + // screenshot const screenshotBuffer = yield* buttons.first().screenshot(); assert(screenshotBuffer.length > 0); diff --git a/src/locator.ts b/src/locator.ts index 5402bce..16a617a 100644 --- a/src/locator.ts +++ b/src/locator.ts @@ -353,7 +353,36 @@ export interface PlaywrightLocatorService { * @see {@link Locator.highlight} * @since 0.4.1 */ - readonly highlight: () => Effect.Effect; + readonly highlight: ( + options?: Parameters[0], + ) => Effect.Effect; + /** + * Hides the element highlight previously added by highlight. + * + * @see {@link Locator.hideHighlight} + * @since 0.5.0 + */ + readonly hideHighlight: Effect.Effect; + /** + * Drops the locator. + * + * @see {@link Locator.drop} + * @since 0.5.0 + */ + readonly drop: ( + data: Parameters[0], + options?: Parameters[1], + ) => Effect.Effect; + /** + * Normalizes the locator. + * + * @see {@link Locator.normalize} + * @since 0.5.0 + */ + readonly normalize: () => Effect.Effect< + PlaywrightLocatorService, + PlaywrightError + >; /** * Captures a screenshot of the element. * @@ -778,7 +807,10 @@ export class PlaywrightLocator extends Context.Tag( Array> >, ), - highlight: () => use((l) => l.highlight()), + highlight: (options) => use((l) => l.highlight(options)), + hideHighlight: use((l) => l.hideHighlight()), + drop: (data, options) => use((l) => l.drop(data, options)), + normalize: () => use((l) => l.normalize().then(PlaywrightLocator.make)), screenshot: (options) => use((l) => l.screenshot(options)), blur: (options) => use((l) => l.blur(options)), clear: (options) => use((l) => l.clear(options)), diff --git a/src/page.test.ts b/src/page.test.ts index f223fbb..ee76e9f 100644 --- a/src/page.test.ts +++ b/src/page.test.ts @@ -578,7 +578,7 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightPage", (it) => { console.warn("Warning from page"); }); - const messages = yield* page.consoleMessages; + const messages = yield* page.consoleMessages(); assert.strictEqual(messages.length, 2); assert.strictEqual(messages[0].text(), "Hello from page"); @@ -628,7 +628,7 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightPage", (it) => { yield* Fiber.join(errorFiber); - const errors = yield* page.pageErrors; + const errors = yield* page.pageErrors(); assert.ok(errors.length >= 1); assert.strictEqual(errors[0].message, "Test Error"); }).pipe(PlaywrightEnvironment.withBrowser), diff --git a/src/page.ts b/src/page.ts index 478cf81..edb4e13 100644 --- a/src/page.ts +++ b/src/page.ts @@ -1,4 +1,12 @@ -import { Context, Effect, identity, Option, Runtime, Stream } from "effect"; +import { + Array, + Context, + Effect, + identity, + Option, + Runtime, + Stream, +} from "effect"; import type { ConsoleMessage, Dialog, @@ -31,6 +39,10 @@ import { PlaywrightKeyboard, type PlaywrightKeyboardService } from "./keyboard"; import { PlaywrightLocator } from "./locator"; import { PlaywrightMouse, type PlaywrightMouseService } from "./mouse"; import type { PageFunction, PatchedEvents } from "./playwright-types"; +import { + PlaywrightScreencast, + type PlaywrightScreencastService, +} from "./screencast"; import { PlaywrightTouchscreen, type PlaywrightTouchscreenService, @@ -113,6 +125,12 @@ export interface PlaywrightPageService { * @since 0.3.0 */ readonly touchscreen: PlaywrightTouchscreenService; + /** + * Access the screencast. + * + * @since 0.5.0 + */ + readonly screencast: PlaywrightScreencastService; /** * Navigates the page to the given URL. * @@ -670,34 +688,100 @@ export interface PlaywrightPageService { */ readonly url: () => string; + /** + * Clears all highlights. + * + * @see {@link Page.hideHighlight} + * @since 0.5.0 + */ + readonly hideHighlight: Effect.Effect; + + /** + * Clears stored console messages. + * + * @see {@link Page.clearConsoleMessages} + * @since 0.5.0 + */ + readonly clearConsoleMessages: Effect.Effect; + + /** + * Clears stored page errors. + * + * @see {@link Page.clearPageErrors} + * @since 0.5.0 + */ + readonly clearPageErrors: Effect.Effect; + /** * Returns all messages that have been logged to the console. * * @example * ```ts - * const consoleMessages = yield* page.consoleMessages; + * const consoleMessages = yield* page.consoleMessages(); * ``` * * @see {@link Page.consoleMessages} * @since 0.3.0 */ - readonly consoleMessages: Effect.Effect< - ReadonlyArray, - PlaywrightError - >; + readonly consoleMessages: ( + options?: Parameters[0], + ) => Effect.Effect, PlaywrightError>; /** * Returns all errors that have been thrown in the page. * * @example * ```ts - * const pageErrors = yield* page.pageErrors; + * const pageErrors = yield* page.pageErrors(); * ``` * * @see {@link Page.pageErrors} * @since 0.3.0 */ - readonly pageErrors: Effect.Effect, PlaywrightError>; + readonly pageErrors: ( + options?: Parameters[0], + ) => Effect.Effect, PlaywrightError>; + + /** + * Returns the most recent network requests from the page. + * + * @see {@link Page.requests} + * @since 0.5.0 + */ + readonly requests: Effect.Effect< + ReadonlyArray, + PlaywrightError + >; + + /** + * Enters an interactive mode where hovering over elements highlights them and shows the corresponding locator. + * + * @see {@link Page.pickLocator} + * @since 0.5.0 + */ + readonly pickLocator: Effect.Effect< + typeof PlaywrightLocator.Service, + PlaywrightError + >; + + /** + * Cancels the locator picking mode. + * + * @see {@link Page.cancelPickLocator} + * @since 0.5.0 + */ + readonly cancelPickLocator: Effect.Effect; + + /** + * Captures the aria snapshot of the page. + * + * @see {@link Page.ariaSnapshot} + * @since 0.5.0 + */ + readonly ariaSnapshot: ( + options?: Parameters[0], + ) => Effect.Effect; + /** * Returns all workers. * @@ -794,6 +878,7 @@ export class PlaywrightPage extends Context.Tag( keyboard: PlaywrightKeyboard.make(page.keyboard), mouse: PlaywrightMouse.make(page.mouse), touchscreen: PlaywrightTouchscreen.make(page.touchscreen), + screencast: PlaywrightScreencast.make(page.screencast), goto: (url, options) => use((p) => p.goto(url, options)), setContent: (html, options) => use((p) => p.setContent(html, options)), waitForTimeout: (timeout) => use((p) => p.waitForTimeout(timeout)), @@ -852,13 +937,22 @@ export class PlaywrightPage extends Context.Tag( getByTitle: (text, options) => PlaywrightLocator.make(page.getByTitle(text, options)), url: () => page.url(), + hideHighlight: use((p) => p.hideHighlight()), + clearConsoleMessages: use((p) => p.clearConsoleMessages()), + clearPageErrors: use((p) => p.clearPageErrors()), + consoleMessages: (options) => use((p) => p.consoleMessages(options)), + pageErrors: (options) => use((p) => p.pageErrors(options)), + requests: use((p) => p.requests()).pipe( + Effect.map(Array.map(PlaywrightRequest.make)), + ), + pickLocator: use((p) => p.pickLocator().then(PlaywrightLocator.make)), + cancelPickLocator: use((p) => p.cancelPickLocator()), + ariaSnapshot: (options) => use((p) => p.ariaSnapshot(options)), context: () => PlaywrightBrowserContext.make(page.context()), opener: use((p) => p.opener()).pipe( Effect.map(Option.fromNullable), Effect.map(Option.map(PlaywrightPage.make)), ), - consoleMessages: use((p) => p.consoleMessages()), - pageErrors: use((p) => p.pageErrors()), workers: () => page.workers().map(PlaywrightWorker.make), frame: (frameSelector) => diff --git a/src/screencast.ts b/src/screencast.ts new file mode 100644 index 0000000..8028bb6 --- /dev/null +++ b/src/screencast.ts @@ -0,0 +1,111 @@ +import { Context, type Effect } from "effect"; +import type { Screencast } from "playwright-core"; +import type { PlaywrightError } from "./errors"; +import { useHelper } from "./utils"; + +/** + * @category model + * @since 0.5.0 + */ +export interface PlaywrightScreencastService { + /** + * Starts recording the screencast. + * + * @see {@link Screencast.start} + * @since 0.5.0 + */ + readonly start: ( + options?: Parameters[0], + ) => Effect.Effect; + + /** + * Stops recording the screencast. + * + * @see {@link Screencast.stop} + * @since 0.5.0 + */ + readonly stop: Effect.Effect; + + /** + * Shows action annotations. + * + * @see {@link Screencast.showActions} + * @since 0.5.0 + */ + readonly showActions: ( + options?: Parameters[0], + ) => Effect.Effect; + + /** + * Hides action annotations. + * + * @see {@link Screencast.hideActions} + * @since 0.5.0 + */ + readonly hideActions: Effect.Effect; + + /** + * Shows a chapter title. + * + * @see {@link Screencast.showChapter} + * @since 0.5.0 + */ + readonly showChapter: ( + title: string, + options?: Parameters[1], + ) => Effect.Effect; + + /** + * Shows a custom HTML overlay. + * + * @see {@link Screencast.showOverlay} + * @since 0.5.0 + */ + readonly showOverlay: ( + html: string, + options?: Parameters[1], + ) => Effect.Effect; + + /** + * Shows all overlays. + * + * @see {@link Screencast.showOverlays} + * @since 0.5.0 + */ + readonly showOverlays: Effect.Effect; + + /** + * Hides all overlays. + * + * @see {@link Screencast.hideOverlays} + * @since 0.5.0 + */ + readonly hideOverlays: Effect.Effect; +} + +/** + * @category tag + */ +export class PlaywrightScreencast extends Context.Tag( + "effect-playwright/PlaywrightScreencast", +)() { + /** + * @category constructor + */ + static make(screencast: Screencast): PlaywrightScreencastService { + const use = useHelper(screencast); + return PlaywrightScreencast.of({ + start: (options) => use((s) => s.start(options).then(() => {})), + stop: use((s) => s.stop()), + showActions: (options) => + use((s) => s.showActions(options).then(() => {})), + hideActions: use((s) => s.hideActions()), + showChapter: (title, options) => + use((s) => s.showChapter(title, options)), + showOverlay: (html, options) => + use((s) => s.showOverlay(html, options).then(() => {})), + showOverlays: use((s) => s.showOverlays()), + hideOverlays: use((s) => s.hideOverlays()), + }); + } +} diff --git a/src/tracing.ts b/src/tracing.ts new file mode 100644 index 0000000..ef32fae --- /dev/null +++ b/src/tracing.ts @@ -0,0 +1,90 @@ +import { Context, type Effect } from "effect"; +import type { Tracing } from "playwright-core"; +import type { PlaywrightError } from "./errors"; +import { useHelper } from "./utils"; + +/** + * @category model + * @since 0.5.0 + */ +export interface PlaywrightTracingService { + /** + * Starts tracing. + * + * @see {@link Tracing.start} + * @since 0.5.0 + */ + readonly start: ( + options?: Parameters[0], + ) => Effect.Effect; + + /** + * Starts a new tracing chunk. + * + * @see {@link Tracing.startChunk} + * @since 0.5.0 + */ + readonly startChunk: ( + options?: Parameters[0], + ) => Effect.Effect; + + /** + * Stops a tracing chunk. + * + * @see {@link Tracing.stopChunk} + * @since 0.5.0 + */ + readonly stopChunk: ( + options?: Parameters[0], + ) => Effect.Effect; + + /** + * Stops tracing. + * + * @see {@link Tracing.stop} + * @since 0.5.0 + */ + readonly stop: ( + options?: Parameters[0], + ) => Effect.Effect; + + /** + * Starts HAR recording. + * + * @see {@link Tracing.startHar} + * @since 0.5.0 + */ + readonly startHar: ( + options: Parameters[0], + ) => Effect.Effect; + + /** + * Stops HAR recording. + * + * @see {@link Tracing.stopHar} + * @since 0.5.0 + */ + readonly stopHar: Effect.Effect; +} + +/** + * @category tag + */ +export class PlaywrightTracing extends Context.Tag( + "effect-playwright/PlaywrightTracing", +)() { + /** + * @category constructor + */ + static make(tracing: Tracing): PlaywrightTracingService { + const use = useHelper(tracing); + return PlaywrightTracing.of({ + start: (options) => use((t) => t.start(options)), + startChunk: (options) => use((t) => t.startChunk(options)), + stopChunk: (options) => use((t) => t.stopChunk(options)), + stop: (options) => use((t) => t.stop(options)), + startHar: (options) => use((t) => t.startHar(options).then(() => {})), + stopHar: use((t) => t.stopHar()), + }); + } +}