Skip to content

Commit 159ab15

Browse files
fix: batch B — browser.go bugs (BUG-05,07,10,28,29,16,26)
BUG-05: Fill element-not-found now returns Go error via JSON result parsing. BUG-07: Replace %q with jsonEscaped() helper in Click and Fill to use proper JavaScript string escaping (JSON.Marshal rules). BUG-10: NavigateBack/Forward now validate both bounds (negative index and past-end). BUG-28: Screenshots typo — already absent from .go files (pre-complying). BUG-29: DomCUAClick boxModel changed to []float64 with len check to prevent panic on short content quads. BUG-16: CUAType now dispatches keyDown+char+keyUp per character via executeCdp directly (no detach+attach between events). BUG-26: DOMSnapshot fallback prepends "/* fallback: plain text */" marker.
1 parent ded778f commit 159ab15

2 files changed

Lines changed: 75 additions & 11 deletions

File tree

internal/client/browser.go

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (c *Client) NavigateBack(tabID string) error {
109109
if err := json.Unmarshal(raw, &history); err != nil {
110110
return err
111111
}
112-
if history.CurrentIndex <= 0 {
112+
if history.CurrentIndex <= 0 || history.CurrentIndex >= len(history.Entries) {
113113
return fmt.Errorf("no previous page in history")
114114
}
115115
entryID := history.Entries[history.CurrentIndex-1].ID
@@ -139,7 +139,7 @@ func (c *Client) NavigateForward(tabID string) error {
139139
if err := json.Unmarshal(raw, &history); err != nil {
140140
return err
141141
}
142-
if history.CurrentIndex >= len(history.Entries)-1 {
142+
if history.CurrentIndex < 0 || history.CurrentIndex >= len(history.Entries)-1 {
143143
return fmt.Errorf("no next page in history")
144144
}
145145
entryID := history.Entries[history.CurrentIndex+1].ID
@@ -256,6 +256,14 @@ func isDebuggerError(err error) bool {
256256
return err != nil && strings.Contains(err.Error(), "not attached")
257257
}
258258

259+
// jsonEscaped returns a JSON-escaped representation of s suitable for embedding
260+
// in JavaScript string literals (e.g., inside Runtime.evaluate expressions).
261+
// Unlike Go's %q, json.Marshal uses the same escaping rules as JavaScript.
262+
func jsonEscaped(s string) string {
263+
b, _ := json.Marshal(s)
264+
return string(b)
265+
}
266+
259267
// --- Playwright API (via CDP) ---
260268

261269
// DOMSnapshot returns an accessibility tree snapshot of the page.
@@ -281,9 +289,9 @@ func (c *Client) DOMSnapshot(tabID string) (string, error) {
281289
} `json:"result"`
282290
}
283291
if json.Unmarshal(raw2, &evalResult) == nil {
284-
return evalResult.Result.Value, nil
292+
return "/* fallback: plain text */\n" + evalResult.Result.Value, nil
285293
}
286-
return string(raw2), nil
294+
return "/* fallback: plain text */\n" + string(raw2), nil
287295
}
288296
return string(raw), nil
289297
}
@@ -338,13 +346,33 @@ func (c *Client) CUAType(tabID, text string) error {
338346
if err != nil {
339347
return err
340348
}
349+
// Ensure debugger is attached once before the key sequence
350+
_ = c.detachTab(id)
351+
if err := c.attachTab(id); err != nil {
352+
return fmt.Errorf("attach failed for tab %d: %w", id, err)
353+
}
341354
for _, ch := range text {
342-
_, err = c.cdpWithAttach(id, "Input.dispatchKeyEvent", map[string]interface{}{
355+
// keyDown
356+
_, err = c.executeCdp(id, "Input.dispatchKeyEvent", map[string]interface{}{
357+
"type": "keyDown", "key": string(ch), "text": string(ch),
358+
})
359+
if err != nil {
360+
return err
361+
}
362+
// char
363+
_, err = c.executeCdp(id, "Input.dispatchKeyEvent", map[string]interface{}{
343364
"type": "char", "text": string(ch),
344365
})
345366
if err != nil {
346367
return err
347368
}
369+
// keyUp
370+
_, err = c.executeCdp(id, "Input.dispatchKeyEvent", map[string]interface{}{
371+
"type": "keyUp", "key": string(ch), "text": string(ch),
372+
})
373+
if err != nil {
374+
return err
375+
}
348376
}
349377
return nil
350378
}
@@ -413,12 +441,15 @@ func (c *Client) DomCUAClick(tabID, nodeID string) error {
413441
}
414442
var box struct {
415443
Model struct {
416-
Content [8]float64 `json:"content"`
444+
Content []float64 `json:"content"`
417445
} `json:"model"`
418446
}
419447
if err := json.Unmarshal(raw, &box); err != nil {
420448
return fmt.Errorf("parse box model: %w", err)
421449
}
450+
if len(box.Model.Content) < 5 {
451+
return fmt.Errorf("box model has insufficient content quads: got %d elements", len(box.Model.Content))
452+
}
422453
// Content quad: [x1,y1, x2,y2, x3,y3, x4,y4] — center is average
423454
cx := (box.Model.Content[0] + box.Model.Content[2] + box.Model.Content[4] + box.Model.Content[6]) / 4
424455
cy := (box.Model.Content[1] + box.Model.Content[3] + box.Model.Content[5] + box.Model.Content[7]) / 4
@@ -480,7 +511,7 @@ func (c *Client) Click(tabID, selector string) error {
480511
if err != nil {
481512
return err
482513
}
483-
js := fmt.Sprintf(`document.querySelector(%q).click()`, selector)
514+
js := fmt.Sprintf(`document.querySelector(%s).click()`, jsonEscaped(selector))
484515
_, err = c.cdpWithAttach(id, "Runtime.evaluate", map[string]interface{}{
485516
"expression": js,
486517
})
@@ -493,11 +524,34 @@ func (c *Client) Fill(tabID, selector, value string) error {
493524
if err != nil {
494525
return err
495526
}
496-
js := fmt.Sprintf(`(() => { const el = document.querySelector(%q); if(el) { el.focus(); el.value = %q; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); } })()`, selector, value)
497-
_, err = c.cdpWithAttach(id, "Runtime.evaluate", map[string]interface{}{
527+
s := jsonEscaped(selector)
528+
v := jsonEscaped(value)
529+
js := fmt.Sprintf(`(function(){var el=document.querySelector(%s);if(!el)return JSON.stringify({error:'element not found: '+%s});el.focus();el.value=%s;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return JSON.stringify({ok:true})})()`, s, s, v)
530+
raw, err := c.cdpWithAttach(id, "Runtime.evaluate", map[string]interface{}{
498531
"expression": js,
499532
})
500-
return err
533+
if err != nil {
534+
return err
535+
}
536+
var evalResult struct {
537+
Result struct {
538+
Value string `json:"value"`
539+
} `json:"result"`
540+
}
541+
if err := json.Unmarshal(raw, &evalResult); err != nil {
542+
return err
543+
}
544+
var fillResult struct {
545+
Ok bool `json:"ok"`
546+
Error string `json:"error"`
547+
}
548+
if err := json.Unmarshal([]byte(evalResult.Result.Value), &fillResult); err != nil {
549+
return err
550+
}
551+
if fillResult.Error != "" {
552+
return fmt.Errorf("fill: %s", fillResult.Error)
553+
}
554+
return nil
501555
}
502556

503557
// Evaluate runs JavaScript in the page context and returns the result.

internal/client/browser_rpc_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,17 @@ func TestClickEscapesSelector(t *testing.T) {
502502
}
503503

504504
func TestFillEscapesValue(t *testing.T) {
505-
c, rec, cleanup := withRecordingServer(t, func(req protocol.Request) interface{} { return map[string]bool{"ok": true} })
505+
c, rec, cleanup := withRecordingServer(t, func(req protocol.Request) interface{} {
506+
if req.Method == "executeCdp" {
507+
return map[string]interface{}{
508+
"result": map[string]interface{}{
509+
"value": `{"ok":true}`,
510+
"type": "string",
511+
},
512+
}
513+
}
514+
return map[string]bool{"ok": true}
515+
})
506516
defer cleanup()
507517

508518
value := `pwd"with\quote`

0 commit comments

Comments
 (0)