Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6adaeaeec2 | |||
|
|
e6a7ecf4f5 | ||
|
|
b993fc18f3 | ||
| e35de439e1 | |||
|
|
950b02fd97 | ||
|
|
96f18346d9 |
10 changed files with 657 additions and 79 deletions
28
actions.go
28
actions.go
|
|
@ -3,9 +3,9 @@ package webview
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
coreerr "dappco.re/go/core/log"
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -84,7 +84,7 @@ type ScrollAction struct {
|
||||||
|
|
||||||
// Execute performs the scroll action.
|
// Execute performs the scroll action.
|
||||||
func (a ScrollAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a ScrollAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf("window.scrollTo(%d, %d)", a.X, a.Y)
|
script := core.Sprintf("window.scrollTo(%d, %d)", a.X, a.Y)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +96,7 @@ type ScrollIntoViewAction struct {
|
||||||
|
|
||||||
// Execute scrolls the element into view.
|
// Execute scrolls the element into view.
|
||||||
func (a ScrollIntoViewAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a ScrollIntoViewAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf("document.querySelector(%q)?.scrollIntoView({behavior: 'smooth', block: 'center'})", a.Selector)
|
script := core.Sprintf("document.querySelector(%q)?.scrollIntoView({behavior: 'smooth', block: 'center'})", a.Selector)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +108,7 @@ type FocusAction struct {
|
||||||
|
|
||||||
// Execute focuses the element.
|
// Execute focuses the element.
|
||||||
func (a FocusAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a FocusAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf("document.querySelector(%q)?.focus()", a.Selector)
|
script := core.Sprintf("document.querySelector(%q)?.focus()", a.Selector)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +120,7 @@ type BlurAction struct {
|
||||||
|
|
||||||
// Execute removes focus from the element.
|
// Execute removes focus from the element.
|
||||||
func (a BlurAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a BlurAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf("document.querySelector(%q)?.blur()", a.Selector)
|
script := core.Sprintf("document.querySelector(%q)?.blur()", a.Selector)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +132,7 @@ type ClearAction struct {
|
||||||
|
|
||||||
// Execute clears the input value.
|
// Execute clears the input value.
|
||||||
func (a ClearAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a ClearAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
const el = document.querySelector(%q);
|
const el = document.querySelector(%q);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.value = '';
|
el.value = '';
|
||||||
|
|
@ -152,7 +152,7 @@ type SelectAction struct {
|
||||||
|
|
||||||
// Execute selects the option.
|
// Execute selects the option.
|
||||||
func (a SelectAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a SelectAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
const el = document.querySelector(%q);
|
const el = document.querySelector(%q);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.value = %q;
|
el.value = %q;
|
||||||
|
|
@ -171,7 +171,7 @@ type CheckAction struct {
|
||||||
|
|
||||||
// Execute checks/unchecks the checkbox.
|
// Execute checks/unchecks the checkbox.
|
||||||
func (a CheckAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a CheckAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
const el = document.querySelector(%q);
|
const el = document.querySelector(%q);
|
||||||
if (el && el.checked !== %t) {
|
if (el && el.checked !== %t) {
|
||||||
el.click();
|
el.click();
|
||||||
|
|
@ -222,7 +222,7 @@ func (a DoubleClickAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
|
|
||||||
if elem.BoundingBox == nil {
|
if elem.BoundingBox == nil {
|
||||||
// Fallback to JavaScript
|
// Fallback to JavaScript
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
const el = document.querySelector(%q);
|
const el = document.querySelector(%q);
|
||||||
if (el) {
|
if (el) {
|
||||||
const event = new MouseEvent('dblclick', {bubbles: true, cancelable: true, view: window});
|
const event = new MouseEvent('dblclick', {bubbles: true, cancelable: true, view: window});
|
||||||
|
|
@ -269,7 +269,7 @@ func (a RightClickAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
|
|
||||||
if elem.BoundingBox == nil {
|
if elem.BoundingBox == nil {
|
||||||
// Fallback to JavaScript
|
// Fallback to JavaScript
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
const el = document.querySelector(%q);
|
const el = document.querySelector(%q);
|
||||||
if (el) {
|
if (el) {
|
||||||
const event = new MouseEvent('contextmenu', {bubbles: true, cancelable: true, view: window});
|
const event = new MouseEvent('contextmenu', {bubbles: true, cancelable: true, view: window});
|
||||||
|
|
@ -377,7 +377,7 @@ type SetAttributeAction struct {
|
||||||
|
|
||||||
// Execute sets the attribute.
|
// Execute sets the attribute.
|
||||||
func (a SetAttributeAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a SetAttributeAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf("document.querySelector(%q)?.setAttribute(%q, %q)", a.Selector, a.Attribute, a.Value)
|
script := core.Sprintf("document.querySelector(%q)?.setAttribute(%q, %q)", a.Selector, a.Attribute, a.Value)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -390,7 +390,7 @@ type RemoveAttributeAction struct {
|
||||||
|
|
||||||
// Execute removes the attribute.
|
// Execute removes the attribute.
|
||||||
func (a RemoveAttributeAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a RemoveAttributeAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf("document.querySelector(%q)?.removeAttribute(%q)", a.Selector, a.Attribute)
|
script := core.Sprintf("document.querySelector(%q)?.removeAttribute(%q)", a.Selector, a.Attribute)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +403,7 @@ type SetValueAction struct {
|
||||||
|
|
||||||
// Execute sets the value.
|
// Execute sets the value.
|
||||||
func (a SetValueAction) Execute(ctx context.Context, wv *Webview) error {
|
func (a SetValueAction) Execute(ctx context.Context, wv *Webview) error {
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
const el = document.querySelector(%q);
|
const el = document.querySelector(%q);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.value = %q;
|
el.value = %q;
|
||||||
|
|
@ -462,7 +462,7 @@ func (s *ActionSequence) WaitForSelector(selector string) *ActionSequence {
|
||||||
func (s *ActionSequence) Execute(ctx context.Context, wv *Webview) error {
|
func (s *ActionSequence) Execute(ctx context.Context, wv *Webview) error {
|
||||||
for i, action := range s.actions {
|
for i, action := range s.actions {
|
||||||
if err := action.Execute(ctx, wv); err != nil {
|
if err := action.Execute(ctx, wv); err != nil {
|
||||||
return coreerr.E("ActionSequence.Execute", fmt.Sprintf("action %d failed", i), err)
|
return coreerr.E("ActionSequence.Execute", core.Sprintf("action %d failed", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
36
angular.go
36
angular.go
|
|
@ -3,11 +3,9 @@ package webview
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
coreerr "dappco.re/go/core/log"
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -209,7 +207,7 @@ func (ah *AngularHelper) NavigateByRouter(path string) error {
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const roots = window.getAllAngularRootElements ? window.getAllAngularRootElements() : [];
|
const roots = window.getAllAngularRootElements ? window.getAllAngularRootElements() : [];
|
||||||
if (roots.length === 0) {
|
if (roots.length === 0) {
|
||||||
|
|
@ -326,7 +324,7 @@ func (ah *AngularHelper) GetComponentProperty(selector, propertyName string) (an
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const selector = %s;
|
const selector = %s;
|
||||||
const propertyName = %s;
|
const propertyName = %s;
|
||||||
|
|
@ -350,7 +348,7 @@ func (ah *AngularHelper) SetComponentProperty(selector, propertyName string, val
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const selector = %s;
|
const selector = %s;
|
||||||
const propertyName = %s;
|
const propertyName = %s;
|
||||||
|
|
@ -383,7 +381,7 @@ func (ah *AngularHelper) CallComponentMethod(selector, methodName string, args .
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var argsStr strings.Builder
|
argsStr := core.NewBuilder()
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
argsStr.WriteString(", ")
|
argsStr.WriteString(", ")
|
||||||
|
|
@ -391,7 +389,7 @@ func (ah *AngularHelper) CallComponentMethod(selector, methodName string, args .
|
||||||
argsStr.WriteString(formatJSValue(arg))
|
argsStr.WriteString(formatJSValue(arg))
|
||||||
}
|
}
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const selector = %s;
|
const selector = %s;
|
||||||
const methodName = %s;
|
const methodName = %s;
|
||||||
|
|
@ -454,7 +452,7 @@ func (ah *AngularHelper) GetService(serviceName string) (any, error) {
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const roots = window.getAllAngularRootElements ? window.getAllAngularRootElements() : [];
|
const roots = window.getAllAngularRootElements ? window.getAllAngularRootElements() : [];
|
||||||
for (const root of roots) {
|
for (const root of roots) {
|
||||||
|
|
@ -481,7 +479,7 @@ func (ah *AngularHelper) WaitForComponent(selector string) error {
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const element = document.querySelector(%q);
|
const element = document.querySelector(%q);
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
|
|
@ -523,7 +521,7 @@ func (ah *AngularHelper) DispatchEvent(selector, eventName string, detail any) e
|
||||||
detailStr = formatJSValue(detail)
|
detailStr = formatJSValue(detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const selector = %s;
|
const selector = %s;
|
||||||
const eventName = %s;
|
const eventName = %s;
|
||||||
|
|
@ -546,7 +544,7 @@ func (ah *AngularHelper) GetNgModel(selector string) (any, error) {
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const element = document.querySelector(%q);
|
const element = document.querySelector(%q);
|
||||||
if (!element) return null;
|
if (!element) return null;
|
||||||
|
|
@ -573,7 +571,7 @@ func (ah *AngularHelper) SetNgModel(selector string, value any) error {
|
||||||
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
ctx, cancel := context.WithTimeout(ah.wv.ctx, ah.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
script := fmt.Sprintf(`
|
script := core.Sprintf(`
|
||||||
(function() {
|
(function() {
|
||||||
const selector = %s;
|
const selector = %s;
|
||||||
const element = document.querySelector(selector);
|
const element = document.querySelector(selector);
|
||||||
|
|
@ -616,14 +614,14 @@ func getString(m map[string]any, key string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatJSValue(v any) string {
|
func formatJSValue(v any) string {
|
||||||
data, err := json.Marshal(v)
|
r := core.JSONMarshal(v)
|
||||||
if err == nil {
|
if r.OK {
|
||||||
return string(data)
|
return string(r.Value.([]byte))
|
||||||
}
|
}
|
||||||
|
|
||||||
fallback, fallbackErr := json.Marshal(fmt.Sprint(v))
|
r = core.JSONMarshal(core.Sprint(v))
|
||||||
if fallbackErr == nil {
|
if r.OK {
|
||||||
return string(fallback)
|
return string(r.Value.([]byte))
|
||||||
}
|
}
|
||||||
|
|
||||||
return "null"
|
return "null"
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ package webview
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ func (s *fakeCDPServer) addTarget(id string) *fakeCDPTarget {
|
||||||
func (s *fakeCDPServer) newTarget() *fakeCDPTarget {
|
func (s *fakeCDPServer) newTarget() *fakeCDPTarget {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.nextTarget++
|
s.nextTarget++
|
||||||
id := fmt.Sprintf("target-%d", s.nextTarget+1)
|
id := core.Sprintf("target-%d", s.nextTarget+1)
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
return s.addTarget(id)
|
return s.addTarget(id)
|
||||||
|
|
@ -100,8 +100,8 @@ func (s *fakeCDPServer) handle(w http.ResponseWriter, r *http.Request) {
|
||||||
s.writeJSON(w, map[string]string{
|
s.writeJSON(w, map[string]string{
|
||||||
"Browser": "Chrome/123.0",
|
"Browser": "Chrome/123.0",
|
||||||
})
|
})
|
||||||
case strings.HasPrefix(r.URL.Path, "/devtools/page/"):
|
case core.HasPrefix(r.URL.Path, "/devtools/page/"):
|
||||||
s.handleWebSocket(w, r, strings.TrimPrefix(r.URL.Path, "/devtools/page/"))
|
s.handleWebSocket(w, r, core.TrimPrefix(r.URL.Path, "/devtools/page/"))
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +209,7 @@ func (tgt *fakeCDPTarget) readLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg cdpMessage
|
var msg cdpMessage
|
||||||
if err := json.Unmarshal(data, &msg); err != nil {
|
if r := core.JSONUnmarshal(data, &msg); !r.OK {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,7 +451,7 @@ func TestNewCDPClient_Bad_RejectsCrossHostWebSocket(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("NewCDPClient succeeded with a cross-host WebSocket URL")
|
t.Fatal("NewCDPClient succeeded with a cross-host WebSocket URL")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "invalid target WebSocket URL") {
|
if !core.Contains(err.Error(), "invalid target WebSocket URL") {
|
||||||
t.Fatalf("NewCDPClient error = %v, want cross-host WebSocket validation failure", err)
|
t.Fatalf("NewCDPClient error = %v, want cross-host WebSocket validation failure", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -543,13 +543,13 @@ func TestAngularHelperSetNgModel_Good_EscapesSelectorAndValue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
expression, _ := target.waitForMessage(t).Params["expression"].(string)
|
expression, _ := target.waitForMessage(t).Params["expression"].(string)
|
||||||
if !strings.Contains(expression, "const selector = "+formatJSValue(selector)+";") {
|
if !core.Contains(expression, "const selector = "+formatJSValue(selector)+";") {
|
||||||
t.Fatalf("expression did not contain safely quoted selector: %s", expression)
|
t.Fatalf("expression did not contain safely quoted selector: %s", expression)
|
||||||
}
|
}
|
||||||
if !strings.Contains(expression, "element.value = "+formatJSValue(value)+";") {
|
if !core.Contains(expression, "element.value = "+formatJSValue(value)+";") {
|
||||||
t.Fatalf("expression did not contain safely quoted value: %s", expression)
|
t.Fatalf("expression did not contain safely quoted value: %s", expression)
|
||||||
}
|
}
|
||||||
if strings.Contains(expression, "throw new Error('Element not found: "+selector+"')") {
|
if core.Contains(expression, "throw new Error('Element not found: "+selector+"')") {
|
||||||
t.Fatalf("expression still embedded selector directly in error text: %s", expression)
|
t.Fatalf("expression still embedded selector directly in error text: %s", expression)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
cdp.go
35
cdp.go
|
|
@ -3,8 +3,6 @@ package webview
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"iter"
|
"iter"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -17,9 +15,10 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
core "dappco.re/go/core"
|
||||||
|
|
||||||
coreerr "dappco.re/go/core/log"
|
coreerr "dappco.re/go/core/log"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
const debugEndpointTimeout = 10 * time.Second
|
const debugEndpointTimeout = 10 * time.Second
|
||||||
|
|
@ -31,7 +30,7 @@ var (
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
errCDPClientClosed = errors.New("cdp client closed")
|
errCDPClientClosed = core.NewError("cdp client closed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// CDPClient handles communication with Chrome DevTools Protocol via WebSocket.
|
// CDPClient handles communication with Chrome DevTools Protocol via WebSocket.
|
||||||
|
|
@ -225,7 +224,7 @@ func (c *CDPClient) readLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var netErr net.Error
|
var netErr net.Error
|
||||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
if core.As(err, &netErr) && netErr.Timeout() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,7 +234,7 @@ func (c *CDPClient) readLoop() {
|
||||||
|
|
||||||
// Try to parse as response
|
// Try to parse as response
|
||||||
var resp cdpResponse
|
var resp cdpResponse
|
||||||
if err := json.Unmarshal(data, &resp); err == nil && resp.ID > 0 {
|
if r := core.JSONUnmarshal(data, &resp); r.OK && resp.ID > 0 {
|
||||||
c.pendMu.Lock()
|
c.pendMu.Lock()
|
||||||
if ch, ok := c.pending[resp.ID]; ok {
|
if ch, ok := c.pending[resp.ID]; ok {
|
||||||
respCopy := resp
|
respCopy := resp
|
||||||
|
|
@ -250,7 +249,7 @@ func (c *CDPClient) readLoop() {
|
||||||
|
|
||||||
// Try to parse as event
|
// Try to parse as event
|
||||||
var event cdpEvent
|
var event cdpEvent
|
||||||
if err := json.Unmarshal(data, &event); err == nil && event.Method != "" {
|
if r := core.JSONUnmarshal(data, &event); r.OK && event.Method != "" {
|
||||||
c.dispatchEvent(event.Method, event.Params)
|
c.dispatchEvent(event.Method, event.Params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -392,8 +391,8 @@ func GetVersion(debugURL string) (map[string]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var version map[string]string
|
var version map[string]string
|
||||||
if err := json.Unmarshal(body, &version); err != nil {
|
if r := core.JSONUnmarshal(body, &version); !r.OK {
|
||||||
return nil, coreerr.E("GetVersion", "failed to parse version", err)
|
return nil, coreerr.E("GetVersion", "failed to parse version", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return version, nil
|
return version, nil
|
||||||
|
|
@ -447,7 +446,7 @@ func parseDebugURL(raw string) (*url.URL, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalDebugURL(debugURL *url.URL) string {
|
func canonicalDebugURL(debugURL *url.URL) string {
|
||||||
return strings.TrimSuffix(debugURL.String(), "/")
|
return core.TrimSuffix(debugURL.String(), "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func doDebugRequest(ctx context.Context, debugBase *url.URL, endpoint, rawQuery string) ([]byte, error) {
|
func doDebugRequest(ctx context.Context, debugBase *url.URL, endpoint, rawQuery string) ([]byte, error) {
|
||||||
|
|
@ -486,8 +485,8 @@ func listTargetsAt(ctx context.Context, debugBase *url.URL) ([]TargetInfo, error
|
||||||
}
|
}
|
||||||
|
|
||||||
var targets []TargetInfo
|
var targets []TargetInfo
|
||||||
if err := json.Unmarshal(body, &targets); err != nil {
|
if r := core.JSONUnmarshal(body, &targets); !r.OK {
|
||||||
return nil, err
|
return nil, coreerr.E("CDPClient.listTargetsAt", "failed to parse targets", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return targets, nil
|
return targets, nil
|
||||||
|
|
@ -505,8 +504,8 @@ func createTargetAt(ctx context.Context, debugBase *url.URL, pageURL string) (*T
|
||||||
}
|
}
|
||||||
|
|
||||||
var target TargetInfo
|
var target TargetInfo
|
||||||
if err := json.Unmarshal(body, &target); err != nil {
|
if r := core.JSONUnmarshal(body, &target); !r.OK {
|
||||||
return nil, err
|
return nil, coreerr.E("CDPClient.createTargetAt", "failed to parse target", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &target, nil
|
return &target, nil
|
||||||
|
|
@ -551,7 +550,7 @@ func targetIDFromWebSocketURL(raw string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
targetID := path.Base(strings.TrimSuffix(wsURL.Path, "/"))
|
targetID := path.Base(core.TrimSuffix(wsURL.Path, "/"))
|
||||||
if targetID == "." || targetID == "/" || targetID == "" {
|
if targetID == "." || targetID == "/" || targetID == "" {
|
||||||
return "", coreerr.E("CDPClient.targetIDFromWebSocketURL", "missing target ID in WebSocket URL", nil)
|
return "", coreerr.E("CDPClient.targetIDFromWebSocketURL", "missing target ID in WebSocket URL", nil)
|
||||||
}
|
}
|
||||||
|
|
@ -595,11 +594,11 @@ func isTerminalReadError(err error) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if errors.Is(err, net.ErrClosed) || errors.Is(err, websocket.ErrCloseSent) {
|
if core.Is(err, net.ErrClosed) || core.Is(err, websocket.ErrCloseSent) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
var closeErr *websocket.CloseError
|
var closeErr *websocket.CloseError
|
||||||
return errors.As(err, &closeErr)
|
return core.As(err, &closeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneMapAny(src map[string]any) map[string]any {
|
func cloneMapAny(src map[string]any) map[string]any {
|
||||||
|
|
|
||||||
18
console.go
18
console.go
|
|
@ -3,13 +3,13 @@ package webview
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"iter"
|
"iter"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConsoleWatcher provides advanced console message watching capabilities.
|
// ConsoleWatcher provides advanced console message watching capabilities.
|
||||||
|
|
@ -41,7 +41,7 @@ type consoleHandlerRegistration struct {
|
||||||
func NewConsoleWatcher(wv *Webview) *ConsoleWatcher {
|
func NewConsoleWatcher(wv *Webview) *ConsoleWatcher {
|
||||||
cw := &ConsoleWatcher{
|
cw := &ConsoleWatcher{
|
||||||
wv: wv,
|
wv: wv,
|
||||||
messages: make([]ConsoleMessage, 0, 100),
|
messages: make([]ConsoleMessage, 0, 1000),
|
||||||
filters: make([]ConsoleFilter, 0),
|
filters: make([]ConsoleFilter, 0),
|
||||||
limit: 1000,
|
limit: 1000,
|
||||||
handlers: make([]consoleHandlerRegistration, 0),
|
handlers: make([]consoleHandlerRegistration, 0),
|
||||||
|
|
@ -272,14 +272,14 @@ func (cw *ConsoleWatcher) handleConsoleEvent(params map[string]any) {
|
||||||
|
|
||||||
// Extract args
|
// Extract args
|
||||||
args, _ := params["args"].([]any)
|
args, _ := params["args"].([]any)
|
||||||
var text strings.Builder
|
text := core.NewBuilder()
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
if argMap, ok := arg.(map[string]any); ok {
|
if argMap, ok := arg.(map[string]any); ok {
|
||||||
if val, ok := argMap["value"]; ok {
|
if val, ok := argMap["value"]; ok {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
text.WriteString(" ")
|
text.WriteString(" ")
|
||||||
}
|
}
|
||||||
text.WriteString(fmt.Sprint(val))
|
text.WriteString(core.Sprint(val))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -525,7 +525,7 @@ func (ew *ExceptionWatcher) handleException(params map[string]any) {
|
||||||
url, _ := exceptionDetails["url"].(string)
|
url, _ := exceptionDetails["url"].(string)
|
||||||
|
|
||||||
// Extract stack trace
|
// Extract stack trace
|
||||||
var stackTrace strings.Builder
|
stackTrace := core.NewBuilder()
|
||||||
if st, ok := exceptionDetails["stackTrace"].(map[string]any); ok {
|
if st, ok := exceptionDetails["stackTrace"].(map[string]any); ok {
|
||||||
if frames, ok := st["callFrames"].([]any); ok {
|
if frames, ok := st["callFrames"].([]any); ok {
|
||||||
for _, f := range frames {
|
for _, f := range frames {
|
||||||
|
|
@ -534,7 +534,7 @@ func (ew *ExceptionWatcher) handleException(params map[string]any) {
|
||||||
frameURL, _ := frame["url"].(string)
|
frameURL, _ := frame["url"].(string)
|
||||||
frameLine, _ := frame["lineNumber"].(float64)
|
frameLine, _ := frame["lineNumber"].(float64)
|
||||||
frameCol, _ := frame["columnNumber"].(float64)
|
frameCol, _ := frame["columnNumber"].(float64)
|
||||||
stackTrace.WriteString(fmt.Sprintf(" at %s (%s:%d:%d)\n", funcName, frameURL, int(frameLine), int(frameCol)))
|
stackTrace.WriteString(core.Sprintf(" at %s (%s:%d:%d)\n", funcName, frameURL, int(frameLine), int(frameCol)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -569,7 +569,7 @@ func (ew *ExceptionWatcher) handleException(params map[string]any) {
|
||||||
|
|
||||||
// FormatConsoleOutput formats console messages for display.
|
// FormatConsoleOutput formats console messages for display.
|
||||||
func FormatConsoleOutput(messages []ConsoleMessage) string {
|
func FormatConsoleOutput(messages []ConsoleMessage) string {
|
||||||
var output strings.Builder
|
output := core.NewBuilder()
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
prefix := ""
|
prefix := ""
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
|
|
@ -585,7 +585,7 @@ func FormatConsoleOutput(messages []ConsoleMessage) string {
|
||||||
prefix = "[LOG]"
|
prefix = "[LOG]"
|
||||||
}
|
}
|
||||||
timestamp := msg.Timestamp.Format("15:04:05.000")
|
timestamp := msg.Timestamp.Format("15:04:05.000")
|
||||||
output.WriteString(fmt.Sprintf("%s %s %s\n", timestamp, prefix, msg.Text))
|
output.WriteString(core.Sprintf("%s %s %s\n", timestamp, prefix, msg.Text))
|
||||||
}
|
}
|
||||||
return output.String()
|
return output.String()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -5,3 +5,5 @@ go 1.26.0
|
||||||
require github.com/gorilla/websocket v1.5.3
|
require github.com/gorilla/websocket v1.5.3
|
||||||
|
|
||||||
require dappco.re/go/core/log v0.1.0
|
require dappco.re/go/core/log v0.1.0
|
||||||
|
|
||||||
|
require dappco.re/go/core v0.8.0-alpha.1
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -1,3 +1,5 @@
|
||||||
|
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||||
|
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
|
|
||||||
505
specs/RFC.md
Normal file
505
specs/RFC.md
Normal file
|
|
@ -0,0 +1,505 @@
|
||||||
|
# webview, **Import:** `dappco.re/go/core/webview`, **Files:** 5
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
### Action
|
||||||
|
Declaration: `type Action interface`
|
||||||
|
|
||||||
|
Browser action contract used by `ActionSequence`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Runs the action against the supplied `Webview` and caller-owned context.
|
||||||
|
|
||||||
|
### ActionSequence
|
||||||
|
Declaration: `type ActionSequence struct`
|
||||||
|
|
||||||
|
Represents an ordered list of `Action` values. All fields are unexported.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Add(action Action) *ActionSequence`: Appends `action` to the sequence and returns the same sequence for chaining.
|
||||||
|
- `Click(selector string) *ActionSequence`: Appends `ClickAction{Selector: selector}`.
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Executes actions in insertion order. The first failing action stops execution and is wrapped as `ActionSequence.Execute` with the zero-based action index.
|
||||||
|
- `Navigate(url string) *ActionSequence`: Appends `NavigateAction{URL: url}`.
|
||||||
|
- `Type(selector, text string) *ActionSequence`: Appends `TypeAction{Selector: selector, Text: text}`.
|
||||||
|
- `Wait(d time.Duration) *ActionSequence`: Appends `WaitAction{Duration: d}`.
|
||||||
|
- `WaitForSelector(selector string) *ActionSequence`: Appends `WaitForSelectorAction{Selector: selector}`.
|
||||||
|
|
||||||
|
### AngularHelper
|
||||||
|
Declaration: `type AngularHelper struct`
|
||||||
|
|
||||||
|
Angular-specific helper bound to a `Webview`. All fields are unexported. The helper stores the target `Webview` and a per-operation timeout, which defaults to 30 seconds in `NewAngularHelper`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `CallComponentMethod(selector, methodName string, args ...any) (any, error)`: Looks up the Angular component instance for `selector`, verifies that `methodName` is callable, invokes it with JSON-marshalled arguments, ticks `ApplicationRef` when available, and returns the evaluated result.
|
||||||
|
- `DispatchEvent(selector, eventName string, detail any) error`: Dispatches a bubbling `CustomEvent` on the matched element. `detail` is marshalled into the page script, or `null` when omitted.
|
||||||
|
- `GetComponentProperty(selector, propertyName string) (any, error)`: Returns `componentInstance[propertyName]` for the Angular component attached to the matched element.
|
||||||
|
- `GetNgModel(selector string) (any, error)`: Returns `element.value` for `input`, `select`, and `textarea` elements, otherwise `element.value || element.textContent`. Missing elements produce `nil`.
|
||||||
|
- `GetRouterState() (*AngularRouterState, error)`: Walks Angular root elements, resolves the first available Router, and returns its URL, fragment, root params, and root query params. Only string values are copied into the returned maps.
|
||||||
|
- `GetService(serviceName string) (any, error)`: Resolves a service from the first Angular root injector and returns `JSON.parse(JSON.stringify(service))`, so only JSON-serialisable data survives.
|
||||||
|
- `NavigateByRouter(path string) error`: Resolves Angular Router from a root injector, calls `navigateByUrl(path)`, and then waits for Zone.js stability.
|
||||||
|
- `SetComponentProperty(selector, propertyName string, value any) error`: Sets `componentInstance[propertyName]` and ticks `ApplicationRef` when available.
|
||||||
|
- `SetNgModel(selector string, value any) error`: Assigns `element.value`, dispatches bubbling `input` and `change` events, and then ticks `ApplicationRef` on the first Angular root that exposes it.
|
||||||
|
- `SetTimeout(d time.Duration)`: Replaces the helper's default timeout for later Angular operations.
|
||||||
|
- `TriggerChangeDetection() error`: Tries to tick `ApplicationRef` on the first Angular root. The method only reports script-evaluation failures; a `false` result from the script is ignored.
|
||||||
|
- `WaitForAngular() error`: Verifies that the page looks like an Angular application, then waits for Zone.js stability. It first tries an async Zone-based script and falls back to polling every 50 ms until the helper timeout expires.
|
||||||
|
- `WaitForComponent(selector string) error`: Polls every 100 ms until `selector` resolves to an element with an Angular `componentInstance`, or until the helper timeout expires.
|
||||||
|
|
||||||
|
### AngularRouterState
|
||||||
|
Declaration: `type AngularRouterState struct`
|
||||||
|
|
||||||
|
Represents the Angular Router state returned by `AngularHelper.GetRouterState`.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `URL string`: Current router URL.
|
||||||
|
- `Fragment string`: Current URL fragment when Angular reports one.
|
||||||
|
- `Params map[string]string`: Root route parameters copied from the router state.
|
||||||
|
- `QueryParams map[string]string`: Root query parameters copied from the router state.
|
||||||
|
|
||||||
|
### BlurAction
|
||||||
|
Declaration: `type BlurAction struct`
|
||||||
|
|
||||||
|
Removes focus from an element selected by CSS.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector passed to `document.querySelector`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Runs `document.querySelector(Selector)?.blur()` through `wv.evaluate`.
|
||||||
|
|
||||||
|
### BoundingBox
|
||||||
|
Declaration: `type BoundingBox struct`
|
||||||
|
|
||||||
|
Represents element coordinates derived from `DOM.getBoxModel`.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `X float64`: Left edge of the content box.
|
||||||
|
- `Y float64`: Top edge of the content box.
|
||||||
|
- `Width float64`: Width computed from the first and second X coordinates in the CDP content quad.
|
||||||
|
- `Height float64`: Height computed from the first and third Y coordinates in the CDP content quad.
|
||||||
|
|
||||||
|
### CDPClient
|
||||||
|
Declaration: `type CDPClient struct`
|
||||||
|
|
||||||
|
Low-level Chrome DevTools Protocol client backed by a single WebSocket connection. All fields are unexported.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Call(ctx context.Context, method string, params map[string]any) (map[string]any, error)`: Clones `params`, assigns a monotonically increasing message ID, writes the request, and waits for the matching CDP response, `ctx.Done()`, or client shutdown.
|
||||||
|
- `Close() error`: Cancels the client context, fails pending calls, closes the WebSocket, waits for the read loop to exit, and returns a wrapped close error only when the socket close itself fails with a non-terminal error.
|
||||||
|
- `CloseTab() error`: Extracts the current target ID from the WebSocket URL, calls `Target.closeTarget`, checks `success` when the field is present, and then closes the client.
|
||||||
|
- `DebugURL() string`: Returns the canonical debug HTTP URL stored on the client.
|
||||||
|
- `NewTab(url string) (*CDPClient, error)`: Creates a target via `/json/new`, validates the returned WebSocket debugger URL, dials it, and returns a new `CDPClient`.
|
||||||
|
- `OnEvent(method string, handler func(map[string]any))`: Registers a handler for a CDP event method name. Event dispatch clones the handler list and event params and invokes each handler in its own goroutine.
|
||||||
|
- `Send(method string, params map[string]any) error`: Clones `params` and writes a fire-and-forget CDP message without waiting for a response.
|
||||||
|
- `WebSocketURL() string`: Returns the target WebSocket URL currently in use.
|
||||||
|
|
||||||
|
### CheckAction
|
||||||
|
Declaration: `type CheckAction struct`
|
||||||
|
|
||||||
|
Synchronises a checkbox state by clicking when the current `checked` state differs from the requested value.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the checkbox element.
|
||||||
|
- `Checked bool`: Desired checked state.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Evaluates a script that clicks the element only when `el.checked != Checked`.
|
||||||
|
|
||||||
|
### ClearAction
|
||||||
|
Declaration: `type ClearAction struct`
|
||||||
|
|
||||||
|
Clears the value of an input-like element.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Sets `el.value = ""` and dispatches bubbling `input` and `change` events when the element exists.
|
||||||
|
|
||||||
|
### ClickAction
|
||||||
|
Declaration: `type ClickAction struct`
|
||||||
|
|
||||||
|
Action wrapper for the `Webview` click implementation.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Delegates to `wv.click(ctx, Selector)`.
|
||||||
|
|
||||||
|
### ConsoleFilter
|
||||||
|
Declaration: `type ConsoleFilter struct`
|
||||||
|
|
||||||
|
Filter used by `ConsoleWatcher`.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Type string`: Exact message-type match. The watcher compares it directly against `ConsoleMessage.Type`.
|
||||||
|
- `Pattern string`: Case-sensitive substring match against `ConsoleMessage.Text`.
|
||||||
|
|
||||||
|
### ConsoleHandler
|
||||||
|
Declaration: `type ConsoleHandler func(msg ConsoleMessage)`
|
||||||
|
|
||||||
|
Callback signature used by `ConsoleWatcher.AddHandler`.
|
||||||
|
|
||||||
|
### ConsoleMessage
|
||||||
|
Declaration: `type ConsoleMessage struct`
|
||||||
|
|
||||||
|
Represents one console message captured from `Runtime.consoleAPICalled`.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Type string`: Console message type reported by CDP.
|
||||||
|
- `Text string`: Message text built by joining `args[*].value` with spaces.
|
||||||
|
- `Timestamp time.Time`: Local capture time assigned in Go with `time.Now()`.
|
||||||
|
- `URL string`: URL taken from the first stack-frame entry when present.
|
||||||
|
- `Line int`: Line number taken from the first stack-frame entry when present.
|
||||||
|
- `Column int`: Column number taken from the first stack-frame entry when present.
|
||||||
|
|
||||||
|
### ConsoleWatcher
|
||||||
|
Declaration: `type ConsoleWatcher struct`
|
||||||
|
|
||||||
|
Higher-level console log collector layered on top of `Webview`. All fields are unexported. New watchers start with an empty message buffer and a default limit of 1000 messages.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `AddFilter(filter ConsoleFilter)`: Appends a filter. When at least one filter exists, `FilteredMessages` and `FilteredMessagesAll` use OR semantics across filters.
|
||||||
|
- `AddHandler(handler ConsoleHandler)`: Registers a callback for future messages captured by this watcher.
|
||||||
|
- `Clear()`: Removes all stored messages.
|
||||||
|
- `ClearFilters()`: Removes every registered filter.
|
||||||
|
- `Count() int`: Returns the current number of stored messages.
|
||||||
|
- `ErrorCount() int`: Counts stored messages where `Type == "error"`.
|
||||||
|
- `Errors() []ConsoleMessage`: Returns a collected slice of error messages.
|
||||||
|
- `ErrorsAll() iter.Seq[ConsoleMessage]`: Returns an iterator that yields stored messages whose type is exactly `"error"`.
|
||||||
|
- `FilteredMessages() []ConsoleMessage`: Returns a collected slice of messages that match the current filter set, or all messages when no filters exist.
|
||||||
|
- `FilteredMessagesAll() iter.Seq[ConsoleMessage]`: Returns an iterator over the current filtered view.
|
||||||
|
- `HasErrors() bool`: Reports whether any stored message has `Type == "error"`.
|
||||||
|
- `Messages() []ConsoleMessage`: Returns a collected slice of all stored messages.
|
||||||
|
- `MessagesAll() iter.Seq[ConsoleMessage]`: Returns an iterator over the stored message buffer.
|
||||||
|
- `SetLimit(limit int)`: Replaces the retention limit used for future appends. Existing messages are only trimmed on later writes.
|
||||||
|
- `WaitForError(ctx context.Context) (*ConsoleMessage, error)`: Equivalent to `WaitForMessage(ctx, ConsoleFilter{Type: "error"})`.
|
||||||
|
- `WaitForMessage(ctx context.Context, filter ConsoleFilter) (*ConsoleMessage, error)`: Returns the first already-stored message that matches `filter`, otherwise registers a temporary handler and waits for the next matching message or `ctx.Done()`.
|
||||||
|
- `Warnings() []ConsoleMessage`: Returns a collected slice of messages whose type is exactly `"warning"`.
|
||||||
|
- `WarningsAll() iter.Seq[ConsoleMessage]`: Returns an iterator over stored messages whose type is exactly `"warning"`.
|
||||||
|
|
||||||
|
### DoubleClickAction
|
||||||
|
Declaration: `type DoubleClickAction struct`
|
||||||
|
|
||||||
|
Double-click action for an element selected by CSS.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Uses a JavaScript `dblclick` fallback when the element has no bounding box; otherwise sends two `mousePressed` and `mouseReleased` pairs with increasing `clickCount` values at the element centre.
|
||||||
|
|
||||||
|
### ElementInfo
|
||||||
|
Declaration: `type ElementInfo struct`
|
||||||
|
|
||||||
|
Represents DOM metadata returned by `Webview.querySelector` and `Webview.querySelectorAll`.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `NodeID int`: CDP node ID used for later DOM operations.
|
||||||
|
- `TagName string`: `nodeName` returned by `DOM.describeNode`.
|
||||||
|
- `Attributes map[string]string`: Attributes parsed from the alternating key/value array returned by CDP.
|
||||||
|
- `InnerHTML string`: Declared field for inner HTML content. The current implementation does not populate it.
|
||||||
|
- `InnerText string`: Declared field for inner text content. The current implementation does not populate it.
|
||||||
|
- `BoundingBox *BoundingBox`: Element box derived from `DOM.getBoxModel`. It is `nil` when box lookup fails.
|
||||||
|
|
||||||
|
### ExceptionInfo
|
||||||
|
Declaration: `type ExceptionInfo struct`
|
||||||
|
|
||||||
|
Represents one `Runtime.exceptionThrown` event captured by `ExceptionWatcher`.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Text string`: Exception text, overridden by `exception.description` when CDP provides one.
|
||||||
|
- `LineNumber int`: Line number reported by CDP.
|
||||||
|
- `ColumnNumber int`: Column number reported by CDP.
|
||||||
|
- `URL string`: Source URL reported by CDP.
|
||||||
|
- `StackTrace string`: Stack trace formatted in Go as one `at function (url:line:column)` line per call frame.
|
||||||
|
- `Timestamp time.Time`: Local capture time assigned in Go with `time.Now()`.
|
||||||
|
|
||||||
|
### ExceptionWatcher
|
||||||
|
Declaration: `type ExceptionWatcher struct`
|
||||||
|
|
||||||
|
Collector for JavaScript exceptions emitted by the bound `Webview`. All fields are unexported.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `AddHandler(handler func(ExceptionInfo))`: Registers a callback for future exception events.
|
||||||
|
- `Clear()`: Removes all stored exceptions.
|
||||||
|
- `Count() int`: Returns the current number of stored exceptions.
|
||||||
|
- `Exceptions() []ExceptionInfo`: Returns a collected slice of all stored exceptions.
|
||||||
|
- `ExceptionsAll() iter.Seq[ExceptionInfo]`: Returns an iterator over the stored exception buffer.
|
||||||
|
- `HasExceptions() bool`: Reports whether any exceptions have been captured.
|
||||||
|
- `WaitForException(ctx context.Context) (*ExceptionInfo, error)`: Returns the most recently stored exception immediately when one already exists; otherwise registers a temporary handler and waits for the next exception or `ctx.Done()`.
|
||||||
|
|
||||||
|
### FocusAction
|
||||||
|
Declaration: `type FocusAction struct`
|
||||||
|
|
||||||
|
Moves focus to an element selected by CSS.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector passed to `document.querySelector`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Runs `document.querySelector(Selector)?.focus()` through `wv.evaluate`.
|
||||||
|
|
||||||
|
### HoverAction
|
||||||
|
Declaration: `type HoverAction struct`
|
||||||
|
|
||||||
|
Moves the mouse pointer over an element.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Looks up the element, requires a non-nil bounding box, computes the box centre, and sends one `Input.dispatchMouseEvent` call with `type: "mouseMoved"`.
|
||||||
|
|
||||||
|
### NavigateAction
|
||||||
|
Declaration: `type NavigateAction struct`
|
||||||
|
|
||||||
|
Action wrapper for page navigation.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `URL string`: URL passed to `Page.navigate`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Calls `Page.navigate` and then waits until `document.readyState == "complete"`.
|
||||||
|
|
||||||
|
### Option
|
||||||
|
Declaration: `type Option func(*Webview) error`
|
||||||
|
|
||||||
|
Functional configuration hook applied by `New`. The exported option constructors are `WithDebugURL`, `WithTimeout`, and `WithConsoleLimit`.
|
||||||
|
|
||||||
|
### PressKeyAction
|
||||||
|
Declaration: `type PressKeyAction struct`
|
||||||
|
|
||||||
|
Key-press action implemented through `Input.dispatchKeyEvent`.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Key string`: Key name or character to send.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Uses explicit CDP key metadata for common keys such as `Enter`, `Tab`, arrow keys, `Delete`, and paging keys. For any other string it sends a `keyDown` with `text: Key` followed by a bare `keyUp`.
|
||||||
|
|
||||||
|
### RemoveAttributeAction
|
||||||
|
Declaration: `type RemoveAttributeAction struct`
|
||||||
|
|
||||||
|
Removes a DOM attribute.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
- `Attribute string`: Attribute name to remove.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Runs `document.querySelector(Selector)?.removeAttribute(Attribute)` through `wv.evaluate`.
|
||||||
|
|
||||||
|
### RightClickAction
|
||||||
|
Declaration: `type RightClickAction struct`
|
||||||
|
|
||||||
|
Context-click action for an element selected by CSS.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Uses a JavaScript `contextmenu` fallback when the element has no bounding box; otherwise sends `mousePressed` and `mouseReleased` with `button: "right"` at the element centre.
|
||||||
|
|
||||||
|
### ScrollAction
|
||||||
|
Declaration: `type ScrollAction struct`
|
||||||
|
|
||||||
|
Window scroll action.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `X int`: Horizontal scroll target passed to `window.scrollTo`.
|
||||||
|
- `Y int`: Vertical scroll target passed to `window.scrollTo`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Evaluates `window.scrollTo(X, Y)`.
|
||||||
|
|
||||||
|
### ScrollIntoViewAction
|
||||||
|
Declaration: `type ScrollIntoViewAction struct`
|
||||||
|
|
||||||
|
Scrolls a selected element into view.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Evaluates `document.querySelector(Selector)?.scrollIntoView({behavior: "smooth", block: "center"})`.
|
||||||
|
|
||||||
|
### SelectAction
|
||||||
|
Declaration: `type SelectAction struct`
|
||||||
|
|
||||||
|
Select-element value setter.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
- `Value string`: Option value assigned to `el.value`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Sets `el.value` and dispatches a bubbling `change` event when the element exists.
|
||||||
|
|
||||||
|
### SetAttributeAction
|
||||||
|
Declaration: `type SetAttributeAction struct`
|
||||||
|
|
||||||
|
Sets a DOM attribute on the selected element.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
- `Attribute string`: Attribute name to set.
|
||||||
|
- `Value string`: Attribute value passed to `setAttribute`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Runs `document.querySelector(Selector)?.setAttribute(Attribute, Value)` through `wv.evaluate`.
|
||||||
|
|
||||||
|
### SetValueAction
|
||||||
|
Declaration: `type SetValueAction struct`
|
||||||
|
|
||||||
|
Directly sets an input-like value.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
- `Value string`: Value assigned to `el.value`.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Sets `el.value` and dispatches bubbling `input` and `change` events when the element exists.
|
||||||
|
|
||||||
|
### TargetInfo
|
||||||
|
Declaration: `type TargetInfo struct`
|
||||||
|
|
||||||
|
Represents one target entry returned by Chrome's `/json` and `/json/new` endpoints.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `ID string`: Chrome target ID.
|
||||||
|
- `Type string`: Target type such as `page`.
|
||||||
|
- `Title string`: Target title reported by Chrome.
|
||||||
|
- `URL string`: Target URL reported by Chrome.
|
||||||
|
- `WebSocketDebuggerURL string`: WebSocket debugger URL for the target.
|
||||||
|
|
||||||
|
### TypeAction
|
||||||
|
Declaration: `type TypeAction struct`
|
||||||
|
|
||||||
|
Action wrapper for `Webview` text entry.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector for the target element.
|
||||||
|
- `Text string`: Text sent one rune at a time.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Delegates to `wv.typeText(ctx, Selector, Text)`.
|
||||||
|
|
||||||
|
### WaitAction
|
||||||
|
Declaration: `type WaitAction struct`
|
||||||
|
|
||||||
|
Time-based delay action.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Duration time.Duration`: Delay length.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Waits for `Duration` with `time.After`, but returns `ctx.Err()` immediately if the context is cancelled first.
|
||||||
|
|
||||||
|
### WaitForSelectorAction
|
||||||
|
Declaration: `type WaitForSelectorAction struct`
|
||||||
|
|
||||||
|
Action wrapper for selector polling.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `Selector string`: CSS selector to wait for.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `Execute(ctx context.Context, wv *Webview) error`: Delegates to `wv.waitForSelector(ctx, Selector)`.
|
||||||
|
|
||||||
|
### Webview
|
||||||
|
Declaration: `type Webview struct`
|
||||||
|
|
||||||
|
High-level browser automation client built on `CDPClient`. All fields are unexported. Instances created by `New` carry a default timeout of 30 seconds and a default console retention limit of 1000 messages.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `ClearConsole()`: Removes all console messages stored on the `Webview`.
|
||||||
|
- `Click(selector string) error`: Creates a timeout-scoped context and delegates to the internal click path. The internal click logic queries the element, uses a JavaScript `.click()` fallback when there is no bounding box, and otherwise sends left-button press and release events at the element centre.
|
||||||
|
- `Close() error`: Cancels the `Webview` context and closes the underlying `CDPClient` when one exists.
|
||||||
|
- `DragAndDrop(sourceSelector, targetSelector string) error`: Looks up both elements, requires bounding boxes for both, then sends `mousePressed` at the source centre, `mouseMoved` to the target centre, and `mouseReleased` at the target centre.
|
||||||
|
- `Evaluate(script string) (any, error)`: Evaluates JavaScript through `Runtime.evaluate` with `returnByValue: true` and `awaitPromise: true`. When CDP reports `exceptionDetails`, the method returns a wrapped JavaScript error.
|
||||||
|
- `GetConsole() []ConsoleMessage`: Returns a collected slice of stored console messages.
|
||||||
|
- `GetConsoleAll() iter.Seq[ConsoleMessage]`: Returns an iterator over the stored console message buffer.
|
||||||
|
- `GetHTML(selector string) (string, error)`: Returns `document.documentElement.outerHTML` when `selector` is empty; otherwise returns `document.querySelector(selector)?.outerHTML || ""`.
|
||||||
|
- `GetTitle() (string, error)`: Evaluates `document.title` and requires the result to be a string.
|
||||||
|
- `GetURL() (string, error)`: Evaluates `window.location.href` and requires the result to be a string.
|
||||||
|
- `GoBack() error`: Calls `Page.goBackOrForward` with `delta: -1`.
|
||||||
|
- `GoForward() error`: Calls `Page.goBackOrForward` with `delta: 1`.
|
||||||
|
- `Navigate(url string) error`: Calls `Page.navigate` and then polls `document.readyState` every 100 ms until it becomes `"complete"` or the timeout expires.
|
||||||
|
- `QuerySelector(selector string) (*ElementInfo, error)`: Fetches the document root, runs `DOM.querySelector`, errors when the selector does not resolve, and returns `ElementInfo` for the matched node.
|
||||||
|
- `QuerySelectorAll(selector string) ([]*ElementInfo, error)`: Runs `DOM.querySelectorAll` and returns one `ElementInfo` per node ID whose metadata lookup succeeds. Nodes whose metadata fetch fails are skipped.
|
||||||
|
- `QuerySelectorAllAll(selector string) iter.Seq[*ElementInfo]`: Returns an iterator that runs `QuerySelectorAll` under the `Webview` timeout and yields each element. Errors produce an empty iterator.
|
||||||
|
- `Reload() error`: Calls `Page.reload` and then waits for `document.readyState == "complete"`.
|
||||||
|
- `Screenshot() ([]byte, error)`: Calls `Page.captureScreenshot` with `format: "png"`, decodes the returned base64 payload, and returns the PNG bytes.
|
||||||
|
- `SetUserAgent(userAgent string) error`: Calls `Emulation.setUserAgentOverride`.
|
||||||
|
- `SetViewport(width, height int) error`: Calls `Emulation.setDeviceMetricsOverride` with the supplied size, `deviceScaleFactor: 1`, and `mobile: false`.
|
||||||
|
- `Type(selector, text string) error`: Creates a timeout-scoped context and delegates to the internal typing path. The internal logic focuses the element with JavaScript, then sends one `keyDown` with `text` and one `keyUp` per rune in `text`.
|
||||||
|
- `UploadFile(selector string, filePaths []string) error`: Resolves the target node with `QuerySelector` and passes its node ID to `DOM.setFileInputFiles`.
|
||||||
|
- `WaitForSelector(selector string) error`: Polls `!!document.querySelector(selector)` every 100 ms until it becomes true or the timeout expires.
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
### FormatConsoleOutput
|
||||||
|
`func FormatConsoleOutput(messages []ConsoleMessage) string`
|
||||||
|
|
||||||
|
Formats each message as `HH:MM:SS.mmm [PREFIX] text\n`. Prefixes are `[ERROR]` for `error`, `[WARN]` for `warning`, `[INFO]` for `info`, `[DEBUG]` for `debug`, and `[LOG]` for every other type.
|
||||||
|
|
||||||
|
### GetVersion
|
||||||
|
`func GetVersion(debugURL string) (map[string]string, error)`
|
||||||
|
|
||||||
|
Validates `debugURL`, requests `/json/version`, and decodes the response body into `map[string]string`.
|
||||||
|
|
||||||
|
### ListTargets
|
||||||
|
`func ListTargets(debugURL string) ([]TargetInfo, error)`
|
||||||
|
|
||||||
|
Validates `debugURL`, requests `/json`, and decodes the response body into a slice of `TargetInfo`.
|
||||||
|
|
||||||
|
### ListTargetsAll
|
||||||
|
`func ListTargetsAll(debugURL string) iter.Seq[TargetInfo]`
|
||||||
|
|
||||||
|
Iterator wrapper around `ListTargets`. When `ListTargets` fails, the iterator yields no values.
|
||||||
|
|
||||||
|
### New
|
||||||
|
`func New(opts ...Option) (*Webview, error)`
|
||||||
|
|
||||||
|
Creates a `Webview`, applies `opts` in order, requires an option that installs a `CDPClient`, enables the `Runtime`, `Page`, and `DOM` domains, and subscribes console capture. On any option or initialisation failure it cancels the context and closes the client when one was created.
|
||||||
|
|
||||||
|
### NewActionSequence
|
||||||
|
`func NewActionSequence() *ActionSequence`
|
||||||
|
|
||||||
|
Creates an empty `ActionSequence`.
|
||||||
|
|
||||||
|
### NewAngularHelper
|
||||||
|
`func NewAngularHelper(wv *Webview) *AngularHelper`
|
||||||
|
|
||||||
|
Creates an `AngularHelper` bound to `wv` with a 30-second default timeout.
|
||||||
|
|
||||||
|
### NewCDPClient
|
||||||
|
`func NewCDPClient(debugURL string) (*CDPClient, error)`
|
||||||
|
|
||||||
|
Validates that `debugURL` is an `http` or `https` DevTools root URL with no credentials, query, fragment, or non-root path. The function requests `/json`, picks the first `page` target with a debugger WebSocket URL, creates a new target via `/json/new` when none exists, validates that the WebSocket host matches the debug host, dials the socket, and starts the client's read loop.
|
||||||
|
|
||||||
|
### NewConsoleWatcher
|
||||||
|
`func NewConsoleWatcher(wv *Webview) *ConsoleWatcher`
|
||||||
|
|
||||||
|
Creates a `ConsoleWatcher`, initialises an empty message buffer with a 1000-message limit, and subscribes it to `Runtime.consoleAPICalled` events on `wv.client`.
|
||||||
|
|
||||||
|
### NewExceptionWatcher
|
||||||
|
`func NewExceptionWatcher(wv *Webview) *ExceptionWatcher`
|
||||||
|
|
||||||
|
Creates an `ExceptionWatcher`, initialises an empty exception buffer, and subscribes it to `Runtime.exceptionThrown` events on `wv.client`.
|
||||||
|
|
||||||
|
### WithConsoleLimit
|
||||||
|
`func WithConsoleLimit(limit int) Option`
|
||||||
|
|
||||||
|
Returns an `Option` that replaces `Webview.consoleLimit`. The default used by `New` is 1000.
|
||||||
|
|
||||||
|
### WithDebugURL
|
||||||
|
`func WithDebugURL(url string) Option`
|
||||||
|
|
||||||
|
Returns an `Option` that dials a `CDPClient` immediately and stores it on the `Webview`.
|
||||||
|
|
||||||
|
### WithTimeout
|
||||||
|
`func WithTimeout(d time.Duration) Option`
|
||||||
|
|
||||||
|
Returns an `Option` that replaces the default per-operation timeout used by `Webview` methods.
|
||||||
70
webview.go
70
webview.go
|
|
@ -25,13 +25,12 @@ package webview
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
|
||||||
"iter"
|
"iter"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
coreerr "dappco.re/go/core/log"
|
coreerr "dappco.re/go/core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -115,7 +114,7 @@ func New(opts ...Option) (*Webview, error) {
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
timeout: 30 * time.Second,
|
timeout: 30 * time.Second,
|
||||||
consoleLogs: make([]ConsoleMessage, 0, 100),
|
consoleLogs: make([]ConsoleMessage, 0, 1000),
|
||||||
consoleLimit: 1000,
|
consoleLimit: 1000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -337,7 +336,7 @@ func (wv *Webview) GetHTML(selector string) (string, error) {
|
||||||
if selector == "" {
|
if selector == "" {
|
||||||
script = "document.documentElement.outerHTML"
|
script = "document.documentElement.outerHTML"
|
||||||
} else {
|
} else {
|
||||||
script = fmt.Sprintf("document.querySelector(%q)?.outerHTML || ''", selector)
|
script = core.Sprintf("document.querySelector(%q)?.outerHTML || ''", selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := wv.evaluate(ctx, script)
|
result, err := wv.evaluate(ctx, script)
|
||||||
|
|
@ -463,14 +462,14 @@ func (wv *Webview) handleConsoleEvent(params map[string]any) {
|
||||||
|
|
||||||
// Extract args
|
// Extract args
|
||||||
args, _ := params["args"].([]any)
|
args, _ := params["args"].([]any)
|
||||||
var text strings.Builder
|
text := core.NewBuilder()
|
||||||
for i, arg := range args {
|
for i, arg := range args {
|
||||||
if argMap, ok := arg.(map[string]any); ok {
|
if argMap, ok := arg.(map[string]any); ok {
|
||||||
if val, ok := argMap["value"]; ok {
|
if val, ok := argMap["value"]; ok {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
text.WriteString(" ")
|
text.WriteString(" ")
|
||||||
}
|
}
|
||||||
text.WriteString(fmt.Sprint(val))
|
text.WriteString(core.Sprint(val))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -526,7 +525,7 @@ func (wv *Webview) waitForSelector(ctx context.Context, selector string) error {
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
script := fmt.Sprintf("!!document.querySelector(%q)", selector)
|
script := core.Sprintf("!!document.querySelector(%q)", selector)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
@ -680,6 +679,8 @@ func (wv *Webview) getElementInfo(ctx context.Context, nodeID int) (*ElementInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
innerHTML, innerText := wv.getElementContent(ctx, nodeID)
|
||||||
|
|
||||||
// Get bounding box
|
// Get bounding box
|
||||||
var box *BoundingBox
|
var box *BoundingBox
|
||||||
if boxResult, err := wv.client.Call(ctx, "DOM.getBoxModel", map[string]any{
|
if boxResult, err := wv.client.Call(ctx, "DOM.getBoxModel", map[string]any{
|
||||||
|
|
@ -705,10 +706,61 @@ func (wv *Webview) getElementInfo(ctx context.Context, nodeID int) (*ElementInfo
|
||||||
NodeID: nodeID,
|
NodeID: nodeID,
|
||||||
TagName: tagName,
|
TagName: tagName,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
|
InnerHTML: innerHTML,
|
||||||
|
InnerText: innerText,
|
||||||
BoundingBox: box,
|
BoundingBox: box,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getElementContent retrieves the element's inner HTML and inner text.
|
||||||
|
func (wv *Webview) getElementContent(ctx context.Context, nodeID int) (string, string) {
|
||||||
|
resolveResult, err := wv.client.Call(ctx, "DOM.resolveNode", map[string]any{
|
||||||
|
"nodeId": nodeID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
object, ok := resolveResult["object"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
objectID, ok := object["objectId"].(string)
|
||||||
|
if !ok || objectID == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
callResult, err := wv.client.Call(ctx, "Runtime.callFunctionOn", map[string]any{
|
||||||
|
"objectId": objectID,
|
||||||
|
"functionDeclaration": "function() { return { innerHTML: this.innerHTML || '', innerText: this.innerText || '' }; }",
|
||||||
|
"returnByValue": true,
|
||||||
|
"awaitPromise": true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseElementContent(callResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseElementContent extracts inner HTML and inner text from a CDP response.
|
||||||
|
func parseElementContent(result map[string]any) (string, string) {
|
||||||
|
resultObj, ok := result["result"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := resultObj["value"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
innerHTML, _ := value["innerHTML"].(string)
|
||||||
|
innerText, _ := value["innerText"].(string)
|
||||||
|
return innerHTML, innerText
|
||||||
|
}
|
||||||
|
|
||||||
// click performs a click on an element.
|
// click performs a click on an element.
|
||||||
func (wv *Webview) click(ctx context.Context, selector string) error {
|
func (wv *Webview) click(ctx context.Context, selector string) error {
|
||||||
// Find element and get its center coordinates
|
// Find element and get its center coordinates
|
||||||
|
|
@ -719,7 +771,7 @@ func (wv *Webview) click(ctx context.Context, selector string) error {
|
||||||
|
|
||||||
if elem.BoundingBox == nil {
|
if elem.BoundingBox == nil {
|
||||||
// Fallback to JavaScript click
|
// Fallback to JavaScript click
|
||||||
script := fmt.Sprintf("document.querySelector(%q)?.click()", selector)
|
script := core.Sprintf("document.querySelector(%q)?.click()", selector)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -748,7 +800,7 @@ func (wv *Webview) click(ctx context.Context, selector string) error {
|
||||||
// typeText types text into an element.
|
// typeText types text into an element.
|
||||||
func (wv *Webview) typeText(ctx context.Context, selector, text string) error {
|
func (wv *Webview) typeText(ctx context.Context, selector, text string) error {
|
||||||
// Focus the element first
|
// Focus the element first
|
||||||
script := fmt.Sprintf("document.querySelector(%q)?.focus()", selector)
|
script := core.Sprintf("document.querySelector(%q)?.focus()", selector)
|
||||||
_, err := wv.evaluate(ctx, script)
|
_, err := wv.evaluate(ctx, script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return coreerr.E("Webview.typeText", "failed to focus element", err)
|
return coreerr.E("Webview.typeText", "failed to focus element", err)
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,26 @@ func TestGetString_Good(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseElementContent_Good verifies inner content extraction from CDP output.
|
||||||
|
func TestParseElementContent_Good(t *testing.T) {
|
||||||
|
result := map[string]any{
|
||||||
|
"result": map[string]any{
|
||||||
|
"value": map[string]any{
|
||||||
|
"innerHTML": "<span>Hello</span>",
|
||||||
|
"innerText": "Hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
innerHTML, innerText := parseElementContent(result)
|
||||||
|
if innerHTML != "<span>Hello</span>" {
|
||||||
|
t.Fatalf("parseElementContent innerHTML = %q, want %q", innerHTML, "<span>Hello</span>")
|
||||||
|
}
|
||||||
|
if innerText != "Hello" {
|
||||||
|
t.Fatalf("parseElementContent innerText = %q, want %q", innerText, "Hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestWaitAction_Good_ContextCancelled verifies WaitAction respects context cancellation.
|
// TestWaitAction_Good_ContextCancelled verifies WaitAction respects context cancellation.
|
||||||
func TestWaitAction_Good_ContextCancelled(t *testing.T) {
|
func TestWaitAction_Good_ContextCancelled(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue