Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
- **Navigation responses**: `Page.goto()`, `Frame.goto()` and `Page.reload()` now return the main resource response as `Option<Playwright.Response>`, 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<void, PlaywrightError>` 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.
- **Scoped tracing**: Added `Tracing.startScoped()` and `Tracing.startChunkScoped()`, which stop tracing when the current `Scope` closes, including on failure or interruption. Stop options such as `path` are passed as the second argument. This replaces Playwright 1.64's disposable return value from `start()` and `startChunk()`; the existing `start()` and `startChunk()` still succeed with `void`.

## 0.8.0

Expand Down
60 changes: 59 additions & 1 deletion src/browser-context.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { mkdtemp, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { assert, layer } from "@effect/vitest";
import { Effect, Option } from "effect";
import { Effect, Exit, Option } from "effect";
import { PlaywrightSpawner } from "effect-playwright";
import { chromium } from "playwright-core";
import { Browser } from "./browser";
Expand Down Expand Up @@ -131,4 +134,59 @@ layer(PlaywrightSpawner.layer(chromium))("BrowserContext", (it) => {
assert.strictEqual(afterDelete.length, 0);
}).pipe(PlaywrightSpawner.withBrowser),
);
it.effect("tracing.startScoped saves the trace when the scope fails", () =>
Effect.gen(function* () {
const browser = yield* Browser;
const context = yield* browser.newContext();
const dir = yield* Effect.promise(() =>
mkdtemp(join(tmpdir(), "effect-playwright-trace-")),
);
yield* Effect.addFinalizer(() =>
Effect.promise(() => rm(dir, { recursive: true, force: true })),
);
const path = join(dir, "trace.zip");

const exit = yield* Effect.gen(function* () {
yield* context.tracing.startScoped({ snapshots: true }, { path });
const page = yield* context.newPage;
yield* page.setContent("<h1>traced</h1>");
return yield* Effect.fail("workflow failed");
}).pipe(Effect.scoped, Effect.exit);

assert.isTrue(Exit.isFailure(exit));
const trace = yield* Effect.promise(() => stat(path));
assert.isAbove(trace.size, 0);

// Tracing was stopped, so it can be started again.
yield* context.tracing.start();
yield* context.tracing.stop();
}).pipe(PlaywrightSpawner.withBrowser),
);

it.effect(
"tracing.startChunkScoped saves the chunk when the scope closes",
() =>
Effect.gen(function* () {
const browser = yield* Browser;
const context = yield* browser.newContext();
const dir = yield* Effect.promise(() =>
mkdtemp(join(tmpdir(), "effect-playwright-trace-")),
);
yield* Effect.addFinalizer(() =>
Effect.promise(() => rm(dir, { recursive: true, force: true })),
);
const path = join(dir, "chunk.zip");

yield* context.tracing.start();
yield* Effect.gen(function* () {
yield* context.tracing.startChunkScoped(undefined, { path });
const page = yield* context.newPage;
yield* page.setContent("<h1>chunk</h1>");
}).pipe(Effect.scoped);

const chunk = yield* Effect.promise(() => stat(path));
assert.isAbove(chunk.size, 0);
yield* context.tracing.stop();
}).pipe(PlaywrightSpawner.withBrowser),
);
});
84 changes: 79 additions & 5 deletions src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @since 0.5.0
*/

import { Context, type Effect } from "effect";
import { Context, Effect, type Scope } from "effect";
import type { Tracing as CoreTracing } from "playwright-core";
import type { PlaywrightError } from "./errors";
import { useHelper } from "./utils";
Expand Down Expand Up @@ -34,6 +34,65 @@ export interface Tracing {
options?: Parameters<CoreTracing["startChunk"]>[0],
) => Effect.Effect<void, PlaywrightError>;

/**
* Starts tracing and stops it when the current `Scope` closes.
*
* **When to use**
*
* Use instead of pairing {@link Tracing.start} with {@link Tracing.stop}, so
* the trace is stopped even when the workflow fails or is interrupted.
*
* **Gotchas**
*
* Pass `stopOptions.path` to save the trace; without it Playwright discards
* the recording. A failure while stopping is logged instead of failing the
* scope.
*
* @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);
* const context = yield* browser.newContext();
* yield* context.tracing.startScoped(
* { screenshots: true, snapshots: true },
* { path: "trace.zip" },
* );
* const page = yield* context.newPage;
* yield* page.goto("https://example.com");
* }).pipe(Effect.scoped, Effect.provide(Playwright.layer));
* ```
*
* @param options - Options passed to {@link Tracing.start}.
* @param stopOptions - Options passed to {@link Tracing.stop} when the scope closes.
* @see {@link CoreTracing.start}
* @since 0.9.0
*/
readonly startScoped: (
options?: Parameters<CoreTracing["start"]>[0],
stopOptions?: Parameters<CoreTracing["stop"]>[0],
) => Effect.Effect<void, PlaywrightError, Scope.Scope>;

/**
* Starts a new tracing chunk and stops it when the current `Scope` closes.
*
* Tracing must already be started. Pass `stopOptions.path` to save the
* chunk; a failure while stopping is logged instead of failing the scope.
*
* @param options - Options passed to {@link Tracing.startChunk}.
* @param stopOptions - Options passed to {@link Tracing.stopChunk} when the scope closes.
* @see {@link CoreTracing.startChunk}
* @since 0.9.0
*/
readonly startChunkScoped: (
options?: Parameters<CoreTracing["startChunk"]>[0],
stopOptions?: Parameters<CoreTracing["stopChunk"]>[0],
) => Effect.Effect<void, PlaywrightError, Scope.Scope>;

/**
* Stops a tracing chunk.
*
Expand Down Expand Up @@ -89,11 +148,26 @@ export const Tracing = Context.Service<Tracing>(
*/
export const makeTracing = (tracing: CoreTracing): Tracing => {
const use = useHelper(tracing);
const start: Tracing["start"] = (options) =>
use((t) => t.start(options).then(() => {}));
const startChunk: Tracing["startChunk"] = (options) =>
use((t) => t.startChunk(options).then(() => {}));
const stopChunk: Tracing["stopChunk"] = (options) =>
use((t) => t.stopChunk(options));
const stop: Tracing["stop"] = (options) => use((t) => t.stop(options));
return Tracing.of({
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)),
start,
startChunk,
startScoped: (options, stopOptions) =>
Effect.acquireRelease(start(options), () =>
stop(stopOptions).pipe(Effect.ignore({ log: true })),
),
startChunkScoped: (options, stopOptions) =>
Effect.acquireRelease(startChunk(options), () =>
stopChunk(stopOptions).pipe(Effect.ignore({ log: true })),
),
stopChunk,
stop,
startHar: (options) => use((t) => t.startHar(options).then(() => {})),
stopHar: use((t) => t.stopHar()),
});
Expand Down
Loading