From 41c3dc2bc8c7420af8b89c474b7a189d83b5fe3e Mon Sep 17 00:00:00 2001 From: kedom Date: Sat, 10 Oct 2026 19:39:45 +0200 Subject: [PATCH 1/3] Return the main resource response from goto Page.goto and Frame.goto dropped the response that Playwright resolves with, so an error status such as 404 could only be read through use. Return it as Option, like goBack and goForward. --- CHANGELOG.md | 6 ++++++ src/frame.test.ts | 22 ++++++++++++++++++++++ src/frame.ts | 18 ++++++++++++++---- src/page.test.ts | 33 +++++++++++++++++++++++++++++++++ src/page.ts | 28 +++++++++++++++++++++++++--- 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 889a717..9675b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## Unreleased + +### Breaking Changes + +- **Navigation responses**: `Page.goto()` and `Frame.goto()` 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`. + ## 0.8.0 ### Breaking Changes diff --git a/src/frame.test.ts b/src/frame.test.ts index e7b7b02..96d7db5 100644 --- a/src/frame.test.ts +++ b/src/frame.test.ts @@ -147,6 +147,28 @@ layer(PlaywrightSpawner.layer(chromium))("Frame", (it) => { }).pipe(PlaywrightSpawner.withBrowser), ); + it.effect("goto should return the main resource response", () => + Effect.gen(function* () { + const browser = yield* Browser; + const page = yield* browser.newPage(); + + yield* page.use((playwrightPage) => + playwrightPage.route("http://frame.test/", (route) => + route.fulfill({ + status: 404, + body: "Not found", + }), + ), + ); + const response = yield* page.mainFrame().goto("http://frame.test/"); + + assert.deepStrictEqual( + Option.map(response, (response) => response.status()), + Option.some(404), + ); + }).pipe(PlaywrightSpawner.withBrowser), + ); + it.effect("waitForLoadState should resolve on frame", () => Effect.gen(function* () { const browser = yield* Browser; diff --git a/src/frame.ts b/src/frame.ts index b1ca941..aefac09 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -4,8 +4,9 @@ * @since 0.1.2 */ -import { Array, Context, type Effect, Option } from "effect"; +import { Array, Context, Effect, Option } from "effect"; import type { Frame as CoreFrame, ElementHandle } from "playwright-core"; +import { makeResponse, type Response } from "./common"; import type { PlaywrightError } from "./errors"; import { type FrameLocator, makeFrameLocator } from "./frame-locator"; import { type Locator, makeLocator } from "./locator"; @@ -19,7 +20,12 @@ import { useHelper } from "./utils"; */ export interface Frame { /** - * Navigates the frame to the given URL. + * Navigates the frame to the given URL and returns the main resource + * response, or `None` when there is none, such as for `about:blank` or a + * change of the URL hash. + * + * Like Playwright, it succeeds whatever HTTP status the server answers with, + * including 404 and 500, so read the status from the response. * * @see {@link CoreFrame.goto} * @since 0.1.3 @@ -27,7 +33,7 @@ export interface Frame { readonly goto: ( url: string, options?: Parameters[1], - ) => Effect.Effect; + ) => Effect.Effect, PlaywrightError>; /** * Waits for the frame to navigate to the given URL. * @@ -288,7 +294,11 @@ export const makeFrame = (frame: CoreFrame): Frame => { const use = useHelper(frame); return Frame.of({ - goto: (url, options) => use((f) => f.goto(url, options)), + goto: (url, options) => + use((f) => f.goto(url, options)).pipe( + Effect.map(Option.fromNullishOr), + Effect.map(Option.map(makeResponse)), + ), waitForURL: (url, options) => use((f) => f.waitForURL(url, options)), waitForLoadState: (state, options) => use((f) => f.waitForLoadState(state, options)), diff --git a/src/page.test.ts b/src/page.test.ts index bdac084..7f398f6 100644 --- a/src/page.test.ts +++ b/src/page.test.ts @@ -92,6 +92,39 @@ layer(PlaywrightSpawner.layer(chromium))("Page", (it) => { }).pipe(PlaywrightSpawner.withBrowser), ); + it.effect("goto should return the main resource response", () => + Effect.gen(function* () { + const browser = yield* Browser; + const page = yield* browser.newPage(); + + yield* page.use((playwrightPage) => + playwrightPage.route("http://status.test/", (route) => + route.fulfill({ + status: 404, + body: "Not found", + }), + ), + ); + const response = yield* page.goto("http://status.test/"); + + assert.deepStrictEqual( + Option.map(response, (response) => [response.status(), response.ok()]), + Option.some([404, false]), + ); + }).pipe(PlaywrightSpawner.withBrowser), + ); + + it.effect("goto should return None when there is no response", () => + Effect.gen(function* () { + const browser = yield* Browser; + const page = yield* browser.newPage(); + + const response = yield* page.goto("about:blank"); + + assert.isTrue(Option.isNone(response)); + }).pipe(PlaywrightSpawner.withBrowser), + ); + it.effect("waitForTimeout should wait", () => Effect.gen(function* () { const browser = yield* Browser; diff --git a/src/page.ts b/src/page.ts index 2a83af7..23de98e 100644 --- a/src/page.ts +++ b/src/page.ts @@ -200,14 +200,32 @@ export interface Page { */ readonly sessionStorage: WebStorage; /** - * Navigates the page to the given URL. + * 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 + * change of the URL hash. + * + * Like Playwright, it succeeds whatever HTTP status the server answers with, + * including 404 and 500, so read the status from the response. + * + * **Example** (Reading the status of the page) + * + * ```ts + * import { Effect, Option } from "effect"; + * import { Playwright } from "effect-playwright"; + * + * const status = Effect.gen(function* () { + * const page = yield* Playwright.Page; + * const response = yield* page.goto("https://example.com"); + * return Option.map(response, (response) => response.status()); + * }); + * ``` * @see {@link CorePage.goto} * @since 0.1.0 */ readonly goto: ( url: string, options?: Parameters[1], - ) => Effect.Effect; + ) => Effect.Effect, PlaywrightError>; /** * This method internally calls [document.write()](https://developer.mozilla.org/en-US/docs/Web/API/Document/write), * inheriting all its specific characteristics and behaviors. @@ -915,7 +933,11 @@ export const makePage = (page: CorePage): Page => { touchscreen: makeTouchscreen(page.touchscreen), screencast: makeScreencast(page.screencast), sessionStorage: makeWebStorage(page.sessionStorage), - goto: (url, options) => use((page) => page.goto(url, options)), + goto: (url, options) => + use((page) => page.goto(url, options)).pipe( + Effect.map(Option.fromNullishOr), + Effect.map(Option.map(makeResponse)), + ), setContent: (html, options) => use((page) => page.setContent(html, options)), waitForTimeout: (timeout) => use((page) => page.waitForTimeout(timeout)), From 6b8fd0c3b9f1ec531d0d8d1e5dffee9797d2b2ba Mon Sep 17 00:00:00 2001 From: kedom Date: Wed, 14 Oct 2026 19:14:31 +0200 Subject: [PATCH 2/3] List the goto response as a feature Code that ignores the result keeps working, so the change is not breaking. Drop the comparison with Playwright from the goto docs, since the whole library mirrors Playwright. --- CHANGELOG.md | 2 +- src/frame.ts | 4 ++-- src/page.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9675b35..c47a4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. ## Unreleased -### Breaking Changes +### Features - **Navigation responses**: `Page.goto()` and `Frame.goto()` 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`. diff --git a/src/frame.ts b/src/frame.ts index aefac09..0c4fba6 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -24,8 +24,8 @@ export interface Frame { * response, or `None` when there is none, such as for `about:blank` or a * change of the URL hash. * - * Like Playwright, it succeeds whatever HTTP status the server answers with, - * including 404 and 500, so read the status from the response. + * It succeeds whatever HTTP status the server answers with, including 404 + * and 500, so read the status from the response. * * @see {@link CoreFrame.goto} * @since 0.1.3 diff --git a/src/page.ts b/src/page.ts index 23de98e..03bbf17 100644 --- a/src/page.ts +++ b/src/page.ts @@ -204,8 +204,8 @@ export interface Page { * response, or `None` when there is none, such as for `about:blank` or a * change of the URL hash. * - * Like Playwright, it succeeds whatever HTTP status the server answers with, - * including 404 and 500, so read the status from the response. + * It succeeds whatever HTTP status the server answers with, including 404 + * and 500, so read the status from the response. * * **Example** (Reading the status of the page) * From b427ea6efb3bbd3a6d2c4116d738d46778fc051a Mon Sep 17 00:00:00 2001 From: kedom Date: Wed, 14 Oct 2026 19:14:31 +0200 Subject: [PATCH 3/3] Return the main resource response from reload Page.reload dropped the response that Playwright resolves with and took no options. Make it a method that takes the reload options and returns Option, like goto, goBack and goForward. --- CHANGELOG.md | 6 +++++- src/page.test.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/page.ts | 16 +++++++++++++--- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47a4e2..f27825f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,13 @@ All notable changes to this project will be documented in this file. ## Unreleased +### 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()`. + ### Features -- **Navigation responses**: `Page.goto()` and `Frame.goto()` 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`. +- **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`. ## 0.8.0 diff --git a/src/page.test.ts b/src/page.test.ts index 7f398f6..2c66df3 100644 --- a/src/page.test.ts +++ b/src/page.test.ts @@ -125,6 +125,54 @@ layer(PlaywrightSpawner.layer(chromium))("Page", (it) => { }).pipe(PlaywrightSpawner.withBrowser), ); + it.effect("reload should return the response of the reloaded page", () => + Effect.gen(function* () { + const browser = yield* Browser; + const page = yield* browser.newPage(); + let requests = 0; + + yield* page.use((playwrightPage) => + playwrightPage.route("http://reload.test/", (route) => { + requests++; + return route.fulfill({ + status: requests === 1 ? 200 : 503, + body: "Reload", + }); + }), + ); + yield* page.goto("http://reload.test/"); + const response = yield* page.reload(); + + assert.deepStrictEqual( + Option.map(response, (response) => response.status()), + Option.some(503), + ); + }).pipe(PlaywrightSpawner.withBrowser), + ); + + it.effect("reload should time out after the timeout option", () => + Effect.gen(function* () { + const browser = yield* Browser; + const page = yield* browser.newPage(); + let requests = 0; + + yield* page.use((playwrightPage) => + playwrightPage.route("http://reload.test/", async (route) => { + requests++; + if (requests === 1) { + await route.fulfill({ + body: "Reload", + }); + } + }), + ); + yield* page.goto("http://reload.test/"); + const error = yield* page.reload({ timeout: 100 }).pipe(Effect.flip); + + assert.strictEqual(error.reason, "Timeout"); + }).pipe(PlaywrightSpawner.withBrowser), + ); + it.effect("waitForTimeout should wait", () => Effect.gen(function* () { const browser = yield* Browser; diff --git a/src/page.ts b/src/page.ts index 03bbf17..96270b9 100644 --- a/src/page.ts +++ b/src/page.ts @@ -673,12 +673,18 @@ export interface Page { ) => Effect.Effect; /** - * Reloads the page. + * Reloads the page and returns the main resource response, or `None` when + * there is none, such as for `about:blank`. + * + * It succeeds whatever HTTP status the server answers with, including 404 + * and 500, so read the status from the response. * * @see {@link CorePage.reload} * @since 0.1.0 */ - readonly reload: Effect.Effect; + readonly reload: ( + options?: Parameters[0], + ) => Effect.Effect, PlaywrightError>; /** * Navigate to the previous page in history. * @@ -1040,7 +1046,11 @@ export const makePage = (page: CorePage): Page => { ), frames: use((page) => Promise.resolve(page.frames().map(makeFrame))), mainFrame: () => makeFrame(page.mainFrame()), - reload: use((page) => page.reload()), + reload: (options) => + use((page) => page.reload(options)).pipe( + Effect.map(Option.fromNullishOr), + Effect.map(Option.map(makeResponse)), + ), goBack: (options) => use((page) => page.goBack(options)).pipe( Effect.map(Option.fromNullishOr),