diff --git a/src/experimental/browser-utils.test.ts b/src/experimental/browser-utils.test.ts
new file mode 100644
index 0000000..a697af4
--- /dev/null
+++ b/src/experimental/browser-utils.test.ts
@@ -0,0 +1,105 @@
+import { assert, layer } from "@effect/vitest";
+import { Chunk, Effect, Fiber, Stream } from "effect";
+import { chromium } from "playwright-core";
+import { Playwright } from "../index";
+import * as BrowserUtils from "./browser-utils";
+
+layer(Playwright.layer)("BrowserUtils", (it) => {
+ it.scoped("allPages should return all pages from all contexts", () =>
+ Effect.gen(function* () {
+ const playwright = yield* Playwright;
+ const browser = yield* playwright.launchScoped(chromium);
+
+ const context1 = yield* browser.newContext();
+ yield* context1.newPage;
+ yield* context1.newPage;
+
+ const context2 = yield* browser.newContext();
+ yield* context2.newPage;
+
+ const pages = yield* BrowserUtils.allPages(browser);
+ assert.strictEqual(pages.length, 3);
+ }),
+ );
+
+ it.scoped("allFrames should return all frames from all pages", () =>
+ Effect.gen(function* () {
+ const playwright = yield* Playwright;
+ const browser = yield* playwright.launchScoped(chromium);
+
+ yield* browser.newPage();
+ yield* browser.newPage();
+
+ // Retrieve frames
+ const frames = yield* BrowserUtils.allFrames(browser);
+
+ // Each page has at least one frame (the main frame)
+ assert.strictEqual(frames.length, 2);
+ }),
+ );
+
+ it.scoped(
+ "allFrameNavigatedEventStream should capture navigations from existing and new pages across multiple contexts",
+ () =>
+ Effect.gen(function* () {
+ const playwright = yield* Playwright;
+ const browser = yield* playwright.launchScoped(chromium);
+
+ // Setup contexts and an initial page
+ const context1 = yield* browser.newContext();
+ const context2 = yield* browser.newContext();
+ const page1 = yield* context1.newPage;
+
+ // Start the event stream
+ const stream = BrowserUtils.allFrameNavigatedEventStream(browser);
+ const eventFiber = yield* stream.pipe(Stream.runCollect, Effect.fork);
+
+ // 1. Navigate existing page
+ yield* page1.goto(
+ "data:text/html,
Page 1 (existing in context 1)
",
+ );
+
+ // 2. Create new page in context 1 and navigate
+ const page2 = yield* context1.newPage;
+ yield* page2.goto(
+ "data:text/html,Page 2 (new in context 1)
",
+ );
+
+ // 3. Create new page in context 2 and navigate
+ const page3 = yield* context2.newPage;
+
+ yield* page3.goto(
+ "data:text/html,Page 3 (new in context 2)
",
+ );
+
+ // 4. Create new page in context 2 and navigate
+ const page4 = yield* context2.newPage;
+
+ yield* page4.goto(
+ "data:text/html,Page 4 (new in context 2)
",
+ );
+
+ yield* browser.close;
+
+ const events = yield* Fiber.join(eventFiber);
+ assert.strictEqual(Chunk.size(events), 4);
+ }),
+ );
+
+ it.scoped("page eventStream should capture framenavigated", () =>
+ Effect.gen(function* () {
+ const playwright = yield* Playwright;
+ const browser = yield* playwright.launchScoped(chromium);
+ const page = yield* browser.newPage();
+
+ const fiber = yield* BrowserUtils.allFrameNavigatedEventStream(
+ browser,
+ ).pipe(Stream.take(1), Stream.runCollect, Effect.fork);
+
+ yield* page.goto("https://example.com");
+
+ const events = yield* Fiber.join(fiber);
+ assert.strictEqual(Chunk.size(events), 1);
+ }),
+ );
+});
diff --git a/src/experimental/browser-utils.ts b/src/experimental/browser-utils.ts
new file mode 100644
index 0000000..e126575
--- /dev/null
+++ b/src/experimental/browser-utils.ts
@@ -0,0 +1,58 @@
+import { Array, Effect, pipe, Stream } from "effect";
+import type { PlaywrightBrowserService } from "../browser";
+
+/**
+ * Returns all pages in the browser from all contexts.
+ * @category util
+ */
+export const allPages = (browser: PlaywrightBrowserService) =>
+ browser.contexts.pipe(
+ Effect.flatMap((contexts) =>
+ Effect.all(contexts.map((context) => context.pages)),
+ ),
+ Effect.map(Array.flatten),
+ );
+
+/**
+ * Returns all frames in the browser from all pages in all contexts.
+ * @category util
+ */
+export const allFrames = (browser: PlaywrightBrowserService) =>
+ allPages(browser).pipe(
+ Effect.flatMap((pages) => Effect.all(pages.map((page) => page.frames))),
+ );
+
+/**
+ * Returns a stream of all framenavigated events for all current and future pages in the browser.
+ * In all current contexts (but not future contexts).
+ * @category util
+ */
+export const allFrameNavigatedEventStream = (
+ browser: PlaywrightBrowserService,
+) =>
+ Effect.gen(function* () {
+ const contexts = yield* browser.contexts;
+ const pages = yield* pipe(
+ contexts.map((c) => c.pages),
+ Effect.all,
+ Effect.map(Array.flatten),
+ );
+
+ // listen for framenavigated for all current pages
+ const currentPages = pages.map((page) =>
+ page.eventStream("framenavigated"),
+ );
+
+ // and all future pages
+ const newPages = pipe(
+ contexts.map((c) => c.eventStream("page")),
+ Stream.mergeAll({ concurrency: "unbounded" }),
+ Stream.flatMap((page) => page.eventStream("framenavigated"), {
+ concurrency: "unbounded",
+ }),
+ );
+
+ return Stream.mergeAll([newPages, ...currentPages], {
+ concurrency: "unbounded",
+ });
+ }).pipe(Stream.unwrap);
diff --git a/src/experimental/index.ts b/src/experimental/index.ts
index eb2d70b..5272085 100644
--- a/src/experimental/index.ts
+++ b/src/experimental/index.ts
@@ -5,4 +5,5 @@
* @packageDocumentation
*/
+export * as BrowserUtils from "./browser-utils";
export * as PlaywrightEnvironment from "./environment";