diff --git a/biome.json b/biome.json index e9e7735..3ad466e 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/src/browser-context.test.ts b/src/browser-context.test.ts index 4a6f78a..d61fe1a 100644 --- a/src/browser-context.test.ts +++ b/src/browser-context.test.ts @@ -1,5 +1,5 @@ import { assert, layer } from "@effect/vitest"; -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import { chromium } from "playwright-core"; import { PlaywrightBrowser } from "./browser"; import { PlaywrightEnvironment } from "./experimental"; @@ -11,6 +11,46 @@ type TestWindow = Window & { layer(PlaywrightEnvironment.layer(chromium))( "PlaywrightBrowserContext", (it) => { + it.scoped("should wrap context methods", () => + Effect.gen(function* () { + const browser = yield* PlaywrightBrowser; + const context = yield* browser.newContext(); + + // Test browser() + const contextBrowser = context.browser(); + assert.isTrue(Option.isSome(contextBrowser)); + + // Test cookies/addCookies/clearCookies + yield* context.addCookies([ + { + name: "test-cookie", + value: "test-value", + url: "https://example.com", + }, + ]); + const cookies = yield* context.cookies(["https://example.com"]); + assert.strictEqual(cookies.length, 1); + assert.strictEqual(cookies[0].name, "test-cookie"); + + yield* context.clearCookies(); + const cookiesAfterClear = yield* context.cookies([ + "https://example.com", + ]); + assert.strictEqual(cookiesAfterClear.length, 0); + + // Test grantPermissions/clearPermissions + yield* context.grantPermissions(["notifications"]); + yield* context.clearPermissions; + + // Test setters + context.setDefaultNavigationTimeout(30000); + context.setDefaultTimeout(30000); + yield* context.setExtraHTTPHeaders({ "X-Test": "test" }); + yield* context.setGeolocation({ latitude: 52, longitude: 13 }); + yield* context.setOffline(false); + }).pipe(PlaywrightEnvironment.withBrowser), + ); + it.scoped("addInitScript should execute script in all new pages", () => Effect.gen(function* () { const browser = yield* PlaywrightBrowser; diff --git a/src/browser-context.ts b/src/browser-context.ts index 6f27225..def8e3a 100644 --- a/src/browser-context.ts +++ b/src/browser-context.ts @@ -1,4 +1,4 @@ -import { Context, Effect, identity, Stream } from "effect"; +import { Context, Effect, identity, Option, Stream } from "effect"; import type { BrowserContext, ConsoleMessage, @@ -9,6 +9,7 @@ import type { WebError, Worker, } from "playwright-core"; +import { PlaywrightBrowser, type PlaywrightBrowserService } from "./browser"; import { PlaywrightClock, type PlaywrightClockService } from "./clock"; import { PlaywrightDialog, @@ -105,6 +106,114 @@ export interface PlaywrightBrowserContextService { arg?: Parameters[1], ) => Effect.Effect; + /** + * Returns the browser that the context belongs to. + * + * @see {@link BrowserContext.browser} + * @since 0.4.0 + */ + readonly browser: () => Option.Option; + + /** + * Clears the cookies from the browser context. + * + * @see {@link BrowserContext.clearCookies} + * @since 0.4.0 + */ + readonly clearCookies: (options?: { + name?: string | RegExp; + domain?: string | RegExp; + path?: string | RegExp; + }) => Effect.Effect; + + /** + * Clears the permissions from the browser context. + * + * @see {@link BrowserContext.clearPermissions} + * @since 0.4.0 + */ + readonly clearPermissions: Effect.Effect; + + /** + * Returns the cookies for the browser context. + * + * @see {@link BrowserContext.cookies} + * @since 0.4.0 + */ + readonly cookies: ( + urls?: string | string[], + ) => Effect.Effect< + Awaited>, + PlaywrightError + >; + + /** + * Sets the cookies for the browser context. + * + * @see {@link BrowserContext.addCookies} + * @since 0.4.0 + */ + readonly addCookies: ( + cookies: Parameters[0], + ) => Effect.Effect; + + /** + * Grants permissions to the browser context. + * + * @see {@link BrowserContext.grantPermissions} + * @since 0.4.0 + */ + readonly grantPermissions: ( + permissions: Parameters[0], + options?: Parameters[1], + ) => Effect.Effect; + + /** + * Sets the extra HTTP headers for the browser context. + * + * @see {@link BrowserContext.setExtraHTTPHeaders} + * @since 0.4.0 + */ + readonly setExtraHTTPHeaders: ( + headers: Parameters[0], + ) => Effect.Effect; + + /** + * Sets the geolocation for the browser context. + * + * @see {@link BrowserContext.setGeolocation} + * @since 0.4.0 + */ + readonly setGeolocation: ( + geolocation: Parameters[0], + ) => Effect.Effect; + + /** + * Sets the offline state for the browser context. + * + * @see {@link BrowserContext.setOffline} + * @since 0.4.0 + */ + readonly setOffline: ( + offline: boolean, + ) => Effect.Effect; + + /** + * Sets the default navigation timeout for the browser context. + * + * @see {@link BrowserContext.setDefaultNavigationTimeout} + * @since 0.4.0 + */ + readonly setDefaultNavigationTimeout: (timeout: number) => void; + + /** + * Sets the default timeout for the browser context. + * + * @see {@link BrowserContext.setDefaultTimeout} + * @since 0.4.0 + */ + readonly setDefaultTimeout: (timeout: number) => void; + /** * Creates a stream of the given event from the browser context. * @@ -144,6 +253,24 @@ export class PlaywrightBrowserContext extends Context.Tag( newPage: use((c) => c.newPage().then(PlaywrightPage.make)), close: use((c) => c.close()), addInitScript: (script, arg) => use((c) => c.addInitScript(script, arg)), + browser: () => + Option.fromNullable(context.browser()).pipe( + Option.map(PlaywrightBrowser.make), + ), + clearCookies: (options) => use((c) => c.clearCookies(options)), + clearPermissions: use((c) => c.clearPermissions()), + cookies: (urls) => use((c) => c.cookies(urls)), + addCookies: (cookies) => use((c) => c.addCookies(cookies)), + grantPermissions: (permissions, options) => + use((c) => c.grantPermissions(permissions, options)), + setExtraHTTPHeaders: (headers) => + use((c) => c.setExtraHTTPHeaders(headers)), + setGeolocation: (geolocation) => + use((c) => c.setGeolocation(geolocation)), + setOffline: (offline) => use((c) => c.setOffline(offline)), + setDefaultNavigationTimeout: (timeout) => + context.setDefaultNavigationTimeout(timeout), + setDefaultTimeout: (timeout) => context.setDefaultTimeout(timeout), eventStream: (event: K) => Stream.asyncPush((emit) => Effect.acquireRelease( diff --git a/src/frame.test.ts b/src/frame.test.ts index ec485e4..22cf3ef 100644 --- a/src/frame.test.ts +++ b/src/frame.test.ts @@ -1,5 +1,5 @@ import { assert, layer } from "@effect/vitest"; -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import { chromium } from "playwright-core"; import { PlaywrightBrowser } from "./browser"; import { PlaywrightEnvironment } from "./experimental"; @@ -58,9 +58,59 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightFrame", (it) => { const byText = yield* frame.getByText("Hello from Frame").count; assert.strictEqual(byText, 1); + // Test getByPlaceholder + yield* frame.evaluate(() => { + const input = document.createElement("input"); + input.placeholder = "Search..."; + document.body.appendChild(input); + }); + const byPlaceholder = yield* frame.getByPlaceholder("Search...").count; + assert.strictEqual(byPlaceholder, 1); + + // Test getByAltText + yield* frame.evaluate(() => { + const img = document.createElement("img"); + img.alt = "Playwright Logo"; + document.body.appendChild(img); + }); + const byAltText = yield* frame.getByAltText("Playwright Logo").count; + assert.strictEqual(byAltText, 1); + + // Test getByTitle + yield* frame.evaluate(() => { + const span = document.createElement("span"); + span.title = "Tooltip"; + document.body.appendChild(span); + }); + const byTitle = yield* frame.getByTitle("Tooltip").count; + assert.strictEqual(byTitle, 1); + // Test name const name = frame.name(); assert.strictEqual(name, "test-frame"); + + // Test page + const framePage = frame.page(); + assert.isOk(framePage); + + // Test parentFrame + const parent = frame.parentFrame(); + assert.isTrue(Option.isSome(parent)); + + // Test childFrames + const children = frame.childFrames(); + assert.strictEqual(children.length, 0); + + // Test isDetached + assert.isFalse(frame.isDetached()); + + // Test waitForTimeout + yield* frame.waitForTimeout(100); + + // Test setContent + yield* frame.setContent("

New Content

"); + const newContent = yield* frame.content; + assert.isTrue(newContent.includes("New Content")); }).pipe(PlaywrightEnvironment.withBrowser), ); diff --git a/src/frame.ts b/src/frame.ts index 5c2f599..b0460b2 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -1,7 +1,8 @@ -import { Context, type Effect } from "effect"; +import { Array, Context, Option, type Effect } from "effect"; import type { Frame } from "playwright-core"; import type { PlaywrightError } from "./errors"; import { PlaywrightLocator } from "./locator"; +import { PlaywrightPage, type PlaywrightPageService } from "./page"; import type { PageFunction } from "./playwright-types"; import { useHelper } from "./utils"; @@ -119,6 +120,92 @@ export interface PlaywrightFrameService { testId: Parameters[0], ) => typeof PlaywrightLocator.Service; + /** + * Returns a locator that matches the given placeholder. + * + * @see {@link Frame.getByPlaceholder} + * @since 0.4.0 + */ + readonly getByPlaceholder: ( + text: Parameters[0], + options?: Parameters[1], + ) => typeof PlaywrightLocator.Service; + + /** + * Returns a locator that matches the given alt text. + * + * @see {@link Frame.getByAltText} + * @since 0.4.0 + */ + readonly getByAltText: ( + text: Parameters[0], + options?: Parameters[1], + ) => typeof PlaywrightLocator.Service; + + /** + * Returns a locator that matches the given title. + * + * @see {@link Frame.getByTitle} + * @since 0.4.0 + */ + readonly getByTitle: ( + text: Parameters[0], + options?: Parameters[1], + ) => typeof PlaywrightLocator.Service; + + /** + * Returns the page that the frame belongs to. + * + * @see {@link Frame.page} + * @since 0.4.0 + */ + readonly page: () => PlaywrightPageService; + + /** + * Returns the parent frame, if any. + * + * @see {@link Frame.parentFrame} + * @since 0.4.0 + */ + readonly parentFrame: () => Option.Option; + + /** + * Returns an array of child frames. + * + * @see {@link Frame.childFrames} + * @since 0.4.0 + */ + readonly childFrames: () => ReadonlyArray; + + /** + * Returns whether the frame is detached. + * + * @see {@link Frame.isDetached} + * @since 0.4.0 + */ + readonly isDetached: () => boolean; + + /** + * Waits for the given timeout in milliseconds. + * + * @see {@link Frame.waitForTimeout} + * @since 0.4.0 + */ + readonly waitForTimeout: ( + timeout: number, + ) => Effect.Effect; + + /** + * Sets the HTML content of the frame. + * + * @see {@link Frame.setContent} + * @since 0.4.0 + */ + readonly setContent: ( + html: string, + options?: Parameters[1], + ) => Effect.Effect; + /** * Returns the current URL of the frame. * @@ -192,6 +279,22 @@ export class PlaywrightFrame extends Context.Tag( PlaywrightLocator.make(frame.getByLabel(label, options)), getByTestId: (testId) => PlaywrightLocator.make(frame.getByTestId(testId)), + getByPlaceholder: (text, options) => + PlaywrightLocator.make(frame.getByPlaceholder(text, options)), + getByAltText: (text, options) => + PlaywrightLocator.make(frame.getByAltText(text, options)), + getByTitle: (text, options) => + PlaywrightLocator.make(frame.getByTitle(text, options)), + page: () => PlaywrightPage.make(frame.page()), + parentFrame: () => + Option.fromNullable(frame.parentFrame()).pipe( + Option.map(PlaywrightFrame.make), + ), + childFrames: () => + Array.map(frame.childFrames(), (f) => PlaywrightFrame.make(f)), + isDetached: () => frame.isDetached(), + waitForTimeout: (timeout) => use((f) => f.waitForTimeout(timeout)), + setContent: (html, options) => use((f) => f.setContent(html, options)), url: () => frame.url(), content: use((f) => f.content()), name: () => frame.name(), diff --git a/src/page.test.ts b/src/page.test.ts index 34ef0f3..f223fbb 100644 --- a/src/page.test.ts +++ b/src/page.test.ts @@ -92,6 +92,18 @@ layer(PlaywrightEnvironment.layer(chromium))("PlaywrightPage", (it) => { }).pipe(PlaywrightEnvironment.withBrowser), ); + it.scoped("waitForTimeout should wait", () => + Effect.gen(function* () { + const browser = yield* PlaywrightBrowser; + const page = yield* browser.newPage(); + + const start = Date.now(); + yield* page.waitForTimeout(100); + const end = Date.now(); + assert(end - start >= 100); + }).pipe(PlaywrightEnvironment.withBrowser), + ); + it.scoped( "evaluate should run code in the page context with destructured arg", () => diff --git a/src/page.ts b/src/page.ts index c9cbc6e..478cf81 100644 --- a/src/page.ts +++ b/src/page.ts @@ -139,6 +139,15 @@ export interface PlaywrightPageService { html: string, options?: Parameters[1], ) => Effect.Effect; + /** + * Waits for the given timeout in milliseconds. + * + * @see {@link Page.waitForTimeout} + * @since 0.4.0 + */ + readonly waitForTimeout: ( + timeout: number, + ) => Effect.Effect; /** * This setting will change the default maximum navigation time for the following methods: * - {@link PlaywrightPageService.goBack} @@ -787,6 +796,7 @@ export class PlaywrightPage extends Context.Tag( touchscreen: PlaywrightTouchscreen.make(page.touchscreen), 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)), setDefaultNavigationTimeout: (timeout) => page.setDefaultNavigationTimeout(timeout), setDefaultTimeout: (timeout) => page.setDefaultTimeout(timeout),