feat(webview): console watcher additions + canonical type normalisation
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Spark draft + opus-style fixes during integration:

- normalizeConsoleType applied consistently across FormatConsoleOutput
  + Errors/ErrorsAll + Warnings/WarningsAll iterators — previously raw
  "warning" inputs missed the "warn" comparisons silently
- core.Trim instead of non-existent core.TrimSpace

Spark feature work preserved (Errors/Warnings iterators, formatted
console output, type normalisation helper).

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-14 15:09:57 +01:00
parent a6304b8e29
commit 7a4450cf9f
2 changed files with 117 additions and 33 deletions

View file

@ -5,6 +5,7 @@ import (
"context"
"iter"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
@ -55,6 +56,93 @@ func NewConsoleWatcher(wv *Webview) *ConsoleWatcher {
return cw
}
// normalizeConsoleType converts CDP event types to package-level values.
func normalizeConsoleType(raw string) string {
normalized := strings.ToLower(core.Trim(core.Sprint(raw)))
switch normalized {
case "warning":
return "warn"
default:
return normalized
}
}
// consoleTextFromArgs extracts message text from Runtime.consoleAPICalled args.
func consoleTextFromArgs(args []any) string {
text := core.NewBuilder()
for i, arg := range args {
if i > 0 {
text.WriteString(" ")
}
text.WriteString(consoleArgText(arg))
}
return text.String()
}
func consoleArgText(arg any) string {
remoteObj, ok := arg.(map[string]any)
if !ok {
return consoleValueToString(arg)
}
if value, ok := remoteObj["value"]; ok {
return consoleValueToString(value)
}
if desc, ok := remoteObj["description"].(string); ok && desc != "" {
return desc
}
if preview, ok := remoteObj["preview"].(map[string]any); ok {
if description, ok := preview["description"].(string); ok && description != "" {
return description
}
}
if preview, ok := remoteObj["preview"].(map[string]any); ok {
if value, ok := preview["value"].(string); ok && value != "" {
return value
}
}
if r := core.JSONMarshal(remoteObj); r.OK {
if encoded, ok := r.Value.([]byte); ok {
return string(encoded)
}
}
return ""
}
func consoleValueToString(value any) string {
if value == nil {
return "null"
}
if valueStr, ok := value.(string); ok {
return valueStr
}
if r := core.JSONMarshal(value); r.OK {
if encoded, ok := r.Value.([]byte); ok {
return string(encoded)
}
}
return core.Sprint(value)
}
func consoleMessageTimestamp(params map[string]any) time.Time {
timestamp, ok := params["timestamp"].(float64)
if !ok {
return time.Now()
}
seconds := int64(timestamp)
nanoseconds := int64((timestamp - float64(seconds)) * float64(time.Second))
return time.Unix(seconds, nanoseconds).UTC()
}
// AddFilter adds a filter to the watcher.
func (cw *ConsoleWatcher) AddFilter(filter ConsoleFilter) {
cw.mu.Lock()
@ -156,7 +244,7 @@ func (cw *ConsoleWatcher) ErrorsAll() iter.Seq[ConsoleMessage] {
defer cw.mu.RUnlock()
for _, msg := range cw.messages {
if msg.Type == "error" {
if normalizeConsoleType(msg.Type) == "error" {
if !yield(msg) {
return
}
@ -177,7 +265,7 @@ func (cw *ConsoleWatcher) WarningsAll() iter.Seq[ConsoleMessage] {
defer cw.mu.RUnlock()
for _, msg := range cw.messages {
if msg.Type == "warning" {
if normalizeConsoleType(msg.Type) == "warn" {
if !yield(msg) {
return
}
@ -268,21 +356,11 @@ func (cw *ConsoleWatcher) ErrorCount() int {
// handleConsoleEvent processes incoming console events.
func (cw *ConsoleWatcher) handleConsoleEvent(params map[string]any) {
msgType, _ := params["type"].(string)
msgType := normalizeConsoleType(core.Sprint(params["type"]))
// Extract args
args, _ := params["args"].([]any)
text := core.NewBuilder()
for i, arg := range args {
if argMap, ok := arg.(map[string]any); ok {
if val, ok := argMap["value"]; ok {
if i > 0 {
text.WriteString(" ")
}
text.WriteString(core.Sprint(val))
}
}
}
text := consoleTextFromArgs(args)
// Extract stack trace info
stackTrace, _ := params["stackTrace"].(map[string]any)
@ -300,8 +378,8 @@ func (cw *ConsoleWatcher) handleConsoleEvent(params map[string]any) {
msg := ConsoleMessage{
Type: msgType,
Text: text.String(),
Timestamp: time.Now(),
Text: text,
Timestamp: consoleMessageTimestamp(params),
URL: url,
Line: line,
Column: column,
@ -346,7 +424,7 @@ func (cw *ConsoleWatcher) matchesFilter(msg ConsoleMessage) bool {
// matchesSingleFilter checks if a message matches a specific filter.
func (cw *ConsoleWatcher) matchesSingleFilter(msg ConsoleMessage, filter ConsoleFilter) bool {
if filter.Type != "" && msg.Type != filter.Type {
if filter.Type != "" && msg.Type != normalizeConsoleType(filter.Type) {
return false
}
if filter.Pattern != "" {
@ -572,10 +650,10 @@ func FormatConsoleOutput(messages []ConsoleMessage) string {
output := core.NewBuilder()
for _, msg := range messages {
prefix := ""
switch msg.Type {
switch normalizeConsoleType(msg.Type) {
case "error":
prefix = "[ERROR]"
case "warning":
case "warn":
prefix = "[WARN]"
case "info":
prefix = "[INFO]"

View file

@ -363,6 +363,10 @@ func (wv *Webview) SetViewport(width, height int) error {
"deviceScaleFactor": 1,
"mobile": false,
})
if err != nil {
return coreerr.E("Webview.SetViewport", "failed to set viewport", err)
}
return err
}
@ -374,6 +378,10 @@ func (wv *Webview) SetUserAgent(userAgent string) error {
_, err := wv.client.Call(ctx, "Emulation.setUserAgentOverride", map[string]any{
"userAgent": userAgent,
})
if err != nil {
return coreerr.E("Webview.SetUserAgent", "failed to set user agent", err)
}
return err
}
@ -398,6 +406,10 @@ func (wv *Webview) GoBack() error {
_, err := wv.client.Call(ctx, "Page.goBackOrForward", map[string]any{
"delta": -1,
})
if err != nil {
return coreerr.E("Webview.GoBack", "failed to go back", err)
}
return err
}
@ -409,6 +421,10 @@ func (wv *Webview) GoForward() error {
_, err := wv.client.Call(ctx, "Page.goBackOrForward", map[string]any{
"delta": 1,
})
if err != nil {
return coreerr.E("Webview.GoForward", "failed to go forward", err)
}
return err
}
@ -458,21 +474,11 @@ func (wv *Webview) enableConsole() error {
// handleConsoleEvent processes console API events.
func (wv *Webview) handleConsoleEvent(params map[string]any) {
msgType, _ := params["type"].(string)
msgType := normalizeConsoleType(core.Sprint(params["type"]))
// Extract args
args, _ := params["args"].([]any)
text := core.NewBuilder()
for i, arg := range args {
if argMap, ok := arg.(map[string]any); ok {
if val, ok := argMap["value"]; ok {
if i > 0 {
text.WriteString(" ")
}
text.WriteString(core.Sprint(val))
}
}
}
text := consoleTextFromArgs(args)
// Extract stack trace info
stackTrace, _ := params["stackTrace"].(map[string]any)
@ -490,8 +496,8 @@ func (wv *Webview) handleConsoleEvent(params map[string]any) {
wv.addConsoleMessage(ConsoleMessage{
Type: msgType,
Text: text.String(),
Timestamp: time.Now(),
Text: text,
Timestamp: consoleMessageTimestamp(params),
URL: url,
Line: line,
Column: column,