barebrowse 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/prd.md ADDED
@@ -0,0 +1,284 @@
1
+ # barebrowse — Product Requirements Document
2
+
3
+ **Version:** 1.0
4
+ **Date:** 2026-02-22
5
+ **Status:** POC
6
+
7
+ ---
8
+
9
+ ## What barebrowse is
10
+
11
+ A standalone vanilla JavaScript library that gives autonomous agents authenticated access to the web through the user's own Chromium browser. One package, one import, three modes.
12
+
13
+ ```js
14
+ import { browse } from 'barebrowse';
15
+ const snapshot = await browse('https://any-page.com');
16
+ ```
17
+
18
+ barebrowse handles: finding the browser, connecting via CDP, injecting cookies, navigating, extracting the ARIA accessibility tree, and pruning it down to what an agent actually needs. The output is a clean, token-efficient snapshot of any web page — authenticated as the real user.
19
+
20
+ ## What barebrowse is NOT
21
+
22
+ - **Not a framework.** No plugin system, no config files, no lifecycle hooks.
23
+ - **Not an MCP server.** But trivially wrappable as one (~30 lines).
24
+ - **Not Playwright.** No bundled browser, no cross-engine abstraction, no 200MB download.
25
+ - **Not an agent.** No LLM, no planning, no orchestration — that's bareagent's job.
26
+ - **Not a scraper.** It browses as the user, not as a bot harvesting data.
27
+
28
+ ---
29
+
30
+ ## The Problem
31
+
32
+ Every AI agent that needs to read or interact with the web hits the same walls:
33
+
34
+ 1. **Cloudflare / bot detection** — headless browsers get blocked
35
+ 2. **Authentication** — sites require login, OAuth, session cookies
36
+ 3. **Token bloat** — raw DOM is 100K+ tokens; agents need ~5K
37
+ 4. **Two consumers, same need** — research agents (read pages) and personal assistants (click/type) both need an authenticated browser, but existing tools force you to choose one path
38
+
39
+ Existing solutions (Playwright MCP, sweetlink, open-operator, browser-use) are either too heavy, too opinionated, or solve only half the problem.
40
+
41
+ ## The Insight
42
+
43
+ The user already has a browser. It's already logged in. It already passes Cloudflare. Instead of fighting the web with headless stealth tricks, **use what's already there**.
44
+
45
+ CDP (Chrome DevTools Protocol) lets us connect to any Chromium-based browser — the same one the user browses with daily. We get their cookies, their sessions, their anti-detection posture, for free.
46
+
47
+ ---
48
+
49
+ ## Core Architecture
50
+
51
+ ### CDP-Direct (Why No Playwright)
52
+
53
+ **Decision:** Use CDP over WebSocket directly. No Playwright dependency.
54
+
55
+ **Why:**
56
+ - Playwright downloads a bundled Chromium (~200MB). barebrowse uses the browser already installed on the user's machine.
57
+ - Playwright abstracts CDP, but we need CDP directly for all three modes (headless, headed, hybrid) against the user's real browser.
58
+ - Every Playwright API call maps 1:1 to a CDP method. The abstraction adds weight without adding capability for our use case.
59
+ - CDP gives us everything: `Accessibility.getFullAXTree`, `Page.navigate`, `Runtime.evaluate`, `Input.dispatch*Event`, `Network.setCookie`, `Page.captureScreenshot`.
60
+ - The CDP WebSocket client is ~100 lines of vanilla JS. Playwright is ~50,000.
61
+
62
+ **What we lose:** Cross-engine support (Firefox, WebKit). CDP only works with Chromium-family browsers (Chrome, Chromium, Edge, Brave, Vivaldi, Arc, Opera). This covers ~80% of desktop browsers. Firefox support could come later via WebDriver BiDi.
63
+
64
+ **What we gain:** Zero heavy deps, uses the user's real browser, same code path for headless/headed/hybrid, drastically simpler codebase.
65
+
66
+ ### ARIA-First (Why Not DOM)
67
+
68
+ **Decision:** Use `Accessibility.getFullAXTree` (ARIA/accessibility tree) as the primary page representation, not DOM.
69
+
70
+ **Why:**
71
+ - The accessibility tree is the semantic structure of the page — roles, names, states, interactive elements. It's what screen readers see. It's also what agents need.
72
+ - DOM is bloated: wrapper divs, styling, tracking pixels, ad scripts. An agent doesn't need any of that.
73
+ - mcprune already proved this: ARIA snapshots pruned by role achieve 75-95% token reduction on typical pages while preserving all actionable information.
74
+ - CDP's `Accessibility.getFullAXTree` returns the tree directly. No parsing HTML, no building a DOM tree, no traversing nodes.
75
+ - ARIA refs map directly to CDP interaction targets — the agent reads a button in the tree and can click it via the same CDP connection.
76
+
77
+ **The pipeline:** CDP connect → authenticate → navigate → ARIA tree → prune → agent gets clean snapshot.
78
+
79
+ ### Three Modes (Why All Three)
80
+
81
+ **Decision:** Headless, headed, and hybrid — not as separate packages or optional features, but as a single flag on the same API.
82
+
83
+ **Why they're not bloat:** The CDP conversation is identical regardless of mode. The only difference is how you get a browser process with a debug port. It's one code path with a different entry point:
84
+
85
+ ```
86
+ headless: spawn chromium --headless=new --remote-debugging-port=N
87
+ headed: connect to user's already-running browser on debug port
88
+ hybrid: try headless → detect failure → fall back to headed
89
+ ```
90
+
91
+ After connection, every CDP command is the same. Three modes = ~20 extra lines in `chromium.js`, not three implementations.
92
+
93
+ **When to use each:**
94
+
95
+ | Mode | Use case | Example |
96
+ |---|---|---|
97
+ | `headless` | Agent research, background tasks, CI | "Read this article and summarize it" |
98
+ | `headed` | Personal assistant, interactive tasks, auth flows | "Book me a flight on this page" |
99
+ | `hybrid` | Default for autonomous agents | Try headless; if CF-blocked, fall back to headed |
100
+
101
+ **Headless is the default.** Most agent tasks are "go read this page." Headed is the escape hatch for when headless fails or the task requires user-visible interaction.
102
+
103
+ ### Cookie Authentication
104
+
105
+ **Decision:** Extract cookies from the user's browser profile and inject via CDP `Network.setCookie`.
106
+
107
+ **Why:**
108
+ - The user's browser has active sessions for every site they use. We reuse those sessions instead of building new auth flows.
109
+ - sweet-cookie (npm package) already extracts cookies from Chrome/Firefox/Safari SQLite databases with OS keychain decryption. We use it or vendor the relevant parts.
110
+ - For headed mode, cookies are already present in the browser — no extraction needed.
111
+ - For headless mode, we extract from the user's profile and inject into the headless instance.
112
+
113
+ **Limitation:** Cookies expire. This works for existing sessions, not new logins. For sites requiring fresh auth, headed mode with user interaction is the fallback.
114
+
115
+ ### Pruning (Absorbed from mcprune)
116
+
117
+ **Decision:** Port mcprune's role-based ARIA tree pruning into barebrowse as a built-in step, not an optional module.
118
+
119
+ **Why:**
120
+ - Pruning is not optional for agent consumption. A raw ARIA tree is still too large for most LLM context windows. Pruning is part of the pipeline, not an afterthought.
121
+ - mcprune's pruning logic is a pure function: takes an ARIA tree, returns a smaller ARIA tree. No browser dependency, no Playwright coupling. It's ~300 lines of role-based tree surgery.
122
+ - By absorbing it, barebrowse becomes a complete "URL in, agent-ready snapshot out" solution. No second package needed.
123
+
124
+ **What we port from mcprune:**
125
+ - Role taxonomy (landmarks, interactive, structural, noise)
126
+ - Landmark extraction (main, nav, banner, etc.)
127
+ - Noise removal (ads, tracking, legal boilerplate)
128
+ - Interactive element preservation (buttons, links, inputs)
129
+ - Wrapper collapsing (nested generics, empty groups)
130
+ - Context-aware filtering (search relevance, dedup)
131
+
132
+ **What stays in mcprune:** The Playwright MCP proxy architecture. mcprune can continue to exist as a Playwright-based MCP server for users who want that path. But for barebrowse consumers, pruning is built in.
133
+
134
+ ---
135
+
136
+ ## API Design
137
+
138
+ ### Public API
139
+
140
+ ```js
141
+ import { browse, connect } from 'barebrowse';
142
+
143
+ // One-shot: URL in, pruned ARIA snapshot out
144
+ const tree = await browse('https://example.com');
145
+
146
+ // With options
147
+ const tree = await browse('https://example.com', {
148
+ mode: 'hybrid', // 'headless' (default) | 'headed' | 'hybrid'
149
+ cookies: true, // inject user's cookies (default: true)
150
+ prune: true, // apply ARIA pruning (default: true)
151
+ browser: 'chrome', // which browser profile for cookies
152
+ timeout: 30000, // navigation timeout ms
153
+ });
154
+
155
+ // Long-lived session for interaction
156
+ const page = await connect({ mode: 'headed' });
157
+ await page.goto('https://amazon.com/cart');
158
+ await page.click('[data-action="checkout"]');
159
+ await page.type('#gift-message', 'Happy birthday!');
160
+ const tree = await page.snapshot(); // ARIA + prune
161
+ await page.close();
162
+ ```
163
+
164
+ ### Design Principles
165
+
166
+ 1. **One package, one import.** No picking pieces. `browse()` does everything. Power users get `connect()` for long-lived sessions.
167
+ 2. **Batteries included.** Cookies, ARIA, pruning — all happen inside by default. Disable with flags if you want raw access.
168
+ 3. **Escape hatches.** `connect()` returns an object with the raw CDP connection accessible. If you need something we don't wrap, you can send CDP commands directly.
169
+ 4. **Progressive complexity.** `browse(url)` for 90% of use cases. Options object for the rest. `connect()` for interactive sessions.
170
+
171
+ ---
172
+
173
+ ## The bare- Ecosystem
174
+
175
+ ```
176
+ bareagent = the brain (orchestration, planning, memory, retries, tool loop)
177
+ barebrowse = the eyes + hands (browse, read, interact with the web)
178
+ ```
179
+
180
+ **Integration with bareagent:**
181
+
182
+ ```js
183
+ import { Loop } from 'bare-agent';
184
+ import { browse } from 'barebrowse';
185
+
186
+ const tools = [
187
+ { name: 'browse', execute: ({ url }) => browse(url) },
188
+ ];
189
+
190
+ const loop = new Loop({ provider });
191
+ await loop.run([{ role: 'user', content: 'Find the cheapest flight to Tokyo' }], tools);
192
+ ```
193
+
194
+ bareagent handles the think/act/observe loop. barebrowse handles "see the web and act on it." Neither is opinionated about the other. Tools are plain functions.
195
+
196
+ **Integration with multis:**
197
+
198
+ multis (personal assistant) uses barebrowse in headed mode for interactive tasks. The multis proxy is already running, providing a desktop session. barebrowse connects to the user's Chrome and drives it on behalf of the assistant.
199
+
200
+ **MCP server wrapper (future):**
201
+
202
+ barebrowse is not an MCP server, but wrapping it as one is ~30 lines. This would replace Playwright MCP + mcprune proxy with a single, lighter MCP server.
203
+
204
+ ---
205
+
206
+ ## Decisions Log — Why We Chose Each
207
+
208
+ This section exists so we don't re-debate settled decisions.
209
+
210
+ | Decision | Choice | Why | Alternative considered | Why not |
211
+ |---|---|---|---|---|
212
+ | Browser protocol | CDP direct | Uses user's browser, ~100 lines, all 3 modes | Playwright | 200MB download, bundles its own Chromium, abstracts what we need raw |
213
+ | Page representation | ARIA tree | Semantic, token-efficient, what agents need | DOM/HTML | Bloated, noisy, needs heavy parsing |
214
+ | Pruning | Built-in | Agents always need pruned output | Optional/separate | Two deps for one job, pruning isn't optional |
215
+ | Cookie auth | Own auth.js + CDP inject | User's existing sessions (Firefox or Chromium), cross-browser injection into headless Chromium | OAuth/credential storage | Complex, security liability, reinventing what the browser already solved |
216
+ | Three modes | One flag | Same CDP code, ~20 lines difference | Separate packages | Same code, artificial separation |
217
+ | Chromium only | CDP constraint | ~80% browser share, user's real browser | Cross-browser (Playwright) | Requires Playwright, loses "use your own browser" benefit |
218
+ | Anti-detection | Runtime.evaluate patches | Minimal stealth for headless mode | Full stealth framework | Over-engineering; headless + real cookies handles 90% |
219
+ | Daemon/server | None | CDP is direct, no intermediary needed | sweetlink daemon pattern | Unnecessary complexity for local agent→browser |
220
+ | Framework | None (vanilla JS) | Matches bare- philosophy, zero deps | Express/Fastify wrapper | Not a server, not needed |
221
+ | Language | Vanilla JavaScript | Node.js ecosystem, same as bareagent, CDP libs available | TypeScript | Added build step, not needed for POC; can add types later |
222
+ | Naming | chromium.js | Covers all Chromium-family browsers, not just Chrome | chrome.js | Too specific; Brave/Edge/Arc are also targets |
223
+ | mcprune integration | Absorb pruning logic | One package does it all, mcprune pruning is a pure function | Keep separate | Agents shouldn't need two packages to browse |
224
+ | openclaw lesson | Single bridge protocol | One CDP connection vs many API integrations | Direct multi-API | openclaw proved this fails — bloat, maintenance, fragility |
225
+
226
+ ---
227
+
228
+ ## Future Features (Post-POC)
229
+
230
+ ### Near-term
231
+ - **Screenshot capture** — `Page.captureScreenshot` via CDP. Useful for visual verification and multimodal agents.
232
+ - **Network interception** — `Network.requestWillBeSent` / `Network.responseReceived` for monitoring page loads. Detect redirects, blocked resources, API calls.
233
+ - **Wait strategies** — `waitForNavigation()` done (Page.loadEventFired). Still needed: network idle, element presence polling.
234
+ - **Tab management** — Multiple pages in one browser session. CDP `Target.createTarget` / `Target.attachToTarget`.
235
+ - **MCP server wrapper** — Expose browse/click/type as MCP tools. Replaces Playwright MCP + mcprune combo.
236
+
237
+ ### Medium-term
238
+ - **Firefox support** — Via WebDriver BiDi protocol (cross-browser standard, still maturing). Second protocol adapter alongside CDP.
239
+ - **Cookie sync** — In hybrid mode, extract fresh cookies from headed session and cache for future headless use. Self-refreshing auth.
240
+ - **Selector discovery** — Port sweetlink's `discoverSelectors` — crawl ARIA tree, score interactive elements, return ranked action targets.
241
+ - **Form understanding** — Detect forms in ARIA tree, map fields to semantic purposes, enable agents to fill forms intelligently.
242
+ - **Proxy/Tor support** — Route headless browser through proxy for geo-restricted content.
243
+
244
+ ### Long-term
245
+ - **Profile management** — Multiple browser profiles for different identities/accounts.
246
+ - **Session recording/replay** — Record browsing sessions as CDP commands, replay for testing.
247
+ - **Visual grounding** — Combine ARIA tree with screenshot regions for multimodal agents.
248
+ - **Agent memory integration** — Remember visited pages, cache snapshots, track which sites need headed mode.
249
+
250
+ ---
251
+
252
+ ## Repos Studied — What We Borrowed and Why
253
+
254
+ | Repo | What we took | What we skipped |
255
+ |---|---|---|
256
+ | **steipete/sweet-cookie** | Cookie extraction from browser profiles, OS keychain decryption | Nothing — clean, focused library |
257
+ | **steipete/sweetlink** | CDP dual-channel concept, selector discovery scoring, click/command patterns | Daemon architecture, WebSocket bridge, in-page runtime injection, HMAC auth |
258
+ | **steipete/canvas** | Stealth/anti-detection config patterns | Go implementation (we're JS) |
259
+ | **nichochar/open-operator** | AI agent web automation patterns | Full framework, too opinionated |
260
+ | **AntlerClaw/playwright-mcp** | How to expose browser as MCP tools | Playwright dependency |
261
+ | **AntlerClaw/mcp-browser-use** | MCP-native browser patterns | Heavy deps |
262
+ | **AitchKay/chromancer** | Accessibility tree extraction approach | Different stack |
263
+ | **mcprune (own)** | ARIA pruning logic — role taxonomy, landmark extraction, noise removal, wrapper collapsing | Playwright dependency, MCP proxy architecture |
264
+ | **openclaw (own)** | Lesson learned: multi-API direct integration = bloat. Use a single bridge protocol | Everything — the architecture was the cautionary tale |
265
+
266
+ ### The openclaw lesson
267
+
268
+ openclaw tried to integrate 10+ messaging APIs directly — each with its own auth, format, quirks. It became a maintenance nightmare. multis solved the same problem by using Beeper/Matrix as a single bridge.
269
+
270
+ barebrowse applies the same lesson: instead of integrating Playwright + Puppeteer + WebDriver + stealth plugins + cookie libraries + proxy managers, we use **one protocol (CDP) to one browser (the user's)**. Everything else is unnecessary.
271
+
272
+ ---
273
+
274
+ ## Success Criteria
275
+
276
+ barebrowse succeeds when:
277
+
278
+ 1. `browse(url)` returns a pruned ARIA snapshot of any page, authenticated as the user
279
+ 2. Zero heavy dependencies — no Playwright, no Puppeteer, no bundled browser
280
+ 3. Works with any installed Chromium-based browser
281
+ 4. Headless for research, headed for interaction, hybrid for autonomous agents
282
+ 5. Plugs into bareagent as plain tool functions
283
+ 6. Total source under 1,000 lines for core functionality
284
+ 7. An agent using barebrowse + bareagent can autonomously research the web and act on pages
@@ -0,0 +1,202 @@
1
+ # barebrowse -- Testing Guide
2
+
3
+ ## Run all tests
4
+
5
+ ```bash
6
+ node --test test/unit/*.test.js test/integration/*.test.js
7
+ ```
8
+
9
+ 54 tests, 5 files, ~45s on a typical machine. No test framework -- uses Node's built-in `node:test` runner.
10
+
11
+ ## Test pyramid
12
+
13
+ ```
14
+ / E2E \ 15 tests — real websites (Google, Wikipedia, GitHub, etc.)
15
+ /----------\
16
+ / Integration \ 11 tests — full browse/connect pipeline against example.com, HN
17
+ /----------------\
18
+ / Unit \ 28 tests — pruning, cookie extraction, CDP client, browser launch
19
+ /--------------------\
20
+ ```
21
+
22
+ Unit tests are fast and isolated. Integration tests launch a real headless Chromium. E2E tests (part of interact.test.js) hit live websites — they require internet and may be slower or flaky on CI.
23
+
24
+ ---
25
+
26
+ ## Unit tests (28 tests)
27
+
28
+ ### `test/unit/prune.test.js` -- 16 tests
29
+
30
+ Tests the 9-step ARIA pruning pipeline in isolation. No browser, no network.
31
+
32
+ | # | Test | What it validates |
33
+ |---|------|-------------------|
34
+ | 1 | returns null for empty tree | prune(null) returns null |
35
+ | 2 | unwraps RootWebArea | Root container node stripped from output |
36
+ | 3 | keeps interactive elements in act mode | Buttons, links, textboxes survive pruning |
37
+ | 4 | drops paragraphs in act mode | Non-interactive text removed in act mode |
38
+ | 5 | keeps paragraphs in browse mode | Text content preserved in browse/read mode |
39
+ | 6 | drops InlineTextBox noise | Low-level rendering nodes always filtered |
40
+ | 7 | keeps headings | h1/h2 headings preserved in browse mode |
41
+ | 8 | drops description headings in act mode | Only primary h1 kept, secondary headings removed |
42
+ | 9 | collapses unnamed structural wrappers | Nested generic divs flattened, children promoted |
43
+ | 10 | keeps named groups | Radiogroup/radio elements preserved |
44
+ | 11 | drops separators | Separator/hr nodes always removed |
45
+ | 12 | drops images in act mode, keeps named in browse | Act strips all images, browse keeps named ones |
46
+ | 13 | trims combobox to just name + selected value | Combobox children (options list) stripped |
47
+ | 14 | uses context keywords to condense non-matching cards | Context filtering collapses irrelevant list items |
48
+ | 15 | extracts main landmark when present | Act mode keeps only main content area |
49
+ | 16 | handles pages without landmarks (HN-style) | Pruning works on flat, landmark-less pages |
50
+
51
+ ### `test/unit/auth.test.js` -- 7 tests
52
+
53
+ Tests cookie extraction from the local filesystem. Reads real browser cookie databases.
54
+
55
+ | # | Test | What it validates |
56
+ |---|------|-------------------|
57
+ | 1 | auto-detects a browser and returns cookies | extractCookies() finds Firefox or Chromium and returns array |
58
+ | 2 | returns cookies with correct shape | Each cookie has name, value, domain, path, secure, httpOnly, sameSite, expires |
59
+ | 3 | filters by domain | Domain filter parameter restricts results |
60
+ | 4 | extracts from firefox explicitly | `{ browser: 'firefox' }` parameter works |
61
+ | 5 | throws for non-existent browser | Error thrown for unknown browser string |
62
+ | 6 | cookies have non-empty values | All returned cookies have non-empty value strings |
63
+ | 7 | sameSite is a valid value | sameSite is one of 'None', 'Lax', or 'Strict' |
64
+
65
+ Note: 2 tests may skip when Chromium profile is locked by a running instance (AES decryption needs keyring access).
66
+
67
+ ### `test/unit/cdp.test.js` -- 5 tests
68
+
69
+ Tests browser discovery, launch, CDP WebSocket client, and session handling.
70
+
71
+ | # | Test | What it validates |
72
+ |---|------|-------------------|
73
+ | 1 | finds a Chromium-based browser | findBrowser() returns path to chromium/chrome/brave/edge |
74
+ | 2 | launches headless Chromium and returns WebSocket URL | launch() returns valid ws:// URL, port, and live process |
75
+ | 3 | connects to browser and sends commands | createCDP() connects, Browser.getVersion responds |
76
+ | 4 | creates session-scoped handles | Target.createTarget + session() dispatches to correct target |
77
+ | 5 | gets accessibility tree from a page | Accessibility.getFullAXTree returns nodes with role/name |
78
+
79
+ ---
80
+
81
+ ## Integration tests (11 tests)
82
+
83
+ ### `test/integration/browse.test.js` -- 11 tests
84
+
85
+ Tests the full `browse()` and `connect()` pipeline end-to-end against real pages.
86
+
87
+ | # | Suite | Test | What it validates |
88
+ |---|-------|------|-------------------|
89
+ | 1 | browse() | returns ARIA snapshot for a public page | browse('example.com') returns non-empty snapshot with title |
90
+ | 2 | browse() | includes heading and ref markers | Snapshot contains roles and [ref=N] markers |
91
+ | 3 | browse() | prunes by default (act mode) | Pruned output smaller than raw ARIA tree |
92
+ | 4 | browse() | browse mode preserves paragraphs | pruneMode: 'browse' keeps text content |
93
+ | 5 | browse() | act mode drops paragraphs | pruneMode: 'act' removes non-interactive text |
94
+ | 6 | browse() | handles complex pages with significant reduction | Hacker News pruned by at least 20% |
95
+ | 7 | browse() | can disable cookies | cookies: false works without error |
96
+ | 8 | browse() | can disable pruning | prune: false keeps raw RootWebArea |
97
+ | 9 | connect() | creates a long-lived session and navigates | connect() + goto() + snapshot() works |
98
+ | 10 | connect() | supports multiple navigations in one session | Multiple goto() calls on same page |
99
+ | 11 | connect() | snapshot accepts prune: false for raw output | snapshot(false) preserves full tree |
100
+
101
+ ---
102
+
103
+ ## E2E tests (15 tests)
104
+
105
+ ### `test/integration/interact.test.js` -- 15 tests
106
+
107
+ Tests real interactions: clicking, typing, scrolling, form submission, and navigation. Uses a local `data:` URL fixture for deterministic tests, plus live websites for real-world coverage.
108
+
109
+ #### Data URL fixture tests (7 tests)
110
+
111
+ | # | Test | What it validates |
112
+ |---|------|-------------------|
113
+ | 1 | click sets button result text | page.click(ref) triggers onclick handler |
114
+ | 2 | type fills an empty input | page.type(ref, text) fills empty textbox |
115
+ | 3 | type with clear replaces existing text | { clear: true } replaces prefilled input |
116
+ | 4 | click on offscreen element scrolls into view first | Auto-scroll before click on element at 3000px |
117
+ | 5 | press Enter submits a form | page.press('Enter') triggers form onsubmit |
118
+ | 6 | press throws on unknown key | Error thrown for unrecognized key names |
119
+ | 7 | link click + waitForNavigation navigates | Cross-page navigation via click + waitForNavigation |
120
+
121
+ #### Live website tests (8 tests)
122
+
123
+ | # | Site | Test | What it validates |
124
+ |---|------|------|-------------------|
125
+ | 1 | Google | search and navigate results | type() + press('Enter') + waitForNavigation() on Google |
126
+ | 2 | Wikipedia | navigate article links | click() + waitForNavigation() on Wikipedia article links |
127
+ | 3 | GitHub | navigate SPA repo links | click() works for SPA navigation (no loadEventFired) |
128
+ | 4 | DuckDuckGo | search query and verify results | type() + press('Enter') + navigation on DDG |
129
+ | 5 | Hacker News | load homepage and navigate to a story | click() + waitForNavigation() on HN story links |
130
+ | 6 | Reddit (old) | load and navigate to a post | Page navigation with fallback to www.reddit.com |
131
+ | 7 | Firefox cookies | extract and inject into CDP session | extractCookies() + injectCookies() workflow |
132
+ | 8 | Firefox cookies | extractCookies with firefox returns array | Explicit browser parameter returns proper array |
133
+
134
+ ---
135
+
136
+ ## Writing new tests
137
+
138
+ Follow the existing pattern:
139
+
140
+ ```javascript
141
+ import { describe, it } from 'node:test';
142
+ import assert from 'node:assert/strict';
143
+ import { connect } from '../../src/index.js';
144
+
145
+ describe('my feature', () => {
146
+ it('does the thing', async () => {
147
+ const page = await connect();
148
+ try {
149
+ await page.goto('https://example.com');
150
+ const snap = await page.snapshot();
151
+ assert.ok(snap.includes('Example Domain'));
152
+ } finally {
153
+ await page.close();
154
+ }
155
+ });
156
+ });
157
+ ```
158
+
159
+ Key conventions:
160
+ - Always `page.close()` in a `finally` block to avoid leaked browser processes
161
+ - Use `data:` URL fixtures for deterministic tests (no network dependency)
162
+ - Real-site tests go in interact.test.js, grouped by site in `describe()` blocks
163
+ - Use `assert.ok()` and `assert.strictEqual()` from `node:assert/strict`
164
+ - No test framework dependencies -- `node:test` only
165
+
166
+ ### Data URL fixture pattern
167
+
168
+ For testing interactions without network:
169
+
170
+ ```javascript
171
+ const FIXTURE = `data:text/html,${encodeURIComponent(`
172
+ <html><body>
173
+ <button onclick="document.getElementById('r').textContent='clicked'">Click Me</button>
174
+ <div id="r"></div>
175
+ </body></html>
176
+ `)}`;
177
+
178
+ it('clicks the button', async () => {
179
+ const page = await connect();
180
+ try {
181
+ await page.goto(FIXTURE);
182
+ const snap = await page.snapshot({ mode: 'browse' });
183
+ const ref = findRef(snap, 'button', 'Click Me');
184
+ await page.click(ref);
185
+ const snap2 = await page.snapshot({ mode: 'browse' });
186
+ assert.ok(snap2.includes('clicked'));
187
+ } finally {
188
+ await page.close();
189
+ }
190
+ });
191
+ ```
192
+
193
+ ---
194
+
195
+ ## CI considerations
196
+
197
+ - Unit tests: fast, no network, always safe to run
198
+ - Integration tests: need Chromium installed, no network (uses example.com/HN but tolerates failures)
199
+ - E2E tests: need internet, may be flaky (sites change, rate limits, geo-blocks)
200
+ - Recommended CI split: run unit + integration always, E2E on manual trigger or nightly
201
+ - Each test launches/kills its own browser instance -- no shared state between tests
202
+ - Auth tests may skip when Chromium profile is locked by a running instance
@@ -0,0 +1,157 @@
1
+ /**
2
+ * barebrowse headed-mode demo
3
+ *
4
+ * This script demonstrates interactive browsing with a VISIBLE browser window.
5
+ * You watch the browser while barebrowse navigates, clicks, and types.
6
+ *
7
+ * SETUP — run this command first in a separate terminal:
8
+ *
9
+ * chromium-browser --remote-debugging-port=9222
10
+ *
11
+ * Then run this script:
12
+ *
13
+ * node examples/headed-demo.js
14
+ *
15
+ * The script connects to the already-running browser via CDP on port 9222.
16
+ * You will see each action happen in real time.
17
+ */
18
+
19
+ import { connect } from '../src/index.js';
20
+
21
+ // --- Helpers ---
22
+
23
+ /** Small delay so you can watch the browser between steps. */
24
+ function wait(ms = 1500) {
25
+ return new Promise((r) => setTimeout(r, ms));
26
+ }
27
+
28
+ /** Find a ref by matching role and name in the snapshot text. */
29
+ function findRoleRef(snapshot, role, name) {
30
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ const re = new RegExp(`${role} "${escaped}".*?\\[ref=([^\\]]+)\\]`);
32
+ const m = snapshot.match(re);
33
+ return m ? m[1] : null;
34
+ }
35
+
36
+ /** Find a ref by partial name match (case-insensitive). */
37
+ function findRoleRefPartial(snapshot, role, nameFragment) {
38
+ const escaped = nameFragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ const re = new RegExp(`${role} "[^"]*${escaped}[^"]*".*?\\[ref=([^\\]]+)\\]`, 'i');
40
+ const m = snapshot.match(re);
41
+ return m ? m[1] : null;
42
+ }
43
+
44
+ /** Print a snapshot truncated to a character limit. */
45
+ function printSnapshot(snapshot, limit = 500) {
46
+ const truncated = snapshot.length > limit
47
+ ? snapshot.slice(0, limit) + `\n... (${snapshot.length - limit} more chars)`
48
+ : snapshot;
49
+ console.log(truncated);
50
+ }
51
+
52
+ // --- Demo ---
53
+
54
+ async function main() {
55
+ console.log('=== barebrowse headed-mode demo ===\n');
56
+ console.log('Connecting to Chromium on port 9222...');
57
+ console.log('(Make sure you ran: chromium-browser --remote-debugging-port=9222)\n');
58
+
59
+ const page = await connect({ mode: 'headed', port: 9222 });
60
+
61
+ try {
62
+ // Step 1: Navigate to Wikipedia
63
+ console.log('[Step 1] Navigating to Wikipedia "JavaScript" article...');
64
+ await page.goto('https://en.wikipedia.org/wiki/JavaScript');
65
+ await wait();
66
+
67
+ // Step 2: Take a snapshot
68
+ console.log('[Step 2] Taking ARIA snapshot of the page...\n');
69
+ let snap = await page.snapshot();
70
+ printSnapshot(snap);
71
+ console.log();
72
+
73
+ // Step 3: Find and click a link
74
+ console.log('[Step 3] Looking for a link to click...');
75
+ // Try to find the "ECMAScript" link — a common one in the JS article
76
+ let linkRef = findRoleRefPartial(snap, 'link', 'ECMAScript');
77
+ if (!linkRef) {
78
+ // Fallback: find any link
79
+ linkRef = findRoleRefPartial(snap, 'link', 'programming');
80
+ }
81
+ if (linkRef) {
82
+ console.log(` Found link ref=${linkRef}, clicking it...`);
83
+ const navPromise = page.waitForNavigation();
84
+ await page.click(linkRef);
85
+
86
+ // Step 4: Wait for navigation
87
+ console.log('[Step 4] Waiting for navigation to complete...');
88
+ await navPromise;
89
+ await wait();
90
+ } else {
91
+ console.log(' No matching link found, skipping click step.');
92
+ }
93
+
94
+ // Step 5: New snapshot after navigation
95
+ console.log('[Step 5] Taking snapshot of the new page...\n');
96
+ snap = await page.snapshot();
97
+ printSnapshot(snap);
98
+ console.log();
99
+
100
+ // Step 6: Navigate to DuckDuckGo
101
+ console.log('[Step 6] Navigating to DuckDuckGo...');
102
+ await page.goto('https://duckduckgo.com');
103
+ await wait();
104
+
105
+ // Step 7: Find search box and type a query
106
+ console.log('[Step 7] Taking snapshot to find the search box...');
107
+ snap = await page.snapshot();
108
+ let searchRef = findRoleRefPartial(snap, 'textbox', 'search')
109
+ || findRoleRefPartial(snap, 'searchbox', 'search')
110
+ || findRoleRefPartial(snap, 'combobox', 'search');
111
+
112
+ if (searchRef) {
113
+ console.log(` Found search box ref=${searchRef}, typing query...`);
114
+ await page.click(searchRef);
115
+ await wait(500);
116
+ await page.type(searchRef, 'barebrowse CDP browser automation');
117
+ await wait();
118
+ } else {
119
+ console.log(' Could not find search box. Snapshot preview:');
120
+ printSnapshot(snap, 300);
121
+ console.log(' Skipping search steps.');
122
+ return;
123
+ }
124
+
125
+ // Step 8: Press Enter
126
+ console.log('[Step 8] Pressing Enter to search...');
127
+ const resultsNav = page.waitForNavigation();
128
+ await page.press('Enter');
129
+
130
+ // Step 9: Wait for results
131
+ console.log('[Step 9] Waiting for search results...');
132
+ await resultsNav;
133
+ await wait(2000);
134
+
135
+ // Step 10: Snapshot the results
136
+ console.log('[Step 10] Taking snapshot of search results...\n');
137
+ snap = await page.snapshot();
138
+ printSnapshot(snap, 800);
139
+ console.log();
140
+
141
+ console.log('=== Demo complete ===');
142
+ } finally {
143
+ console.log('Closing session...');
144
+ await page.close();
145
+ }
146
+ }
147
+
148
+ main().catch((err) => {
149
+ if (err.message?.includes('ECONNREFUSED') || err.message?.includes('connect')) {
150
+ console.error('\nError: Could not connect to Chromium on port 9222.');
151
+ console.error('Make sure you have Chromium running with remote debugging:');
152
+ console.error('\n chromium-browser --remote-debugging-port=9222\n');
153
+ } else {
154
+ console.error('\nError:', err.message || err);
155
+ }
156
+ process.exit(1);
157
+ });