Align webview behavior with RFC

This commit is contained in:
Snider 2026-04-15 14:21:23 +01:00
parent d6fcd6a2f4
commit f6e235d83e
5 changed files with 57 additions and 80 deletions

View file

@ -290,8 +290,8 @@ func (ah *AngularHelper) GetRouterState() (*AngularRouterState, error) {
state.Fragment = core.Sprint(fragment)
}
state.Params = stringifyMap(resultMap["params"])
state.QueryParams = stringifyMap(resultMap["queryParams"])
state.Params = copyStringOnlyMap(resultMap["params"])
state.QueryParams = copyStringOnlyMap(resultMap["queryParams"])
return state, nil
}
@ -598,12 +598,14 @@ func getString(m map[string]any, key string) string {
return ""
}
func stringifyMap(value any) map[string]string {
func copyStringOnlyMap(value any) map[string]string {
switch typed := value.(type) {
case map[string]any:
result := make(map[string]string, len(typed))
for key, item := range typed {
result[key] = core.Sprint(item)
if text, ok := item.(string); ok {
result[key] = text
}
}
return result
case map[string]string:

View file

@ -672,7 +672,7 @@ func TestExceptionWatcherWaitForException_Good_PreservesExistingHandlers(t *test
}
}
func TestWebviewGoBack_Good_UsesNavigationHistoryAndWaitsForLoad(t *testing.T) {
func TestWebviewGoBack_Good_UsesGoBackOrForwardAndWaitsForLoad(t *testing.T) {
server := newFakeCDPServer(t)
target := server.primaryTarget()
@ -681,17 +681,9 @@ func TestWebviewGoBack_Good_UsesNavigationHistoryAndWaitsForLoad(t *testing.T) {
methods = append(methods, msg.Method)
switch msg.Method {
case "Page.getNavigationHistory":
target.reply(msg.ID, map[string]any{
"currentIndex": float64(1),
"entries": []map[string]any{
{"id": float64(101), "url": "https://example.com/one"},
{"id": float64(202), "url": "https://example.com/two"},
},
})
case "Page.navigateToHistoryEntry":
if got, ok := msg.Params["entryId"].(float64); !ok || got != 101 {
t.Fatalf("navigateToHistoryEntry entryId = %v, want 101", msg.Params["entryId"])
case "Page.goBackOrForward":
if got, ok := msg.Params["delta"].(float64); !ok || got != -1 {
t.Fatalf("goBackOrForward delta = %v, want -1", msg.Params["delta"])
}
target.reply(msg.ID, map[string]any{})
case "Runtime.evaluate":
@ -717,27 +709,29 @@ func TestWebviewGoBack_Good_UsesNavigationHistoryAndWaitsForLoad(t *testing.T) {
t.Fatalf("GoBack returned error: %v", err)
}
if len(methods) != 3 {
t.Fatalf("expected 3 CDP calls, got %d (%v)", len(methods), methods)
if len(methods) != 2 {
t.Fatalf("expected 2 CDP calls, got %d (%v)", len(methods), methods)
}
if methods[0] != "Page.getNavigationHistory" || methods[1] != "Page.navigateToHistoryEntry" || methods[2] != "Runtime.evaluate" {
if methods[0] != "Page.goBackOrForward" || methods[1] != "Runtime.evaluate" {
t.Fatalf("unexpected call sequence: %v", methods)
}
}
func TestWebviewGoForward_Bad_NoHistoryEntry(t *testing.T) {
func TestWebviewGoForward_Good_UsesGoBackOrForwardAndWaitsForLoad(t *testing.T) {
server := newFakeCDPServer(t)
target := server.primaryTarget()
target.onMessage = func(target *fakeCDPTarget, msg cdpMessage) {
if msg.Method != "Page.getNavigationHistory" {
switch msg.Method {
case "Page.goBackOrForward":
if got, ok := msg.Params["delta"].(float64); !ok || got != 1 {
t.Fatalf("goBackOrForward delta = %v, want 1", msg.Params["delta"])
}
target.reply(msg.ID, map[string]any{})
case "Runtime.evaluate":
target.replyValue(msg.ID, "complete")
default:
t.Fatalf("unexpected method %q", msg.Method)
}
target.reply(msg.ID, map[string]any{
"currentIndex": float64(0),
"entries": []map[string]any{
{"id": float64(101), "url": "https://example.com/one"},
},
})
}
client, err := NewCDPClient(server.DebugURL())
@ -752,8 +746,8 @@ func TestWebviewGoForward_Bad_NoHistoryEntry(t *testing.T) {
timeout: time.Second,
}
if err := wv.GoForward(); err == nil {
t.Fatal("GoForward succeeded without a forward history entry")
if err := wv.GoForward(); err != nil {
t.Fatalf("GoForward returned error: %v", err)
}
}
@ -791,7 +785,7 @@ func TestWebviewEvaluate_Bad_UsesExceptionText(t *testing.T) {
}
}
func TestAngularHelperGetRouterState_Good_StringifiesParams(t *testing.T) {
func TestAngularHelperGetRouterState_Good_KeepsOnlyStringParams(t *testing.T) {
server := newFakeCDPServer(t)
target := server.primaryTarget()
target.onMessage = func(target *fakeCDPTarget, msg cdpMessage) {
@ -802,11 +796,12 @@ func TestAngularHelperGetRouterState_Good_StringifiesParams(t *testing.T) {
"url": "/items/123",
"fragment": "details",
"params": map[string]any{
"id": float64(123),
"id": "123",
"active": true,
},
"queryParams": map[string]any{
"page": float64(2),
"page": "2",
"debug": float64(1),
},
})
}
@ -828,10 +823,16 @@ func TestAngularHelperGetRouterState_Good_StringifiesParams(t *testing.T) {
if err != nil {
t.Fatalf("GetRouterState returned error: %v", err)
}
if state.Params["id"] != "123" || state.Params["active"] != "true" {
if state.Params["id"] != "123" {
t.Fatalf("unexpected params: %#v", state.Params)
}
if _, ok := state.Params["active"]; ok {
t.Fatalf("expected non-string params to be omitted, got %#v", state.Params)
}
if state.QueryParams["page"] != "2" {
t.Fatalf("unexpected query params: %#v", state.QueryParams)
}
if _, ok := state.QueryParams["debug"]; ok {
t.Fatalf("expected non-string query params to be omitted, got %#v", state.QueryParams)
}
}

View file

@ -27,7 +27,7 @@ type ConsoleWatcher struct {
// ConsoleFilter filters console messages.
type ConsoleFilter struct {
Type string // Filter by type (log, warn, error, info, debug), empty for all
Type string // Exact message type match, empty for all
Pattern string // Filter by text pattern (substring match)
}
@ -60,12 +60,7 @@ func NewConsoleWatcher(wv *Webview) *ConsoleWatcher {
// 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
}
return normalized
}
// consoleTextFromArgs extracts message text from Runtime.consoleAPICalled args.
@ -279,7 +274,7 @@ func (cw *ConsoleWatcher) ErrorsAll() iter.Seq[ConsoleMessage] {
defer cw.mu.RUnlock()
for _, msg := range cw.messages {
if normalizeConsoleType(msg.Type) == "error" {
if msg.Type == "error" {
if !yield(msg) {
return
}
@ -300,7 +295,7 @@ func (cw *ConsoleWatcher) WarningsAll() iter.Seq[ConsoleMessage] {
defer cw.mu.RUnlock()
for _, msg := range cw.messages {
if normalizeConsoleType(msg.Type) == "warn" {
if msg.Type == "warning" {
if !yield(msg) {
return
}
@ -361,7 +356,7 @@ func (cw *ConsoleWatcher) HasErrors() bool {
defer cw.mu.RUnlock()
for _, msg := range cw.messages {
if normalizeConsoleType(msg.Type) == "error" {
if msg.Type == "error" {
return true
}
}
@ -382,7 +377,7 @@ func (cw *ConsoleWatcher) ErrorCount() int {
count := 0
for _, msg := range cw.messages {
if normalizeConsoleType(msg.Type) == "error" {
if msg.Type == "error" {
count++
}
}
@ -459,7 +454,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 != normalizeConsoleType(filter.Type) {
if filter.Type != "" && msg.Type != filter.Type {
return false
}
if filter.Pattern != "" {
@ -684,7 +679,7 @@ func FormatConsoleOutput(messages []ConsoleMessage) string {
switch normalizeConsoleType(msg.Type) {
case "error":
prefix = "[ERROR]"
case "warn":
case "warning", "warn":
prefix = "[WARN]"
case "info":
prefix = "[INFO]"

View file

@ -47,7 +47,7 @@ type Webview struct {
// ConsoleMessage represents a captured console log message.
type ConsoleMessage struct {
Type string `json:"type"` // log, warn, error, info, debug
Type string `json:"type"` // log, warning, error, info, debug
Text string `json:"text"` // Message text
Timestamp time.Time `json:"timestamp"` // When the message was logged
URL string `json:"url"` // Source URL
@ -415,41 +415,11 @@ func (wv *Webview) navigateHistory(delta int, scope string) error {
ctx, cancel := context.WithTimeout(wv.ctx, wv.timeout)
defer cancel()
result, err := wv.client.Call(ctx, "Page.getNavigationHistory", nil)
if err != nil {
return coreerr.E(scope, "failed to get navigation history", err)
}
currentIndex, ok := result["currentIndex"].(float64)
if !ok {
return coreerr.E(scope, "invalid navigation history index", nil)
}
entries, ok := result["entries"].([]any)
if !ok {
return coreerr.E(scope, "invalid navigation history entries", nil)
}
targetIndex := int(currentIndex) + delta
if targetIndex < 0 || targetIndex >= len(entries) {
return coreerr.E(scope, "no history entry available", nil)
}
entry, ok := entries[targetIndex].(map[string]any)
if !ok {
return coreerr.E(scope, "invalid navigation history entry", nil)
}
entryID, ok := entry["id"].(float64)
if !ok {
return coreerr.E(scope, "invalid navigation history entry ID", nil)
}
_, err = wv.client.Call(ctx, "Page.navigateToHistoryEntry", map[string]any{
"entryId": int(entryID),
_, err := wv.client.Call(ctx, "Page.goBackOrForward", map[string]any{
"delta": delta,
})
if err != nil {
return coreerr.E(scope, "failed to navigate to history entry", err)
return coreerr.E(scope, "failed to navigate history", err)
}
return wv.waitForLoad(ctx)

View file

@ -579,6 +579,15 @@ func TestConsoleWatcherFilter_Good(t *testing.T) {
if cw.matchesFilter(msg) {
t.Error("Expected 'test error' NOT to match pattern 'hello'")
}
cw.ClearFilters()
cw.AddFilter(ConsoleFilter{Type: "warning"})
if !cw.matchesFilter(ConsoleMessage{Type: "warning", Text: "deprecated"}) {
t.Error("Expected warning message to match warning filter")
}
if cw.matchesFilter(ConsoleMessage{Type: "warn", Text: "deprecated"}) {
t.Error("Expected warn message not to match warning filter")
}
}
// TestConsoleWatcherCounts_Good verifies console watcher counting methods.