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:
Snider 2026-02-20 15:01:55 +00:00
parent b752f15545
commit 7c46558e5b
6 changed files with 598 additions and 75 deletions

View file

@ -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
```

View file

@ -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
View file

@ -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
View 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
View 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
View 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.