fix(webview): AX-6 webview_test.go + Bad/Ugly variants (#625 #626)
Some checks failed
Security Scan / security (push) Has been cancelled
Test / test (push) Has been cancelled

- webview_test.go: removed replaceable io and strings usage; AX-6 annotations on net/http + net/http/httptest fixture imports
- webview.go: WithTimeout now rejects non-positive durations
- webview_test.go: added Bad/Ugly variants for ConsoleMessage, ElementInfo, BoundingBox, WithTimeout

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=625
Closes tasks.lthn.sh/view.php?id=626
This commit is contained in:
Snider 2026-04-25 14:54:44 +01:00
parent 47c2f01c9c
commit 1c47ae2c18
2 changed files with 135 additions and 8 deletions

View file

@ -106,6 +106,9 @@ func WithDebugURL(url string) Option {
// webview.New(webview.WithDebugURL("http://localhost:9222"), webview.WithTimeout(10*time.Second))
func WithTimeout(d time.Duration) Option {
return func(wv *Webview) error {
if d <= 0 {
return coreerr.E("Webview.WithTimeout", "timeout must be positive", nil)
}
wv.timeout = d
return nil
}

View file

@ -3,10 +3,8 @@ package webview
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"net/http" // Note: AX-6 intrinsic - in-process CDP fixture server; no Core HTTP test server primitive yet.
"net/http/httptest" // Note: AX-6 intrinsic - bounded in-process CDP fixture lifecycle for debug endpoint tests.
"testing"
"time"
)
@ -33,6 +31,40 @@ func TestConsoleMessage_Good(t *testing.T) {
}
}
// TestConsoleMessage_Bad_ZeroValue verifies a missing CDP payload stays distinguishable from a populated message.
func TestConsoleMessage_Bad_ZeroValue(t *testing.T) {
var msg ConsoleMessage
if msg.Type != "" {
t.Errorf("Expected empty type, got %q", msg.Type)
}
if msg.Text != "" {
t.Errorf("Expected empty text, got %q", msg.Text)
}
if !msg.Timestamp.IsZero() {
t.Errorf("Expected zero timestamp, got %v", msg.Timestamp)
}
if msg.Line != 0 || msg.Column != 0 {
t.Errorf("Expected zero source coordinates, got line %d column %d", msg.Line, msg.Column)
}
}
// TestConsoleMessage_Ugly_ControlCharacters verifies log text is retained as data before output formatting sanitises it.
func TestConsoleMessage_Ugly_ControlCharacters(t *testing.T) {
msg := ConsoleMessage{
Type: "error",
Text: "first line\nsecond line\x1b[31m",
Line: -1,
}
if msg.Text != "first line\nsecond line\x1b[31m" {
t.Errorf("Expected text to be retained verbatim, got %q", msg.Text)
}
if msg.Line != -1 {
t.Errorf("Expected malformed source line to be retained, got %d", msg.Line)
}
}
// TestElementInfo_Good verifies the ElementInfo struct has expected fields.
func TestElementInfo_Good(t *testing.T) {
elem := ElementInfo{
@ -69,6 +101,41 @@ func TestElementInfo_Good(t *testing.T) {
}
}
// TestElementInfo_Bad_NilAttributes verifies a malformed element payload can be inspected without a map allocation.
func TestElementInfo_Bad_NilAttributes(t *testing.T) {
elem := ElementInfo{
NodeID: 321,
TagName: "DIV",
Attributes: nil,
}
if elem.Attributes != nil {
t.Fatalf("Expected nil attributes, got %#v", elem.Attributes)
}
if got := elem.Attributes["missing"]; got != "" {
t.Fatalf("Expected missing nil-map attribute to read as empty, got %q", got)
}
}
// TestElementInfo_Ugly_EmptyContentWithZeroBox verifies boundary element data keeps an explicit zero-size box.
func TestElementInfo_Ugly_EmptyContentWithZeroBox(t *testing.T) {
elem := ElementInfo{
NodeID: 0,
TagName: "",
Attributes: map[string]string{},
InnerHTML: "",
InnerText: "",
BoundingBox: &BoundingBox{},
}
if elem.BoundingBox == nil {
t.Fatal("Expected explicit zero bounding box")
}
if elem.BoundingBox.Width != 0 || elem.BoundingBox.Height != 0 {
t.Fatalf("Expected zero-size bounding box, got %#v", elem.BoundingBox)
}
}
// TestBoundingBox_Good verifies the BoundingBox struct has expected fields.
func TestBoundingBox_Good(t *testing.T) {
box := BoundingBox{
@ -92,6 +159,37 @@ func TestBoundingBox_Good(t *testing.T) {
}
}
// TestBoundingBox_Bad_NegativeDimensions verifies invalid geometry remains visible to callers.
func TestBoundingBox_Bad_NegativeDimensions(t *testing.T) {
box := BoundingBox{
X: -10,
Y: -20,
Width: -100,
Height: -50,
}
if box.Width != -100 {
t.Errorf("Expected negative width to be retained, got %f", box.Width)
}
if box.Height != -50 {
t.Errorf("Expected negative height to be retained, got %f", box.Height)
}
}
// TestBoundingBox_Ugly_ZeroArea verifies zero-area geometry is represented exactly.
func TestBoundingBox_Ugly_ZeroArea(t *testing.T) {
box := BoundingBox{
X: 10,
Y: 20,
Width: 0,
Height: 0,
}
if box.Width != 0 || box.Height != 0 {
t.Fatalf("Expected zero-area box, got %#v", box)
}
}
// TestWithTimeout_Good verifies the WithTimeout option sets timeout correctly.
func TestWithTimeout_Good(t *testing.T) {
// We can't fully test without a real Chrome connection,
@ -109,6 +207,31 @@ func TestWithTimeout_Good(t *testing.T) {
}
}
// TestWithTimeout_Bad_NonPositiveDuration verifies invalid timeouts are rejected.
func TestWithTimeout_Bad_NonPositiveDuration(t *testing.T) {
for _, timeout := range []time.Duration{0, -1 * time.Second} {
wv := &Webview{timeout: 30 * time.Second}
err := WithTimeout(timeout)(wv)
if err == nil {
t.Fatalf("Expected error for timeout %v", timeout)
}
if wv.timeout != 30*time.Second {
t.Fatalf("Expected existing timeout to remain unchanged, got %v", wv.timeout)
}
}
}
// TestWithTimeout_Ugly_MinimumPositiveDuration verifies the smallest positive timeout is still accepted.
func TestWithTimeout_Ugly_MinimumPositiveDuration(t *testing.T) {
wv := &Webview{}
if err := WithTimeout(time.Nanosecond)(wv); err != nil {
t.Fatalf("WithTimeout returned error: %v", err)
}
if wv.timeout != time.Nanosecond {
t.Fatalf("Expected timeout 1ns, got %v", wv.timeout)
}
}
// TestWithConsoleLimit_Good verifies the WithConsoleLimit option sets limit correctly.
func TestWithConsoleLimit_Good(t *testing.T) {
wv := &Webview{}
@ -620,13 +743,14 @@ func TestValidateNavigationURL_Bad_RejectsDangerousSchemes(t *testing.T) {
// TestDoDebugRequest_Bad_RejectsOversizedBody verifies debug responses are bounded.
func TestDoDebugRequest_Bad_RejectsOversizedBody(t *testing.T) {
var payload strings.Builder
payload.Grow(maxDebugResponseBytes + 1)
payload.WriteString(strings.Repeat("a", maxDebugResponseBytes+1))
payload := make([]byte, maxDebugResponseBytes+1)
for i := range payload {
payload[i] = 'a'
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, payload.String())
_, _ = w.Write(payload)
}))
t.Cleanup(server.Close)