docs: add human-friendly documentation
All checks were successful
Security Scan / security (push) Successful in 7s
Test / test (push) Successful in 34s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-11 13:02:40 +00:00
parent 6a459ec08b
commit 29ebe46fe7
3 changed files with 362 additions and 137 deletions

View file

@ -1,45 +1,46 @@
---
title: Architecture
description: Internals of go-webview -- CDP connection, message protocol, DOM queries, console capture, action system, and Angular helpers.
---
# Architecture
Module: `forge.lthn.ai/core/go-webview`
This document describes how `go-webview` works internally. It covers the CDP connection lifecycle, message protocol, DOM query mechanics, input simulation, console capture, the action system, Angular helpers, and thread safety.
## Overview
## High-Level Data Flow
go-webview is a Chrome DevTools Protocol (CDP) client for browser automation, testing, and scraping. It provides a high-level Go API over the low-level CDP WebSocket protocol, connecting to an externally managed Chrome or Chromium instance running with the remote debugging port enabled.
```
Application Code
|
v
Webview (high-level API: Navigate, Click, Type, Screenshot, ...)
|
v
CDPClient (WebSocket transport, message framing, event dispatch)
|
v
Chrome / Chromium (running with --remote-debugging-port=9222)
```
The package does not launch Chrome itself. The caller is responsible for starting a Chrome process with `--remote-debugging-port=9222` before constructing a `Webview`.
---
## Package Structure
| File | Responsibility |
|------|---------------|
| `webview.go` | `Webview` struct, public API, navigation, DOM, screenshot, JS evaluation |
| `cdp.go` | `CDPClient` — WebSocket transport, message framing, event dispatch |
| `actions.go` | `Action` interface, concrete action types, `ActionSequence` builder |
| `console.go` | `ConsoleWatcher`, `ExceptionWatcher`, log formatting |
| `angular.go` | `AngularHelper` — SPA-specific helpers for Angular 2+ and AngularJS 1.x |
---
The application interacts with `Webview` methods. Each method constructs a CDP command, passes it to `CDPClient.Call()`, which serialises it as JSON over a WebSocket connection to Chrome. Chrome processes the command and returns a JSON response. Events (console messages, exceptions, navigation state changes) flow in the opposite direction: Chrome pushes them over the WebSocket, the `CDPClient` read loop dispatches them to registered handlers.
## CDP Connection
### Initialisation
`NewCDPClient(debugURL string)` connects to Chrome's HTTP endpoint:
`NewCDPClient(debugURL string)` connects to Chrome's HTTP endpoint in four steps:
1. Issues `GET {debugURL}/json` to retrieve the list of available targets (tabs/pages).
2. Selects the first target with `type == "page"` that has a `webSocketDebuggerUrl`.
3. If no page target exists, calls `GET {debugURL}/json/new` to create one.
4. Upgrades the connection to WebSocket using `github.com/gorilla/websocket`.
5. Starts a background `readLoop` goroutine on the connection.
4. Upgrades the connection to WebSocket using `github.com/gorilla/websocket` and starts a background `readLoop` goroutine.
### Message Protocol
CDP uses JSON-framed messages over WebSocket. The client distinguishes two message kinds:
- **Commands** sent by the client with an integer `id`. Chrome responds with a matching `id` and a `result` or `error` field.
- **Events** sent by Chrome without an `id`. They carry a `method` name and a `params` map.
- **Commands** -- sent by the client with an integer `id`. Chrome responds with a matching `id` and a `result` or `error` field.
- **Events** -- sent by Chrome without an `id`. They carry a `method` name and a `params` map.
The `CDPClient` maintains a `pending` map of `id -> chan *cdpResponse`. When `Call()` sends a command it registers a channel, then blocks on that channel until the matching response arrives or the context expires.
@ -49,25 +50,42 @@ Events are dispatched to zero or more registered handlers via `OnEvent(method, h
```
New(WithDebugURL(...))
└── NewCDPClient(url)
├── HTTP GET /json (target discovery)
├── websocket.Dial(wsURL) (WebSocket upgrade)
└── go readLoop() (background goroutine)
+-- NewCDPClient(url)
|-- HTTP GET /json (target discovery)
|-- websocket.Dial(wsURL) (WebSocket upgrade)
+-- go readLoop() (background goroutine)
wv.Close()
└── cancel() (signals readLoop to stop)
└── CDPClient.Close()
├── <-done (waits for readLoop to finish)
└── conn.Close() (closes WebSocket)
+-- cancel() (signals readLoop to stop)
+-- CDPClient.Close()
|-- <-done (waits for readLoop to finish)
+-- conn.Close() (closes WebSocket)
```
---
## Key Types
## Webview Struct
### CDPClient
```go
type CDPClient struct {
conn *websocket.Conn
debugURL string
wsURL string
msgID atomic.Int64 // monotonic command ID
pending map[int64]chan *cdpResponse // awaiting responses
handlers map[string][]func(map[string]any) // event subscribers
ctx context.Context
cancel context.CancelFunc
done chan struct{}
}
```
The core transport layer. All WebSocket reads happen in the `readLoop` goroutine. All writes are serialised through a `sync.RWMutex`. The `pending` map and `handlers` map each have their own dedicated mutexes.
### Webview
```go
type Webview struct {
mu sync.RWMutex
client *CDPClient
ctx context.Context
cancel context.CancelFunc
@ -77,40 +95,22 @@ type Webview struct {
}
```
`New()` accepts functional options:
The high-level API surface. Constructed via `New()` with functional options. On construction, it enables three CDP domains -- `Runtime`, `Page`, and `DOM` -- and registers a handler for `Runtime.consoleAPICalled` events so console capture begins immediately.
| Option | Effect |
|--------|--------|
| `WithDebugURL(url)` | Required. Connects to Chrome at the given HTTP debug endpoint. |
| `WithTimeout(d)` | Overrides the default 30-second operation timeout. |
| `WithConsoleLimit(n)` | Maximum console messages to retain in memory (default 1000). |
### ConsoleMessage
On construction, `New()` enables three CDP domains — `Runtime`, `Page`, and `DOM` — and registers a handler for `Runtime.consoleAPICalled` events to begin console capture immediately.
```go
type ConsoleMessage struct {
Type string // log, warn, error, info, debug
Text string // message text
Timestamp time.Time
URL string // source URL
Line int // source line number
Column int // source column number
}
```
---
## Navigation
`Navigate(url string) error` calls `Page.navigate` then polls `document.readyState` via `Runtime.evaluate` at 100 ms intervals until the value is `"complete"` or the context deadline is exceeded.
`Reload()`, `GoBack()`, and `GoForward()` follow the same pattern: issue a CDP command then call `waitForLoad`.
`waitForSelector(ctx, selector)` polls `document.querySelector(selector)` at 100 ms intervals.
---
## DOM Queries
DOM queries follow a two-step pattern:
1. Call `DOM.getDocument` to obtain the root node ID.
2. Call `DOM.querySelector` or `DOM.querySelectorAll` with that node ID and the CSS selector string.
For each matching node, `getElementInfo` calls:
- `DOM.describeNode` — tag name and attribute list (flat alternating key/value array)
- `DOM.getBoxModel` — bounding rectangle from the `content` quad
The returned `ElementInfo` carries:
### ElementInfo
```go
type ElementInfo struct {
@ -123,7 +123,37 @@ type ElementInfo struct {
}
```
---
### BoundingBox
```go
type BoundingBox struct {
X float64
Y float64
Width float64
Height float64
}
```
## Navigation
`Navigate(url string) error` calls `Page.navigate` then polls `document.readyState` via `Runtime.evaluate` at 100 ms intervals until the value is `"complete"` or the context deadline is exceeded.
`Reload()`, `GoBack()`, and `GoForward()` follow the same pattern: issue a CDP command then call `waitForLoad`.
`waitForSelector(ctx, selector)` polls `document.querySelector(selector)` at 100 ms intervals until the element exists or the context expires.
## DOM Queries
DOM queries follow a two-step pattern:
1. Call `DOM.getDocument` to obtain the root node ID.
2. Call `DOM.querySelector` or `DOM.querySelectorAll` with that node ID and the CSS selector string.
For each matching node, `getElementInfo` calls:
- `DOM.describeNode` -- tag name and attribute list (flat alternating key/value array)
- `DOM.getBoxModel` -- bounding rectangle from the `content` quad
`QuerySelectorAllAll(selector)` returns an `iter.Seq[*ElementInfo]` iterator for lazy consumption of results.
## Click and Type
@ -137,8 +167,6 @@ type ElementInfo struct {
`PressKeyAction` handles named keys (Enter, Tab, Escape, Backspace, Delete, arrow keys, Home, End, Page Up, Page Down) by mapping them to their CDP virtual key codes and code strings.
---
## Console Capture
Console capture is enabled in `New()` by subscribing to `Runtime.consoleAPICalled` events.
@ -148,43 +176,64 @@ Console capture is enabled in `New()` by subscribing to `Runtime.consoleAPICalle
The `Webview` itself accumulates messages in a slice guarded by `sync.RWMutex`. When the buffer reaches `consoleLimit`, the oldest 100 messages are dropped.
```go
msgs := wv.GetConsole() // returns a copy
msgs := wv.GetConsole() // returns a collected slice
wv.ClearConsole()
// Or iterate lazily
for msg := range wv.GetConsoleAll() {
fmt.Println(msg.Text)
}
```
### ConsoleWatcher
`ConsoleWatcher` (constructed via `NewConsoleWatcher(wv)`) registers its own handler on the same `Runtime.consoleAPICalled` event. It adds filtering and reactive capabilities:
- `AddFilter(ConsoleFilter)` — filter by message type and/or text pattern
- `AddHandler(ConsoleHandler)` callback invoked for each incoming message (outside the write lock)
- `WaitForMessage(ctx, filter)` blocks until a matching message arrives
- `WaitForError(ctx)` convenience wrapper for `type == "error"`
- `AddFilter(ConsoleFilter)` -- filter by message type and/or text pattern (substring match)
- `AddHandler(ConsoleHandler)` -- callback invoked for each incoming message (outside the write lock)
- `WaitForMessage(ctx, filter)` -- blocks until a matching message arrives
- `WaitForError(ctx)` -- convenience wrapper for `type == "error"`
- `Errors()`, `Warnings()`, `HasErrors()`, `ErrorCount()`
- `FilteredMessages()` / `FilteredMessagesAll()` -- returns messages matching all active filters
### ExceptionWatcher
`ExceptionWatcher` subscribes to `Runtime.exceptionThrown` events and captures unhandled JavaScript exceptions with full stack traces. It exposes the same reactive pattern as `ConsoleWatcher`: `AddHandler`, `WaitForException`, `HasExceptions`.
`ExceptionWatcher` subscribes to `Runtime.exceptionThrown` events and captures unhandled JavaScript exceptions with full stack traces:
---
```go
type ExceptionInfo struct {
Text string
LineNumber int
ColumnNumber int
URL string
StackTrace string
Timestamp time.Time
}
```
It exposes the same reactive pattern as `ConsoleWatcher`: `AddHandler`, `WaitForException`, `HasExceptions`, `Count`.
### FormatConsoleOutput
The package-level `FormatConsoleOutput(messages)` function formats a slice of `ConsoleMessage` into human-readable lines with timestamp, level prefix (`[ERROR]`, `[WARN]`, `[INFO]`, `[DEBUG]`, `[LOG]`), and message text.
## Screenshots
`Screenshot()` calls `Page.captureScreenshot` with `format: "png"`. Chrome returns the image as a base64-encoded string in the `data` field of the response. The method decodes this and returns raw PNG bytes.
---
## JavaScript Evaluation
`evaluate(ctx, script)` calls `Runtime.evaluate` with `returnByValue: true`. The result is extracted from `result.result.value`. If `result.exceptionDetails` is present, the error description is returned as a Go error.
`Evaluate(script string) (any, error)` is the public wrapper that applies the default timeout.
`GetURL()` and `GetTitle()` are thin wrappers that evaluate `window.location.href` and `document.title` respectively.
Convenience wrappers:
`GetHTML(selector string)` evaluates `outerHTML` on the matched element, or `document.documentElement.outerHTML` when the selector is empty.
---
| Method | JavaScript evaluated |
|--------|---------------------|
| `GetURL()` | `window.location.href` |
| `GetTitle()` | `document.title` |
| `GetHTML(selector)` | `document.querySelector(selector)?.outerHTML` (or `document.documentElement.outerHTML` when selector is empty) |
## Action System
@ -196,12 +245,36 @@ type Action interface {
}
```
Concrete action types cover: `Click`, `Type`, `Navigate`, `Wait`, `WaitForSelector`, `Scroll`, `ScrollIntoView`, `Focus`, `Blur`, `Clear`, `Select`, `Check`, `Hover`, `DoubleClick`, `RightClick`, `PressKey`, `SetAttribute`, `RemoveAttribute`, `SetValue`.
### Concrete Action Types
`ActionSequence` provides a fluent builder:
| Type | Description |
|------|-------------|
| `ClickAction` | Click an element by CSS selector |
| `TypeAction` | Type text into a focused element |
| `NavigateAction` | Navigate to a URL and wait for load |
| `WaitAction` | Wait for a fixed duration |
| `WaitForSelectorAction` | Wait for an element to appear |
| `ScrollAction` | Scroll to absolute coordinates |
| `ScrollIntoViewAction` | Scroll an element into view smoothly |
| `FocusAction` | Focus an element |
| `BlurAction` | Remove focus from an element |
| `ClearAction` | Clear an input's value, firing `input` and `change` events |
| `SelectAction` | Select a value in a `<select>` element |
| `CheckAction` | Check or uncheck a checkbox |
| `HoverAction` | Hover over an element |
| `DoubleClickAction` | Double-click an element |
| `RightClickAction` | Right-click (context menu) an element |
| `PressKeyAction` | Press a named key (Enter, Tab, Escape, etc.) |
| `SetAttributeAction` | Set an HTML attribute on an element |
| `RemoveAttributeAction` | Remove an HTML attribute from an element |
| `SetValueAction` | Set an input's value, firing `input` and `change` events |
### ActionSequence
`ActionSequence` provides a fluent builder. Actions are executed sequentially; the first failure halts the sequence and returns the action index with the error.
```go
err := NewActionSequence().
err := webview.NewActionSequence().
Navigate("https://example.com").
WaitForSelector("#login-form").
Type("#email", "user@example.com").
@ -210,23 +283,24 @@ err := NewActionSequence().
Execute(ctx, wv)
```
`Execute` runs actions sequentially and returns the index and error of the first failure.
### File Upload and Drag-and-Drop
`UploadFile(selector, filePaths)` uses `DOM.setFileInputFiles` on the node ID of the resolved file input element.
These are methods on `Webview` rather than action types:
`DragAndDrop(sourceSelector, targetSelector)` dispatches `mousePressed`, `mouseMoved`, and `mouseReleased` events between the centre points of the two elements.
---
- `UploadFile(selector, filePaths)` -- uses `DOM.setFileInputFiles` on the resolved file input node
- `DragAndDrop(sourceSelector, targetSelector)` -- dispatches `mousePressed`, `mouseMoved`, and `mouseReleased` events between the centre points of two elements
## Angular Helpers
`AngularHelper` (constructed via `NewAngularHelper(wv)`) provides SPA-specific utilities. All methods accept the `AngularHelper.timeout` deadline (default 30 s).
`AngularHelper` (constructed via `NewAngularHelper(wv)`) provides SPA-specific utilities for Angular 2+ applications. All methods use the helper's configurable timeout (default 30 seconds).
### Application Detection
`isAngularApp` checks for Angular 2+ via `window.getAllAngularRootElements`, the `[ng-version]` attribute, or `window.ng.probe`. It also checks for AngularJS 1.x via `window.angular.element`.
`isAngularApp` checks for Angular by probing:
- `window.getAllAngularRootElements` (Angular 2+)
- The `[ng-version]` attribute on DOM elements
- `window.ng.probe` (Angular debug utilities)
- `window.angular.element` (AngularJS 1.x)
### Zone.js Stability
@ -234,44 +308,45 @@ err := NewActionSequence().
### Router Integration
`NavigateByRouter(path)` obtains the `Router` service from the Angular injector and calls `router.navigateByUrl(path)`, then waits for Zone.js stability.
`GetRouterState()` returns an `AngularRouterState` with the current URL, fragment, route params, and query params.
- `NavigateByRouter(path)` -- obtains the `Router` service from the Angular injector, calls `router.navigateByUrl(path)`, then waits for Zone.js stability
- `GetRouterState()` -- returns an `AngularRouterState` with the current URL, fragment, route params, and query params
### Component Introspection
`GetComponentProperty(selector, property)` and `SetComponentProperty(selector, property, value)` access component instances via `window.ng.probe(element).componentInstance`. After setting a property, `ApplicationRef.tick()` is called to trigger change detection.
These methods require the Angular application to be running in debug mode (`window.ng.probe` must be available):
`CallComponentMethod(selector, method, args...)` invokes a method on the component instance and triggers change detection.
- `GetComponentProperty(selector, property)` -- reads a property from a component instance
- `SetComponentProperty(selector, property, value)` -- writes a property and triggers `ApplicationRef.tick()`
- `CallComponentMethod(selector, method, args...)` -- invokes a method and triggers change detection
- `GetService(name)` -- retrieves a named service from the root injector, returned as a JSON-serialisable value
`GetService(name)` retrieves a named service from the root injector and returns a JSON-serialisable representation.
### ngModel Access
### ngModel
- `GetNgModel(selector)` -- reads the current value of an ngModel-bound input
- `SetNgModel(selector, value)` -- writes the value, fires `input` and `change` events, and triggers `ApplicationRef.tick()`
`GetNgModel(selector)` reads the current value of an ngModel-bound input. `SetNgModel(selector, value)` writes the value, fires `input` and `change` events, and triggers `ApplicationRef.tick()`.
### Other Helpers
---
- `TriggerChangeDetection()` -- manually triggers `ApplicationRef.tick()` across all root elements
- `WaitForComponent(selector)` -- polls until a component instance exists on the matched element
- `DispatchEvent(selector, eventName, detail)` -- dispatches a `CustomEvent` on an element
## Multi-Tab Support
`CDPClient.NewTab(url)` calls `GET {debugURL}/json/new?{url}` and returns a new `CDPClient` connected to the WebSocket of the newly created tab. Each tab has its own independent read loop and event handler registry, so console events and other notifications are tab-scoped.
`CDPClient.CloseTab()` calls `Browser.close` on the tab's CDP session.
`ListTargets(debugURL)` and `ListTargetsAll(debugURL)` are package-level utilities that query the HTTP endpoint without requiring an active WebSocket connection. `ListTargetsAll` returns an `iter.Seq[targetInfo]` iterator.
`ListTargets(debugURL)` and `GetVersion(debugURL)` are package-level utilities that query the HTTP endpoint without requiring an active WebSocket connection.
---
`GetVersion(debugURL)` returns Chrome version information as a string map.
## Emulation
`SetViewport(width, height int)` calls `Emulation.setDeviceMetricsOverride` with `deviceScaleFactor: 1` and `mobile: false`.
`SetUserAgent(ua string)` calls `Emulation.setUserAgentOverride`.
---
- `SetViewport(width, height int)` -- calls `Emulation.setDeviceMetricsOverride` with `deviceScaleFactor: 1` and `mobile: false`
- `SetUserAgent(ua string)` -- calls `Emulation.setUserAgentOverride`
## Thread Safety
- `CDPClient` uses `sync.RWMutex` for WebSocket writes and `sync.Mutex` for the pending-response map. Event handler registration uses a separate `sync.RWMutex`.
- `Webview` uses `sync.RWMutex` for its console log slice.
- `ConsoleWatcher` and `ExceptionWatcher` use `sync.RWMutex` for their message and handler slices. Handlers are copied before being called so they execute outside the write lock.
- **CDPClient** uses `sync.RWMutex` for WebSocket writes and `sync.Mutex` for the pending-response map. Event handler registration uses a separate `sync.RWMutex`.
- **Webview** uses `sync.RWMutex` for its console log slice.
- **ConsoleWatcher** and **ExceptionWatcher** use `sync.RWMutex` for their message and handler slices. Handlers are copied before being called so they execute outside the write lock.
- Event handlers registered via `OnEvent` are dispatched in separate goroutines so they cannot block the WebSocket read loop.

View file

@ -1,10 +1,15 @@
---
title: Development Guide
description: How to build, test, and contribute to go-webview -- prerequisites, test patterns, coding standards, and extension guides.
---
# Development Guide
## Prerequisites
### Go
Go 1.25 or later is required. The module path is `forge.lthn.ai/core/go-webview`.
Go 1.26 or later is required. The module path is `forge.lthn.ai/core/go-webview`.
### Chrome or Chromium
@ -38,8 +43,6 @@ The only runtime dependency is `github.com/gorilla/websocket v1.5.3`, declared i
go mod download
```
---
## Build and Test
### Running Tests
@ -48,7 +51,7 @@ go mod download
go test ./...
```
Tests must pass before committing. There are currently no build tags that gate tests behind a Chrome connection; the integration tests in `webview_test.go` that require a live browser (`TestNew_Bad_InvalidDebugURL`) will fail gracefully because they assert that the error is non-nil when connecting to an unavailable port.
Tests must pass before committing. The integration tests in `webview_test.go` that reference a live browser (`TestNew_Bad_InvalidDebugURL`) are designed to fail gracefully -- they assert that the error is non-nil when connecting to an unavailable port.
```bash
# Run a specific test
@ -58,17 +61,24 @@ go test -v -run TestActionSequence_Good ./...
go test -v ./...
```
### Vetting and Formatting
```bash
gofmt -w .
go vet ./...
```
### Test Naming Convention
Tests follow the `_Good`, `_Bad`, `_Ugly` suffix pattern, consistent with the broader Core Go ecosystem:
- `_Good` — happy path, verifies correct behaviour under valid input.
- `_Bad` — expected error conditions, verifies that errors are returned and have the correct shape.
- `_Ugly` — panic/edge cases, unexpected or degenerate inputs.
- `_Good` -- happy path, verifies correct behaviour under valid input.
- `_Bad` -- expected error conditions, verifies that errors are returned and have the correct shape.
- `_Ugly` -- panic/edge cases, unexpected or degenerate inputs.
All test functions use the standard `testing.T` interface; the project does not use a test framework.
### CI Headless Tests
### Headless CI Tests
To add tests that exercise the full CDP stack in CI:
@ -76,21 +86,19 @@ To add tests that exercise the full CDP stack in CI:
2. Serve test fixtures using `net/http/httptest` so tests do not depend on external URLs.
3. Use `WithTimeout` to set conservative deadlines appropriate for the CI environment.
---
## Code Organisation
New source files belong in the root package (`package webview`). The package is intentionally a single flat package; do not create sub-packages.
Keep separation between layers:
- **CDP transport**`cdp.go`. Do not put browser-level logic here.
- **High-level API**`webview.go`. Methods here should be safe to call from application code without CDP knowledge.
- **Action types**`actions.go`. Add new action types here; keep each action focused on a single interaction.
- **Diagnostics**`console.go`. Console and exception capture live here.
- **SPA helpers**`angular.go`. Framework-specific helpers belong here or in a new file named after the framework (e.g. `react.go`, `vue.go`).
---
| Layer | File | Guidance |
|-------|------|----------|
| CDP transport | `cdp.go` | Do not put browser-level logic here. |
| High-level API | `webview.go` | Methods here should be safe to call from application code without CDP knowledge. |
| Action types | `actions.go` | Add new action types here; keep each action focused on a single interaction. |
| Diagnostics | `console.go` | Console and exception capture live here. |
| SPA helpers | `angular.go` | Framework-specific helpers belong here or in a new file named after the framework (e.g. `react.go`, `vue.go`). |
## Coding Standards
@ -129,8 +137,6 @@ Every Go source file must begin with:
The project is licenced under the European Union Public Licence 1.2 (EUPL-1.2).
---
## Commit Guidelines
Use conventional commits:
@ -160,8 +166,6 @@ WaitForException API consistent with ConsoleWatcher.
Co-Authored-By: Virgil <virgil@lethean.io>
```
---
## Adding a New Action Type
1. Define a struct in `actions.go` with exported fields for the action's parameters.
@ -188,8 +192,6 @@ func (s *ActionSequence) Submit(selector string) *ActionSequence {
}
```
---
## Adding a New Angular Helper
Add methods to `AngularHelper` in `angular.go`. Follow the established pattern:
@ -200,7 +202,14 @@ Add methods to `AngularHelper` in `angular.go`. Follow the established pattern:
4. After state-modifying operations, call `TriggerChangeDetection()` or inline `appRef.tick()`.
5. For polling-based waits, use a `time.NewTicker` at 100 ms and select over `ctx.Done()`.
---
## Adding a New SPA Framework Helper
To add support for a different single-page application framework (e.g. React, Vue):
1. Create a new file named after the framework (e.g. `react.go`).
2. Define a helper struct that holds a `*Webview` reference and a configurable timeout.
3. Use `evaluate()` to inject JavaScript that probes framework-specific globals and APIs.
4. Follow the same `context.WithTimeout` + polling pattern established in `angular.go`.
## Forge Push

141
docs/index.md Normal file
View file

@ -0,0 +1,141 @@
---
title: go-webview
description: Chrome DevTools Protocol client for browser automation, testing, and scraping in Go.
---
# go-webview
`go-webview` is a Go package that provides browser automation via the Chrome DevTools Protocol (CDP). It connects to an externally managed Chrome or Chromium instance running with `--remote-debugging-port=9222` and exposes a high-level API for navigation, DOM queries, input simulation, screenshot capture, console monitoring, and JavaScript evaluation.
The package does not launch Chrome itself. The caller is responsible for starting the browser process before constructing a `Webview`.
**Module path:** `forge.lthn.ai/core/go-webview`
**Licence:** EUPL-1.2
**Go version:** 1.26+
**Dependencies:** `github.com/gorilla/websocket v1.5.3`
## Quick Start
Start Chrome with the remote debugging port enabled:
```bash
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222
# Linux
google-chrome --remote-debugging-port=9222
# Headless (suitable for CI)
google-chrome --headless=new --remote-debugging-port=9222 --no-sandbox --disable-gpu
```
Then use the package in Go:
```go
import "forge.lthn.ai/core/go-webview"
// Connect to Chrome
wv, err := webview.New(webview.WithDebugURL("http://localhost:9222"))
if err != nil {
log.Fatal(err)
}
defer wv.Close()
// Navigate and interact
if err := wv.Navigate("https://example.com"); err != nil {
log.Fatal(err)
}
if err := wv.Click("#submit-button"); err != nil {
log.Fatal(err)
}
```
### Fluent Action Sequences
Chain multiple browser actions together with `ActionSequence`:
```go
err := webview.NewActionSequence().
Navigate("https://example.com").
WaitForSelector("#login-form").
Type("#email", "user@example.com").
Type("#password", "secret").
Click("#submit").
Execute(ctx, wv)
```
### Console Monitoring
Capture and filter browser console output:
```go
cw := webview.NewConsoleWatcher(wv)
cw.AddFilter(webview.ConsoleFilter{Type: "error"})
// ... perform browser actions ...
if cw.HasErrors() {
for _, msg := range cw.Errors() {
log.Printf("JS error: %s at %s:%d", msg.Text, msg.URL, msg.Line)
}
}
```
### Screenshots
Capture the current page as PNG:
```go
png, err := wv.Screenshot()
if err != nil {
log.Fatal(err)
}
os.WriteFile("screenshot.png", png, 0644)
```
### Angular Applications
First-class support for Angular single-page applications:
```go
ah := webview.NewAngularHelper(wv)
// Wait for Angular to stabilise
if err := ah.WaitForAngular(); err != nil {
log.Fatal(err)
}
// Navigate using Angular Router
if err := ah.NavigateByRouter("/dashboard"); err != nil {
log.Fatal(err)
}
// Inspect component state (debug mode only)
value, err := ah.GetComponentProperty("app-widget", "title")
```
## Package Layout
| File | Responsibility |
|------|----------------|
| `webview.go` | `Webview` struct, public API (navigate, click, type, screenshot, JS evaluation, DOM queries) |
| `cdp.go` | `CDPClient` -- WebSocket transport, CDP message framing, event dispatch, tab management |
| `actions.go` | `Action` interface, 19 concrete action types, `ActionSequence` fluent builder |
| `console.go` | `ConsoleWatcher`, `ExceptionWatcher`, console log formatting |
| `angular.go` | `AngularHelper` -- Zone.js stability, router navigation, component introspection, ngModel |
| `webview_test.go` | Unit tests for structs, options, and action building |
## Configuration Options
| Option | Default | Description |
|--------|---------|-------------|
| `WithDebugURL(url)` | *(required)* | Chrome DevTools HTTP debug endpoint, e.g. `http://localhost:9222` |
| `WithTimeout(d)` | 30 seconds | Default timeout for all browser operations |
| `WithConsoleLimit(n)` | 1000 | Maximum number of console messages retained in memory |
## Further Documentation
- [Architecture](architecture.md) -- internals, data flow, CDP protocol, type reference
- [Development Guide](development.md) -- build, test, contribute, coding standards
- [Project History](history.md) -- extraction origin, completed phases, known limitations