docs: graduate TODO/FINDINGS into production documentation
Replace internal task tracking (TODO.md, FINDINGS.md) with structured documentation in docs/. Trim CLAUDE.md to agent instructions only. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b752f15545
commit
7c46558e5b
6 changed files with 598 additions and 75 deletions
36
CLAUDE.md
36
CLAUDE.md
|
|
@ -1,26 +1,27 @@
|
|||
# CLAUDE.md
|
||||
|
||||
## What This Is
|
||||
|
||||
Chrome DevTools Protocol (CDP) client for browser automation, testing, and scraping. Module: `forge.lthn.ai/core/go-webview`
|
||||
Module: `forge.lthn.ai/core/go-webview` — Chrome DevTools Protocol client for browser automation.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
go test ./... # Run all tests
|
||||
go test -v -run Name # Run single test
|
||||
go test ./... # Run all tests (must pass before commit)
|
||||
go test -v -run Name # Run a single test
|
||||
gofmt -w . # Format code
|
||||
```
|
||||
|
||||
## Architecture
|
||||
## Coding Standards
|
||||
|
||||
- `webview.New(webview.WithDebugURL("http://localhost:9222"))` connects to Chrome
|
||||
- Navigation, DOM queries, console capture, screenshots, JS evaluation
|
||||
- Angular-specific helpers for SPA testing
|
||||
- UK English in all comments, docs, and commit messages
|
||||
- EUPL-1.2 licence header (`// SPDX-License-Identifier: EUPL-1.2`) in every Go file
|
||||
- Conventional commits: `type(scope): description`
|
||||
- Co-author trailer on every commit: `Co-Authored-By: Virgil <virgil@lethean.io>`
|
||||
- Test naming: `_Good` (happy path), `_Bad` (expected errors), `_Ugly` (panics/edge cases)
|
||||
|
||||
## Key API
|
||||
|
||||
```go
|
||||
wv, _ := webview.New(webview.WithDebugURL("http://localhost:9222"))
|
||||
wv, err := webview.New(webview.WithDebugURL("http://localhost:9222"))
|
||||
defer wv.Close()
|
||||
wv.Navigate("https://example.com")
|
||||
wv.Click("#submit")
|
||||
|
|
@ -28,9 +29,14 @@ wv.Type("#input", "text")
|
|||
screenshot, _ := wv.Screenshot()
|
||||
```
|
||||
|
||||
## Coding Standards
|
||||
## Docs
|
||||
|
||||
- UK English
|
||||
- `go test ./...` must pass before commit
|
||||
- Conventional commits: `type(scope): description`
|
||||
- Co-Author: `Co-Authored-By: Virgil <virgil@lethean.io>`
|
||||
- `docs/architecture.md` — CDP connection, DOM queries, console capture, Angular helpers
|
||||
- `docs/development.md` — prerequisites, build/test, coding standards, adding actions
|
||||
- `docs/history.md` — completed phases, known limitations, future considerations
|
||||
|
||||
## Forge Push
|
||||
|
||||
```bash
|
||||
git push ssh://git@forge.lthn.ai:2223/core/go-webview.git HEAD:main
|
||||
```
|
||||
|
|
|
|||
25
FINDINGS.md
25
FINDINGS.md
|
|
@ -1,25 +0,0 @@
|
|||
# FINDINGS.md -- go-webview
|
||||
|
||||
## 2026-02-19: Split from core/go (Virgil)
|
||||
|
||||
### Origin
|
||||
|
||||
Extracted from `forge.lthn.ai/core/go` `pkg/webview/` on 19 Feb 2026.
|
||||
|
||||
### Architecture
|
||||
|
||||
- Chrome DevTools Protocol (CDP) client over WebSocket
|
||||
- Connects to Chrome's remote debugging port (default 9222)
|
||||
- High-level API: `Navigate`, `Click`, `Type`, `QuerySelector`, `Evaluate`, `Screenshot`
|
||||
- Console capture via `Runtime.consoleAPICalled` CDP events
|
||||
- Multi-tab support via `Target.createTarget` / `Target.closeTarget`
|
||||
- Angular-specific helpers for SPA testing workflows
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `github.com/gorilla/websocket` -- WebSocket client for CDP connection
|
||||
|
||||
### Notes
|
||||
|
||||
- Requires a running Chrome instance with `--remote-debugging-port=9222`
|
||||
- No headless Chrome launcher included -- the caller must start Chrome separately
|
||||
35
TODO.md
35
TODO.md
|
|
@ -1,35 +0,0 @@
|
|||
# TODO.md -- go-webview
|
||||
|
||||
## Phase 1: Test Coverage
|
||||
|
||||
- [ ] Add headless Chrome tests runnable in CI (use `--headless=new` flag)
|
||||
- [ ] Test navigation, click, type, screenshot in headless mode
|
||||
- [ ] Add test fixtures (minimal HTML pages served by httptest)
|
||||
|
||||
## Phase 2: Multi-Tab Support
|
||||
|
||||
- [ ] `NewTab` / `CloseTab` exist but are not well tested
|
||||
- [ ] Add tests for multi-tab lifecycle (open, switch, close)
|
||||
- [ ] Verify console capture is tab-scoped (no cross-tab bleed)
|
||||
|
||||
## Phase 3: Angular Helpers
|
||||
|
||||
- [ ] Expand Angular-specific helpers beyond current set
|
||||
- [ ] Add component detection (query by Angular selector)
|
||||
- [ ] Add router integration (wait for navigation, read current route)
|
||||
- [ ] Add zone.js stability detection (wait for async operations)
|
||||
|
||||
## Phase 4: Screenshot Comparison
|
||||
|
||||
- [ ] Add visual regression testing support
|
||||
- [ ] Pixel-diff between baseline and current screenshots
|
||||
- [ ] Configurable threshold for acceptable difference percentage
|
||||
- [ ] Save diff images for failed comparisons
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Virgil in core/go writes tasks here after research
|
||||
2. This repo's dedicated session picks up tasks in phase order
|
||||
3. Mark `[x]` when done, note commit hash
|
||||
277
docs/architecture.md
Normal file
277
docs/architecture.md
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
# Architecture
|
||||
|
||||
Module: `forge.lthn.ai/core/go-webview`
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
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 |
|
||||
|
||||
---
|
||||
|
||||
## CDP Connection
|
||||
|
||||
### Initialisation
|
||||
|
||||
`NewCDPClient(debugURL string)` connects to Chrome's HTTP endpoint:
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
Events are dispatched to zero or more registered handlers via `OnEvent(method, handler)`. Each handler is called in its own goroutine so it cannot block the read loop.
|
||||
|
||||
### Connection Lifecycle
|
||||
|
||||
```
|
||||
New(WithDebugURL(...))
|
||||
└── 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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webview Struct
|
||||
|
||||
```go
|
||||
type Webview struct {
|
||||
mu sync.RWMutex
|
||||
client *CDPClient
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
timeout time.Duration // default 30s
|
||||
consoleLogs []ConsoleMessage
|
||||
consoleLimit int // default 1000
|
||||
}
|
||||
```
|
||||
|
||||
`New()` accepts functional options:
|
||||
|
||||
| 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). |
|
||||
|
||||
On construction, `New()` enables three CDP domains — `Runtime`, `Page`, and `DOM` — and registers a handler for `Runtime.consoleAPICalled` events to begin console capture immediately.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
```go
|
||||
type ElementInfo struct {
|
||||
NodeID int
|
||||
TagName string
|
||||
Attributes map[string]string
|
||||
InnerHTML string
|
||||
InnerText string
|
||||
BoundingBox *BoundingBox // nil if element has no layout box
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Click and Type
|
||||
|
||||
### Click
|
||||
|
||||
`click(ctx, selector)` resolves the element's bounding box, computes the centre point, then dispatches `Input.dispatchMouseEvent` for `mousePressed` then `mouseReleased`. If the element has no bounding box (e.g. a hidden element), it falls back to evaluating `document.querySelector(selector)?.click()`.
|
||||
|
||||
### Type
|
||||
|
||||
`typeText(ctx, selector, text)` first focuses the element via JavaScript, then dispatches `Input.dispatchKeyEvent` with `type: "keyDown"` and `type: "keyUp"` for each character in the string individually.
|
||||
|
||||
`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.
|
||||
|
||||
### Basic Capture (Webview)
|
||||
|
||||
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
|
||||
wv.ClearConsole()
|
||||
```
|
||||
|
||||
### 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"`
|
||||
- `Errors()`, `Warnings()`, `HasErrors()`, `ErrorCount()`
|
||||
|
||||
### 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`.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
`GetHTML(selector string)` evaluates `outerHTML` on the matched element, or `document.documentElement.outerHTML` when the selector is empty.
|
||||
|
||||
---
|
||||
|
||||
## Action System
|
||||
|
||||
The `Action` interface has a single method:
|
||||
|
||||
```go
|
||||
type Action interface {
|
||||
Execute(ctx context.Context, wv *Webview) error
|
||||
}
|
||||
```
|
||||
|
||||
Concrete action types cover: `Click`, `Type`, `Navigate`, `Wait`, `WaitForSelector`, `Scroll`, `ScrollIntoView`, `Focus`, `Blur`, `Clear`, `Select`, `Check`, `Hover`, `DoubleClick`, `RightClick`, `PressKey`, `SetAttribute`, `RemoveAttribute`, `SetValue`.
|
||||
|
||||
`ActionSequence` provides a fluent builder:
|
||||
|
||||
```go
|
||||
err := NewActionSequence().
|
||||
Navigate("https://example.com").
|
||||
WaitForSelector("#login-form").
|
||||
Type("#email", "user@example.com").
|
||||
Type("#password", "secret").
|
||||
Click("#submit").
|
||||
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.
|
||||
|
||||
`DragAndDrop(sourceSelector, targetSelector)` dispatches `mousePressed`, `mouseMoved`, and `mouseReleased` events between the centre points of the two elements.
|
||||
|
||||
---
|
||||
|
||||
## Angular Helpers
|
||||
|
||||
`AngularHelper` (constructed via `NewAngularHelper(wv)`) provides SPA-specific utilities. All methods accept the `AngularHelper.timeout` deadline (default 30 s).
|
||||
|
||||
### 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`.
|
||||
|
||||
### Zone.js Stability
|
||||
|
||||
`WaitForAngular()` waits for Zone.js to report stability by checking `zone.isStable` and subscribing to `zone.onStable`. If the injector-based approach fails (production builds without debug info), it falls back to polling `window.Zone.current._inner._hasPendingMicrotasks` and `_hasPendingMacrotasks` at 50 ms intervals.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
`CallComponentMethod(selector, method, args...)` invokes a method on the component instance and triggers change detection.
|
||||
|
||||
`GetService(name)` retrieves a named service from the root injector and returns a JSON-serialisable representation.
|
||||
|
||||
### 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()`.
|
||||
|
||||
---
|
||||
|
||||
## 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 `GetVersion(debugURL)` are package-level utilities that query the HTTP endpoint without requiring an active WebSocket connection.
|
||||
|
||||
---
|
||||
|
||||
## Emulation
|
||||
|
||||
`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.
|
||||
213
docs/development.md
Normal file
213
docs/development.md
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# Development Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Go
|
||||
|
||||
Go 1.25 or later is required. The module path is `forge.lthn.ai/core/go-webview`.
|
||||
|
||||
### Chrome or Chromium
|
||||
|
||||
A running Chrome or Chromium instance with the remote debugging port enabled is required for any tests or usage that exercises the CDP connection. The package does not launch Chrome itself.
|
||||
|
||||
Start Chrome with the remote debugging port:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
The default debug URL is `http://localhost:9222`. Verify it is reachable by visiting `http://localhost:9222/json` in a browser or with `curl`.
|
||||
|
||||
### Dependencies
|
||||
|
||||
The only runtime dependency is `github.com/gorilla/websocket v1.5.3`, declared in `go.mod`. Fetch dependencies with:
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build and Test
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
# Run a specific test
|
||||
go test -v -run TestActionSequence_Good ./...
|
||||
|
||||
# Run all tests with verbose output
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
All test functions use the standard `testing.T` interface; the project does not use a test framework.
|
||||
|
||||
### CI Headless Tests
|
||||
|
||||
To add tests that exercise the full CDP stack in CI:
|
||||
|
||||
1. Start Chrome in headless mode in the CI job before running `go test`.
|
||||
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`).
|
||||
|
||||
---
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Language
|
||||
|
||||
UK English throughout all source comments, documentation, commit messages, and identifiers where natural language appears. Use "colour", "organisation", "behaviour", "initialise", not their American equivalents.
|
||||
|
||||
### Formatting
|
||||
|
||||
Standard `gofmt` formatting is mandatory. Run before committing:
|
||||
|
||||
```bash
|
||||
gofmt -w .
|
||||
```
|
||||
|
||||
### Types and Error Handling
|
||||
|
||||
- All exported functions must have Go doc comments.
|
||||
- Use `fmt.Errorf("context: %w", err)` for error wrapping so callers can use `errors.Is` and `errors.As`.
|
||||
- Return errors; do not panic in library code.
|
||||
- Use `context.Context` for all operations that involve I/O or waiting so callers can impose deadlines.
|
||||
|
||||
### Concurrency
|
||||
|
||||
- Protect shared mutable state with `sync.RWMutex` (read lock for reads, write lock for writes).
|
||||
- Do not call handlers or callbacks while holding a lock. Copy the slice of handlers, release the lock, then call them.
|
||||
- CDP WebSocket writes are serialised with a dedicated mutex in `CDPClient`; do not write to `conn` directly from outside `cdp.go`.
|
||||
|
||||
### Licence Header
|
||||
|
||||
Every Go source file must begin with:
|
||||
|
||||
```go
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
```
|
||||
|
||||
The project is licenced under the European Union Public Licence 1.2 (EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
Use conventional commits:
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
```
|
||||
|
||||
Common types: `feat`, `fix`, `docs`, `test`, `refactor`, `chore`.
|
||||
|
||||
Example scopes: `cdp`, `angular`, `console`, `actions`.
|
||||
|
||||
All commits must include the co-author trailer:
|
||||
|
||||
```
|
||||
Co-Authored-By: Virgil <virgil@lethean.io>
|
||||
```
|
||||
|
||||
Full example:
|
||||
|
||||
```
|
||||
feat(console): add ExceptionWatcher with stack trace capture
|
||||
|
||||
Subscribes to Runtime.exceptionThrown events and exposes a reactive
|
||||
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.
|
||||
2. Implement `Execute(ctx context.Context, wv *Webview) error` on the struct.
|
||||
3. Add a builder method on `ActionSequence` that appends the new action.
|
||||
4. Add a `_Good` test in `webview_test.go` that verifies the struct fields are set correctly.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
// SubmitAction submits a form element.
|
||||
type SubmitAction struct {
|
||||
Selector string
|
||||
}
|
||||
|
||||
func (a SubmitAction) Execute(ctx context.Context, wv *Webview) error {
|
||||
script := fmt.Sprintf("document.querySelector(%q)?.submit()", a.Selector)
|
||||
_, err := wv.evaluate(ctx, script)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ActionSequence) Submit(selector string) *ActionSequence {
|
||||
return s.Add(SubmitAction{Selector: selector})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Angular Helper
|
||||
|
||||
Add methods to `AngularHelper` in `angular.go`. Follow the established pattern:
|
||||
|
||||
1. Obtain a context with the helper's timeout using `context.WithTimeout`.
|
||||
2. Build the JavaScript as a self-invoking function expression `(function() { ... })()`.
|
||||
3. Call `ah.wv.evaluate(ctx, script)` to execute it.
|
||||
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()`.
|
||||
|
||||
---
|
||||
|
||||
## Forge Push
|
||||
|
||||
Push to the canonical remote via SSH:
|
||||
|
||||
```bash
|
||||
git push ssh://git@forge.lthn.ai:2223/core/go-webview.git HEAD:main
|
||||
```
|
||||
|
||||
HTTPS authentication is not available on this remote.
|
||||
87
docs/history.md
Normal file
87
docs/history.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Project History
|
||||
|
||||
## Origin
|
||||
|
||||
go-webview was extracted from the `pkg/webview/` directory of `forge.lthn.ai/core/go` on 19 February 2026 by Virgil. The extraction made the package independently importable and gave it its own module path, dependency management, and commit history.
|
||||
|
||||
---
|
||||
|
||||
## Completed Phases
|
||||
|
||||
### Extraction from core/go
|
||||
|
||||
Commit `45f119b9ac0e0ebe34f5c8387a070a5b8bd2de6b` — 2026-02-19
|
||||
|
||||
Initial extraction. All source files were moved from `go` `pkg/webview/` into the root of this repository. The module was renamed from the internal path to `forge.lthn.ai/core/go-webview`.
|
||||
|
||||
Files established at extraction:
|
||||
|
||||
- `webview.go` — `Webview` struct, public API
|
||||
- `cdp.go` — `CDPClient` WebSocket transport
|
||||
- `actions.go` — `Action` interface and all concrete action types
|
||||
- `console.go` — `ConsoleWatcher` and `ExceptionWatcher`
|
||||
- `angular.go` — `AngularHelper`
|
||||
- `webview_test.go` — struct and option tests
|
||||
|
||||
### Dependency Fix
|
||||
|
||||
Commit `56d2d476a110b740828ff66b44cc4cbd689dd969` — 2026-02-19
|
||||
|
||||
Added `github.com/gorilla/websocket v1.5.3` to `go.mod` and `go.sum`. The `go.mod` was missing the explicit `require` directive after the extraction.
|
||||
|
||||
### Fleet Delegation Docs
|
||||
|
||||
Commit `b752f155459c8bf610cfcc6486f4a9431ec4b7c3` — 2026-02-19
|
||||
|
||||
Added `TODO.md` and `FINDINGS.md` to record research findings and task backlog for the agent fleet. These files have since been superseded by this `docs/` directory and have been removed.
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### No Chrome Launcher
|
||||
|
||||
The package has no built-in mechanism for starting Chrome. All usage requires the caller to ensure a Chrome or Chromium process is already running with `--remote-debugging-port=9222`. This is intentional — launcher behaviour (headless vs headed, profile selection, proxy configuration) varies too widely between use cases to be handled generically.
|
||||
|
||||
### Single-Tab Webview
|
||||
|
||||
The `Webview` struct connects to one tab at construction time. `CDPClient.NewTab` returns a raw `CDPClient` rather than a full `Webview`, so the high-level API (navigate, click, screenshot, etc.) is not available directly on new tabs without wrapping the client manually.
|
||||
|
||||
### Console Capture: Value-Only Arguments
|
||||
|
||||
The console event handler extracts only arguments with a `value` field (primitives). Complex objects that Chrome serialises as `type: "object"` with a `description` rather than a `value` are silently omitted from the captured text. This means `console.log({foo: 'bar'})` will produce an empty message.
|
||||
|
||||
### Angular Helper: Debug Mode Dependency
|
||||
|
||||
`AngularHelper` methods that use `window.ng.probe` require the Angular application to be running in development mode (debug mode). Production builds compiled with `enableProdMode()` disable the debug utilities, making component introspection, `GetComponentProperty`, `SetComponentProperty`, `CallComponentMethod`, and `GetService` non-functional. `WaitForAngular` has a Zone.js polling fallback that works in production.
|
||||
|
||||
### Angular Helper: AngularJS 1.x
|
||||
|
||||
Detection of AngularJS 1.x (`window.angular.element`) is implemented in `isAngularApp`, but none of the other `AngularHelper` methods have 1.x-specific code paths. Methods beyond `WaitForAngular` are Angular 2+ only.
|
||||
|
||||
### Zone Stability Timeout
|
||||
|
||||
The JavaScript Promise used in `waitForZoneStability` has an internal 5-second `setTimeout` fallback. If Zone.js does not become stable within that window, the Promise resolves with the current stability value rather than rejecting. This can cause `WaitForAngular` to return successfully even when the application is still processing asynchronous work.
|
||||
|
||||
### waitForLoad Polling
|
||||
|
||||
`waitForLoad` polls `document.readyState` at 100 ms intervals rather than subscribing to the `Page.loadEventFired` CDP event. This works correctly but adds up to 100 ms of unnecessary latency after the load event fires.
|
||||
|
||||
### Key Dispatch: Modifier Keys
|
||||
|
||||
`PressKeyAction` handles named keys but does not support modifier combinations (Ctrl+A, Shift+Tab, etc.). Modifier key presses require constructing the CDP params map manually and calling `CDPClient.Call` directly.
|
||||
|
||||
### CloseTab Implementation
|
||||
|
||||
`CDPClient.CloseTab` calls `Browser.close`, which closes the entire browser rather than just the tab. The correct CDP command for closing a single tab is `Target.closeTarget` with the target's ID extracted from the WebSocket URL. This is a bug.
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
The following were identified as planned phases at the time of extraction. They are recorded here for context but are not committed work items.
|
||||
|
||||
- **Headless CI tests** — integration tests using `net/http/httptest` fixtures and Chrome with `--headless=new`, runnable without a display.
|
||||
- **Multi-tab lifecycle tests** — tests verifying that `NewTab`/`CloseTab` work correctly and that console events are scoped to their tab.
|
||||
- **Angular helper expansion** — component detection by Angular selector, proper production-mode router integration.
|
||||
- **Visual regression** — pixel-diff comparison between a baseline screenshot and a current screenshot, with configurable threshold and diff image output.
|
||||
Loading…
Add table
Reference in a new issue