@@ -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.
0 commit comments