diff --git a/CHANGELOG.md b/CHANGELOG.md index 889a717..f27825f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ 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()`, `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 ### 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..0c4fba6 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. + * + * 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..2c66df3 100644 --- a/src/page.test.ts +++ b/src/page.test.ts @@ -92,6 +92,87 @@ 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("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 2a83af7..96270b9 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. + * + * 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. @@ -655,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. * @@ -915,7 +939,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)), @@ -1018,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),