Add missing webview tests
This commit is contained in:
parent
40d2e584aa
commit
f38ceb3bd6
5 changed files with 1853 additions and 0 deletions
426
actions_test.go
Normal file
426
actions_test.go
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
package webview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newActionHarness(t *testing.T, onMessage func(*fakeCDPTarget, cdpMessage)) (*Webview, *fakeCDPTarget) {
|
||||
t.Helper()
|
||||
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
target.onMessage = onMessage
|
||||
|
||||
client := newConnectedCDPClient(t, target)
|
||||
wv := &Webview{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
timeout: time.Second,
|
||||
consoleLogs: make([]ConsoleMessage, 0),
|
||||
consoleLimit: 10,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
return wv, target
|
||||
}
|
||||
|
||||
func TestActions_ActionSequence_Good(t *testing.T) {
|
||||
var methods []string
|
||||
wv, _ := newActionHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
methods = append(methods, msg.Method)
|
||||
switch msg.Method {
|
||||
case "Runtime.evaluate":
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
if expr == "document.readyState" {
|
||||
target.replyValue(msg.ID, "complete")
|
||||
return
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
case "Page.navigate":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
seq := NewActionSequence().
|
||||
Scroll(10, 20).
|
||||
Focus("#input").
|
||||
Navigate("https://example.com")
|
||||
|
||||
if err := seq.Execute(context.Background(), wv); err != nil {
|
||||
t.Fatalf("ActionSequence.Execute returned error: %v", err)
|
||||
}
|
||||
if len(methods) != 4 {
|
||||
t.Fatalf("ActionSequence.Execute methods = %v, want 4 calls", methods)
|
||||
}
|
||||
if methods[0] != "Runtime.evaluate" || methods[1] != "Runtime.evaluate" || methods[2] != "Page.navigate" || methods[3] != "Runtime.evaluate" {
|
||||
t.Fatalf("ActionSequence.Execute call order = %v", methods)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActions_EvaluateActions_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action Action
|
||||
wantSub string
|
||||
}{
|
||||
{name: "scroll", action: ScrollAction{X: 10, Y: 20}, wantSub: "window.scrollTo(10, 20)"},
|
||||
{name: "scroll into view", action: ScrollIntoViewAction{Selector: "#target"}, wantSub: `scrollIntoView({behavior: 'smooth', block: 'center'})`},
|
||||
{name: "focus", action: FocusAction{Selector: "#input"}, wantSub: `?.focus()`},
|
||||
{name: "blur", action: BlurAction{Selector: "#input"}, wantSub: `?.blur()`},
|
||||
{name: "clear", action: ClearAction{Selector: "#input"}, wantSub: `el.value = '';`},
|
||||
{name: "select", action: SelectAction{Selector: "#dropdown", Value: "option1"}, wantSub: `el.value = "option1";`},
|
||||
{name: "check", action: CheckAction{Selector: "#checkbox", Checked: true}, wantSub: `el && el.checked !== true`},
|
||||
{name: "set attribute", action: SetAttributeAction{Selector: "#element", Attribute: "data-value", Value: "test"}, wantSub: `setAttribute("data-value", "test")`},
|
||||
{name: "remove attribute", action: RemoveAttributeAction{Selector: "#element", Attribute: "disabled"}, wantSub: `removeAttribute("disabled")`},
|
||||
{name: "set value", action: SetValueAction{Selector: "#input", Value: "new value"}, wantSub: `el.value = "new value";`},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wv, _ := newActionHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
if !strings.Contains(expr, tc.wantSub) {
|
||||
t.Fatalf("expression %q does not contain %q", expr, tc.wantSub)
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := tc.action.Execute(context.Background(), wv); err != nil {
|
||||
t.Fatalf("%T.Execute returned error: %v", tc.action, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActions_DomActions_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action Action
|
||||
handler func(*testing.T, *fakeCDPTarget, cdpMessage)
|
||||
check func(*testing.T, []cdpMessage)
|
||||
}{
|
||||
{
|
||||
name: "click",
|
||||
action: ClickAction{Selector: "#button"},
|
||||
handler: func(t *testing.T, target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(10)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "BUTTON", "attributes": []any{"id", "button"}}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-1"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "<span>ok</span>", "innerText": "ok"}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": []any{float64(10), float64(20), float64(30), float64(20), float64(30), float64(40), float64(10), float64(40)}}})
|
||||
case "Input.dispatchMouseEvent":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, msgs []cdpMessage) {
|
||||
if len(msgs) != 8 {
|
||||
t.Fatalf("click made %d CDP calls, want 8", len(msgs))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hover",
|
||||
action: HoverAction{Selector: "#menu"},
|
||||
handler: func(t *testing.T, target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(11)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "DIV"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-2"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": []any{float64(0), float64(0), float64(20), float64(0), float64(20), float64(20), float64(0), float64(20)}}})
|
||||
case "Input.dispatchMouseEvent":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, msgs []cdpMessage) {
|
||||
if len(msgs) != 7 {
|
||||
t.Fatalf("hover made %d CDP calls, want 7", len(msgs))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "double click",
|
||||
action: DoubleClickAction{Selector: "#editable"},
|
||||
handler: func(t *testing.T, target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(12)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "DIV"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-3"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": []any{float64(0), float64(0), float64(10), float64(0), float64(10), float64(10), float64(0), float64(10)}}})
|
||||
case "Input.dispatchMouseEvent":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, msgs []cdpMessage) {
|
||||
if len(msgs) != 10 {
|
||||
t.Fatalf("double click made %d CDP calls, want 10", len(msgs))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "right click",
|
||||
action: RightClickAction{Selector: "#context"},
|
||||
handler: func(t *testing.T, target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(13)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "DIV"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-4"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": []any{float64(0), float64(0), float64(10), float64(0), float64(10), float64(10), float64(0), float64(10)}}})
|
||||
case "Input.dispatchMouseEvent":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, msgs []cdpMessage) {
|
||||
if len(msgs) != 8 {
|
||||
t.Fatalf("right click made %d CDP calls, want 8", len(msgs))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "press key",
|
||||
action: PressKeyAction{Key: "Enter"},
|
||||
handler: func(t *testing.T, target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Input.dispatchKeyEvent" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
},
|
||||
check: func(t *testing.T, msgs []cdpMessage) {
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("press key made %d CDP calls, want 2", len(msgs))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upload file",
|
||||
action: &uploadFileAction{selector: "#file", files: []string{"/tmp/a.txt"}},
|
||||
handler: func(t *testing.T, target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(22)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "INPUT"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-5"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": []any{float64(0), float64(0), float64(1), float64(0), float64(1), float64(1), float64(0), float64(1)}}})
|
||||
case "DOM.setFileInputFiles":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, msgs []cdpMessage) {
|
||||
if len(msgs) != 7 {
|
||||
t.Fatalf("upload file made %d CDP calls, want 7", len(msgs))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "drag and drop",
|
||||
action: &dragDropAction{source: "#source", target: "#target"},
|
||||
handler: func(t *testing.T, target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
sel, _ := msg.Params["selector"].(string)
|
||||
switch sel {
|
||||
case "#source":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(31)})
|
||||
case "#target":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(32)})
|
||||
default:
|
||||
t.Fatalf("unexpected selector %q", sel)
|
||||
}
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "DIV"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-6"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
nodeID := int(msg.Params["nodeId"].(float64))
|
||||
box := []any{float64(nodeID), float64(nodeID), float64(nodeID + 10), float64(nodeID), float64(nodeID + 10), float64(nodeID + 10), float64(nodeID), float64(nodeID + 10)}
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": box}})
|
||||
case "Input.dispatchMouseEvent":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
},
|
||||
check: func(t *testing.T, msgs []cdpMessage) {
|
||||
if len(msgs) != 15 {
|
||||
t.Fatalf("drag and drop made %d CDP calls, want 15", len(msgs))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var msgs []cdpMessage
|
||||
wv, _ := newActionHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
msgs = append(msgs, msg)
|
||||
tc.handler(t, target, msg)
|
||||
})
|
||||
|
||||
err := tc.action.Execute(context.Background(), wv)
|
||||
if err != nil {
|
||||
t.Fatalf("%T.Execute returned error: %v", tc.action, err)
|
||||
}
|
||||
tc.check(t, msgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActions_WaitForSelectorAction_Good(t *testing.T) {
|
||||
var calls int
|
||||
wv, _ := newActionHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
calls++
|
||||
if calls == 1 {
|
||||
target.replyValue(msg.ID, false)
|
||||
return
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := (WaitForSelectorAction{Selector: "#ready"}).Execute(context.Background(), wv); err != nil {
|
||||
t.Fatalf("WaitForSelectorAction.Execute returned error: %v", err)
|
||||
}
|
||||
if calls < 2 {
|
||||
t.Fatalf("WaitForSelectorAction made %d evaluate calls, want at least 2", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActions_HoverAction_Bad_MissingBoundingBox(t *testing.T) {
|
||||
wv, _ := newActionHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(10)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "DIV"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-1"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
if err := (HoverAction{Selector: "#menu"}).Execute(context.Background(), wv); err == nil {
|
||||
t.Fatal("HoverAction succeeded without a bounding box")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActions_ClickAction_Ugly_FallsBackToJS(t *testing.T) {
|
||||
var expressions []string
|
||||
wv, _ := newActionHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(10)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "BUTTON"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-1"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
case "Runtime.evaluate":
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
expressions = append(expressions, expr)
|
||||
target.replyValue(msg.ID, true)
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
if err := (ClickAction{Selector: "#button"}).Execute(context.Background(), wv); err != nil {
|
||||
t.Fatalf("ClickAction returned error: %v", err)
|
||||
}
|
||||
if len(expressions) != 1 || !strings.Contains(expressions[0], `document.querySelector("#button")?.click()`) {
|
||||
t.Fatalf("ClickAction fallback expression = %v", expressions)
|
||||
}
|
||||
}
|
||||
|
||||
type uploadFileAction struct {
|
||||
selector string
|
||||
files []string
|
||||
}
|
||||
|
||||
func (a *uploadFileAction) Execute(ctx context.Context, wv *Webview) error {
|
||||
return wv.UploadFile(a.selector, a.files)
|
||||
}
|
||||
|
||||
type dragDropAction struct {
|
||||
source string
|
||||
target string
|
||||
}
|
||||
|
||||
func (a *dragDropAction) Execute(ctx context.Context, wv *Webview) error {
|
||||
return wv.DragAndDrop(a.source, a.target)
|
||||
}
|
||||
318
angular_test.go
Normal file
318
angular_test.go
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
package webview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newAngularTestHarness(t *testing.T, onMessage func(*fakeCDPTarget, cdpMessage)) (*AngularHelper, *fakeCDPTarget, *CDPClient) {
|
||||
t.Helper()
|
||||
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
target.onMessage = onMessage
|
||||
|
||||
client := newConnectedCDPClient(t, target)
|
||||
wv := &Webview{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
timeout: time.Second,
|
||||
consoleLogs: make([]ConsoleMessage, 0),
|
||||
consoleLimit: 10,
|
||||
}
|
||||
return NewAngularHelper(wv), target, client
|
||||
}
|
||||
|
||||
func TestAngular_SetTimeout_Good(t *testing.T) {
|
||||
ah := NewAngularHelper(&Webview{})
|
||||
ah.SetTimeout(5 * time.Second)
|
||||
if ah.timeout != 5*time.Second {
|
||||
t.Fatalf("SetTimeout = %v, want 5s", ah.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_WaitForAngular_Bad_NotAngular(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.replyValue(msg.ID, false)
|
||||
})
|
||||
|
||||
if err := ah.WaitForAngular(); err == nil {
|
||||
t.Fatal("WaitForAngular succeeded for a non-Angular page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_WaitForAngular_Good(t *testing.T) {
|
||||
var evaluateCount int
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
evaluateCount++
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
if strings.Contains(expr, "getAllAngularRootElements") || strings.Contains(expr, "[ng-version]") {
|
||||
target.replyValue(msg.ID, true)
|
||||
return
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := ah.WaitForAngular(); err != nil {
|
||||
t.Fatalf("WaitForAngular returned error: %v", err)
|
||||
}
|
||||
if evaluateCount < 2 {
|
||||
t.Fatalf("WaitForAngular made %d evaluate calls, want at least 2", evaluateCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_waitForZoneStability_Good_FallsBackToPolling(t *testing.T) {
|
||||
var calls int
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
calls++
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
switch {
|
||||
case strings.Contains(expr, "new Promise"):
|
||||
target.replyError(msg.ID, "zone probe failed")
|
||||
default:
|
||||
target.replyValue(msg.ID, true)
|
||||
}
|
||||
})
|
||||
|
||||
if err := ah.waitForZoneStability(context.Background()); err != nil {
|
||||
t.Fatalf("waitForZoneStability returned error: %v", err)
|
||||
}
|
||||
if calls < 2 {
|
||||
t.Fatalf("waitForZoneStability made %d evaluate calls, want at least 2", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_NavigateByRouter_Good(t *testing.T) {
|
||||
var expressions []string
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
expressions = append(expressions, expr)
|
||||
if strings.Contains(expr, "navigateByUrl") {
|
||||
target.replyValue(msg.ID, true)
|
||||
return
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := ah.NavigateByRouter("/dashboard"); err != nil {
|
||||
t.Fatalf("NavigateByRouter returned error: %v", err)
|
||||
}
|
||||
if len(expressions) < 2 {
|
||||
t.Fatalf("NavigateByRouter made %d evaluate calls, want at least 2", len(expressions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_NavigateByRouter_Bad(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.replyError(msg.ID, "could not find router")
|
||||
})
|
||||
|
||||
if err := ah.NavigateByRouter("/dashboard"); err == nil {
|
||||
t.Fatal("NavigateByRouter succeeded despite evaluation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_GetComponentProperty_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
if !strings.Contains(expr, `const selector = "app-user";`) {
|
||||
t.Fatalf("expression did not quote selector: %s", expr)
|
||||
}
|
||||
if !strings.Contains(expr, `const propertyName = "displayName";`) {
|
||||
t.Fatalf("expression did not quote property name: %s", expr)
|
||||
}
|
||||
target.replyValue(msg.ID, "Ada")
|
||||
})
|
||||
|
||||
got, err := ah.GetComponentProperty("app-user", "displayName")
|
||||
if err != nil {
|
||||
t.Fatalf("GetComponentProperty returned error: %v", err)
|
||||
}
|
||||
if got != "Ada" {
|
||||
t.Fatalf("GetComponentProperty = %v, want Ada", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_SetComponentProperty_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
if !strings.Contains(expr, `component[propertyName] = true;`) {
|
||||
t.Fatalf("expression did not set the component property: %s", expr)
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := ah.SetComponentProperty("app-user", "active", true); err != nil {
|
||||
t.Fatalf("SetComponentProperty returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_CallComponentMethod_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
if !strings.Contains(expr, `component[methodName](1, "two")`) {
|
||||
t.Fatalf("expression did not marshal method args: %s", expr)
|
||||
}
|
||||
target.replyValue(msg.ID, map[string]any{"ok": true})
|
||||
})
|
||||
|
||||
got, err := ah.CallComponentMethod("app-user", "save", 1, "two")
|
||||
if err != nil {
|
||||
t.Fatalf("CallComponentMethod returned error: %v", err)
|
||||
}
|
||||
if gotMap, ok := got.(map[string]any); !ok || gotMap["ok"] != true {
|
||||
t.Fatalf("CallComponentMethod = %#v, want ok=true", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_TriggerChangeDetection_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := ah.TriggerChangeDetection(); err != nil {
|
||||
t.Fatalf("TriggerChangeDetection returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_GetService_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.replyValue(msg.ID, map[string]any{"name": "session"})
|
||||
})
|
||||
|
||||
got, err := ah.GetService("SessionService")
|
||||
if err != nil {
|
||||
t.Fatalf("GetService returned error: %v", err)
|
||||
}
|
||||
if gotMap, ok := got.(map[string]any); !ok || gotMap["name"] != "session" {
|
||||
t.Fatalf("GetService = %#v, want session map", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_WaitForComponent_Good(t *testing.T) {
|
||||
var calls int
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
calls++
|
||||
if calls == 1 {
|
||||
target.replyValue(msg.ID, false)
|
||||
return
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := ah.WaitForComponent("app-user"); err != nil {
|
||||
t.Fatalf("WaitForComponent returned error: %v", err)
|
||||
}
|
||||
if calls < 2 {
|
||||
t.Fatalf("WaitForComponent calls = %d, want at least 2", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_DispatchEvent_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
if !strings.Contains(expr, `new CustomEvent(eventName, { bubbles: true, detail: {"count":1} })`) && !strings.Contains(expr, `new CustomEvent(eventName, { bubbles: true, detail: {\"count\":1} })`) {
|
||||
t.Fatalf("expression did not dispatch custom event with detail: %s", expr)
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := ah.DispatchEvent("app-user", "count-change", map[string]any{"count": 1}); err != nil {
|
||||
t.Fatalf("DispatchEvent returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_GetNgModel_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.replyValue(msg.ID, "hello")
|
||||
})
|
||||
|
||||
got, err := ah.GetNgModel("input[name=email]")
|
||||
if err != nil {
|
||||
t.Fatalf("GetNgModel returned error: %v", err)
|
||||
}
|
||||
if got != "hello" {
|
||||
t.Fatalf("GetNgModel = %v, want hello", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_SetNgModel_Good(t *testing.T) {
|
||||
ah, _, _ := newAngularTestHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := ah.SetNgModel(`input[name="x"]`, `";window.hacked=true;//`); err != nil {
|
||||
t.Fatalf("SetNgModel returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAngular_copyStringOnlyMap_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in any
|
||||
want map[string]string
|
||||
}{
|
||||
{name: "map any", in: map[string]any{"a": "1", "b": 2}, want: map[string]string{"a": "1"}},
|
||||
{name: "map string", in: map[string]string{"c": "3"}, want: map[string]string{"c": "3"}},
|
||||
{name: "nil", in: nil, want: nil},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := copyStringOnlyMap(tc.in)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("copyStringOnlyMap len = %d, want %d", len(got), len(tc.want))
|
||||
}
|
||||
for k, want := range tc.want {
|
||||
if got[k] != want {
|
||||
t.Fatalf("copyStringOnlyMap[%q] = %q, want %q", k, got[k], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
404
cdp_test.go
Normal file
404
cdp_test.go
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
package webview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func newConnectedCDPClient(t *testing.T, target *fakeCDPTarget) *CDPClient {
|
||||
t.Helper()
|
||||
|
||||
client, err := NewCDPClient(target.server.DebugURL())
|
||||
if err != nil {
|
||||
t.Fatalf("NewCDPClient returned error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
func TestCdp_parseDebugURL_Good(t *testing.T) {
|
||||
tests := []string{
|
||||
"http://localhost:9222",
|
||||
"http://127.0.0.1:9222",
|
||||
"http://[::1]:9222",
|
||||
"https://localhost:9222/",
|
||||
}
|
||||
|
||||
for _, raw := range tests {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
got, err := parseDebugURL(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parseDebugURL returned error: %v", err)
|
||||
}
|
||||
if got.Scheme != strings.Split(raw, ":")[0] {
|
||||
t.Fatalf("parseDebugURL scheme = %q, want %q", got.Scheme, strings.Split(raw, ":")[0])
|
||||
}
|
||||
if got.Path != "/" {
|
||||
t.Fatalf("parseDebugURL path = %q, want %q", got.Path, "/")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_parseDebugURL_Bad(t *testing.T) {
|
||||
tests := []string{
|
||||
"http://example.com:9222",
|
||||
"http://localhost:9222/json",
|
||||
"http://localhost:9222?x=1",
|
||||
"http://localhost:9222#frag",
|
||||
"http://user:pass@localhost:9222",
|
||||
"ftp://localhost:9222",
|
||||
"localhost:9222",
|
||||
}
|
||||
|
||||
for _, raw := range tests {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
if _, err := parseDebugURL(raw); err == nil {
|
||||
t.Fatalf("parseDebugURL(%q) returned nil error", raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_validateNavigationURL_Good(t *testing.T) {
|
||||
for _, raw := range []string{
|
||||
"http://localhost:8080/path?q=1",
|
||||
"https://example.com",
|
||||
"about:blank",
|
||||
} {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
if err := validateNavigationURL(raw); err != nil {
|
||||
t.Fatalf("validateNavigationURL returned error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_validateNavigationURL_Bad(t *testing.T) {
|
||||
for _, raw := range []string{
|
||||
"javascript:alert(1)",
|
||||
"data:text/html,hello",
|
||||
"file:///etc/passwd",
|
||||
"about:srcdoc",
|
||||
"http://",
|
||||
"https://user:pass@example.com",
|
||||
} {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
if err := validateNavigationURL(raw); err == nil {
|
||||
t.Fatalf("validateNavigationURL(%q) returned nil error", raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_normalisedPort_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
raw string
|
||||
want string
|
||||
}{
|
||||
{"http://localhost", "80"},
|
||||
{"ws://localhost", "80"},
|
||||
{"https://localhost", "443"},
|
||||
{"wss://localhost", "443"},
|
||||
{"http://localhost:1234", "1234"},
|
||||
{"ws://localhost:5678", "5678"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.raw, func(t *testing.T) {
|
||||
u, err := url.Parse(tc.raw)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse returned error: %v", err)
|
||||
}
|
||||
if got := normalisedPort(u); got != tc.want {
|
||||
t.Fatalf("normalisedPort(%q) = %q, want %q", tc.raw, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_normalisedPort_Ugly(t *testing.T) {
|
||||
u, err := url.Parse("ftp://localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse returned error: %v", err)
|
||||
}
|
||||
if got := normalisedPort(u); got != "" {
|
||||
t.Fatalf("normalisedPort(ftp://localhost) = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_targetIDFromWebSocketURL_Good(t *testing.T) {
|
||||
got, err := targetIDFromWebSocketURL("ws://localhost:9222/devtools/page/target-1")
|
||||
if err != nil {
|
||||
t.Fatalf("targetIDFromWebSocketURL returned error: %v", err)
|
||||
}
|
||||
if got != "target-1" {
|
||||
t.Fatalf("targetIDFromWebSocketURL = %q, want %q", got, "target-1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_targetIDFromWebSocketURL_Bad(t *testing.T) {
|
||||
for _, raw := range []string{
|
||||
"ws://localhost:9222/",
|
||||
"ws://localhost:9222",
|
||||
} {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
if _, err := targetIDFromWebSocketURL(raw); err == nil {
|
||||
t.Fatalf("targetIDFromWebSocketURL(%q) returned nil error", raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_isTerminalReadError_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{name: "nil", err: nil, want: false},
|
||||
{name: "net closed", err: net.ErrClosed, want: true},
|
||||
{name: "ws close sent", err: websocket.ErrCloseSent, want: true},
|
||||
{name: "close error", err: &websocket.CloseError{Code: 1000, Text: "bye"}, want: true},
|
||||
{name: "other", err: context.DeadlineExceeded, want: false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := isTerminalReadError(tc.err); got != tc.want {
|
||||
t.Fatalf("isTerminalReadError(%v) = %v, want %v", tc.err, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_cloneHelpers_Good(t *testing.T) {
|
||||
original := map[string]any{
|
||||
"nested": map[string]any{"count": float64(1)},
|
||||
"items": []any{map[string]any{"id": "alpha"}},
|
||||
"value": "original",
|
||||
}
|
||||
|
||||
cloned := cloneMapAny(original)
|
||||
cloned["value"] = "changed"
|
||||
cloned["nested"].(map[string]any)["count"] = float64(2)
|
||||
cloned["items"].([]any)[0].(map[string]any)["id"] = "beta"
|
||||
|
||||
if got := original["value"]; got != "original" {
|
||||
t.Fatalf("original scalar mutated: %v", got)
|
||||
}
|
||||
if got := original["nested"].(map[string]any)["count"]; got != float64(1) {
|
||||
t.Fatalf("original nested map mutated: %v", got)
|
||||
}
|
||||
if got := original["items"].([]any)[0].(map[string]any)["id"]; got != "alpha" {
|
||||
t.Fatalf("original nested slice mutated: %v", got)
|
||||
}
|
||||
|
||||
if cloneMapAny(nil) != nil {
|
||||
t.Fatal("cloneMapAny(nil) = non-nil")
|
||||
}
|
||||
if cloneSliceAny(nil) != nil {
|
||||
t.Fatal("cloneSliceAny(nil) = non-nil")
|
||||
}
|
||||
if got := cloneAny(original).(map[string]any)["value"]; got != "original" {
|
||||
t.Fatalf("cloneAny(map) = %v, want original value", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_ListTargets_Good(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
|
||||
targets, err := ListTargets(server.DebugURL())
|
||||
if err != nil {
|
||||
t.Fatalf("ListTargets returned error: %v", err)
|
||||
}
|
||||
if len(targets) != 1 {
|
||||
t.Fatalf("ListTargets returned %d targets, want 1", len(targets))
|
||||
}
|
||||
if targets[0].Type != "page" {
|
||||
t.Fatalf("ListTargets type = %q, want page", targets[0].Type)
|
||||
}
|
||||
|
||||
got := make([]TargetInfo, 0)
|
||||
for target := range ListTargetsAll(server.DebugURL()) {
|
||||
got = append(got, target)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("ListTargetsAll yielded %d targets, want 1", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_GetVersion_Good(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
|
||||
version, err := GetVersion(server.DebugURL())
|
||||
if err != nil {
|
||||
t.Fatalf("GetVersion returned error: %v", err)
|
||||
}
|
||||
if got := version["Browser"]; got != "Chrome/123.0" {
|
||||
t.Fatalf("GetVersion Browser = %q, want Chrome/123.0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_NewCDPClient_Good_AutoCreatesTarget(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
server.mu.Lock()
|
||||
server.targets = make(map[string]*fakeCDPTarget)
|
||||
server.nextTarget = 0
|
||||
server.mu.Unlock()
|
||||
|
||||
client, err := NewCDPClient(server.DebugURL())
|
||||
if err != nil {
|
||||
t.Fatalf("NewCDPClient returned error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
|
||||
if client.DebugURL() != server.DebugURL() {
|
||||
t.Fatalf("DebugURL() = %q, want %q", client.DebugURL(), server.DebugURL())
|
||||
}
|
||||
if client.WebSocketURL() == "" {
|
||||
t.Fatal("WebSocketURL() returned empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_NewCDPClient_Bad_RejectsInvalidDebugURL(t *testing.T) {
|
||||
_, err := NewCDPClient("http://example.com:9222")
|
||||
if err == nil {
|
||||
t.Fatal("NewCDPClient succeeded for remote host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_Send_Good(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
|
||||
client := newConnectedCDPClient(t, target)
|
||||
|
||||
done := make(chan cdpMessage, 1)
|
||||
target.onMessage = func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
done <- msg
|
||||
}
|
||||
|
||||
if err := client.Send("Page.enable", map[string]any{"foo": "bar"}); err != nil {
|
||||
t.Fatalf("Send returned error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case msg := <-done:
|
||||
if msg.Method != "Page.enable" {
|
||||
t.Fatalf("Send method = %q, want Page.enable", msg.Method)
|
||||
}
|
||||
if got := msg.Params["foo"]; got != "bar" {
|
||||
t.Fatalf("Send param foo = %v, want bar", got)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for sent message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_NewTab_Good(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
client := newConnectedCDPClient(t, target)
|
||||
|
||||
tab, err := client.NewTab("about:blank")
|
||||
if err != nil {
|
||||
t.Fatalf("NewTab returned error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = tab.Close()
|
||||
})
|
||||
|
||||
if tab.WebSocketURL() == "" {
|
||||
t.Fatal("NewTab returned empty WebSocket URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_CloseTab_Bad_TargetCloseNotAcknowledged(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
target.onMessage = func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Target.closeTarget" {
|
||||
t.Fatalf("CloseTab sent %q, want Target.closeTarget", msg.Method)
|
||||
}
|
||||
target.reply(msg.ID, map[string]any{"success": false})
|
||||
}
|
||||
|
||||
client := newConnectedCDPClient(t, target)
|
||||
if err := client.CloseTab(); err == nil {
|
||||
t.Fatal("CloseTab succeeded without target close acknowledgement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_failPending_Good(t *testing.T) {
|
||||
c1 := make(chan *cdpResponse, 1)
|
||||
c2 := make(chan *cdpResponse, 1)
|
||||
client := &CDPClient{
|
||||
pending: map[int64]chan *cdpResponse{
|
||||
1: c1,
|
||||
2: c2,
|
||||
},
|
||||
}
|
||||
|
||||
client.failPending(errors.New("boom"))
|
||||
|
||||
for i, ch := range []chan *cdpResponse{c1, c2} {
|
||||
select {
|
||||
case resp := <-ch:
|
||||
if resp.Error == nil || resp.Error.Message != "boom" {
|
||||
t.Fatalf("pending response %d = %#v, want boom error", i+1, resp)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("pending response %d was not delivered", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_createTargetAt_Good(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
target, err := createTargetAt(context.Background(), mustParseURL(t, server.DebugURL()), "about:blank")
|
||||
if err != nil {
|
||||
t.Fatalf("createTargetAt returned error: %v", err)
|
||||
}
|
||||
if target == nil || target.WebSocketDebuggerURL == "" {
|
||||
t.Fatalf("createTargetAt returned %#v", target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCdp_doDebugRequest_Bad_HTTPStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
debugURL, err := parseDebugURL(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("parseDebugURL returned error: %v", err)
|
||||
}
|
||||
if _, err := doDebugRequest(context.Background(), debugURL, "/json", ""); err == nil {
|
||||
t.Fatal("doDebugRequest returned nil error for non-2xx status")
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseURL(t *testing.T, raw string) *url.URL {
|
||||
t.Helper()
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse returned error: %v", err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
301
console_test.go
Normal file
301
console_test.go
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
package webview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConsole_normalizeConsoleType_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
raw string
|
||||
want string
|
||||
}{
|
||||
{raw: "warn", want: "warn"},
|
||||
{raw: "warning", want: "warn"},
|
||||
{raw: " WARNING ", want: "warn"},
|
||||
{raw: "error", want: "error"},
|
||||
{raw: "info", want: "info"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.raw, func(t *testing.T) {
|
||||
if got := normalizeConsoleType(tc.raw); got != tc.want {
|
||||
t.Fatalf("normalizeConsoleType(%q) = %q, want %q", tc.raw, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_consoleValueToString_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
val any
|
||||
want string
|
||||
}{
|
||||
{name: "nil", val: nil, want: "null"},
|
||||
{name: "string", val: "hello", want: "hello"},
|
||||
{name: "number", val: float64(12), want: "12"},
|
||||
{name: "bool", val: true, want: "true"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := consoleValueToString(tc.val); got != tc.want {
|
||||
t.Fatalf("consoleValueToString(%v) = %q, want %q", tc.val, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_consoleArgText_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg any
|
||||
want string
|
||||
}{
|
||||
{name: "value", arg: map[string]any{"value": "alpha"}, want: "alpha"},
|
||||
{name: "description", arg: map[string]any{"description": "bravo"}, want: "bravo"},
|
||||
{name: "preview description", arg: map[string]any{"preview": map[string]any{"description": "charlie"}}, want: "charlie"},
|
||||
{name: "preview value", arg: map[string]any{"preview": map[string]any{"value": "delta"}}, want: "delta"},
|
||||
{name: "plain scalar", arg: 42, want: "42"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := consoleArgText(tc.arg); got != tc.want {
|
||||
t.Fatalf("consoleArgText(%v) = %q, want %q", tc.arg, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_consoleArgText_Ugly(t *testing.T) {
|
||||
got := consoleArgText(map[string]any{"value": map[string]any{"nested": true}})
|
||||
if !strings.Contains(got, `"nested":true`) {
|
||||
t.Fatalf("consoleArgText fallback JSON = %q, want JSON encoding", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_trimConsoleMessages_Good(t *testing.T) {
|
||||
messages := []ConsoleMessage{
|
||||
{Text: "one"},
|
||||
{Text: "two"},
|
||||
{Text: "three"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
limit int
|
||||
want []string
|
||||
}{
|
||||
{name: "no trim", limit: 3, want: []string{"one", "two", "three"}},
|
||||
{name: "trim to one", limit: 1, want: []string{"three"}},
|
||||
{name: "zero", limit: 0, want: nil},
|
||||
{name: "negative", limit: -1, want: nil},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cloned := append([]ConsoleMessage(nil), messages...)
|
||||
got := trimConsoleMessages(cloned, tc.limit)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("trimConsoleMessages len = %d, want %d", len(got), len(tc.want))
|
||||
}
|
||||
for i, want := range tc.want {
|
||||
if got[i].Text != want {
|
||||
t.Fatalf("trimConsoleMessages[%d] = %q, want %q", i, got[i].Text, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_sanitizeConsoleText_Good(t *testing.T) {
|
||||
got := sanitizeConsoleText("line1\nline2\r\t\x1b[31m\x7f")
|
||||
if !strings.Contains(got, `line1\nline2\r\t\x1b[31m`) {
|
||||
t.Fatalf("sanitizeConsoleText did not escape control characters: %q", got)
|
||||
}
|
||||
if strings.Contains(got, "\x7f") {
|
||||
t.Fatalf("sanitizeConsoleText kept DEL byte: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_runtimeExceptionText_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
want string
|
||||
}{
|
||||
{name: "description", in: map[string]any{"exception": map[string]any{"description": "stack"}}, want: "stack"},
|
||||
{name: "text", in: map[string]any{"text": "boom"}, want: "boom"},
|
||||
{name: "default", in: map[string]any{}, want: "JavaScript error"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := runtimeExceptionText(tc.in); got != tc.want {
|
||||
t.Fatalf("runtimeExceptionText = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_NewConsoleWatcher_Good(t *testing.T) {
|
||||
watcher := NewConsoleWatcher(nil)
|
||||
if watcher == nil {
|
||||
t.Fatal("NewConsoleWatcher returned nil")
|
||||
}
|
||||
if watcher.Count() != 0 {
|
||||
t.Fatalf("NewConsoleWatcher count = %d, want 0", watcher.Count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_NewConsoleWatcher_Good_SubscribesToClient(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
client := newConnectedCDPClient(t, target)
|
||||
watcher := NewConsoleWatcher(&Webview{client: client})
|
||||
|
||||
target.writeJSON(cdpEvent{
|
||||
Method: "Runtime.consoleAPICalled",
|
||||
Params: map[string]any{
|
||||
"type": "log",
|
||||
"args": []any{map[string]any{"value": "hello"}},
|
||||
},
|
||||
})
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if watcher.Count() != 1 {
|
||||
t.Fatalf("NewConsoleWatcher subscription count = %d, want 1", watcher.Count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_WaitForError_Good(t *testing.T) {
|
||||
watcher := &ConsoleWatcher{
|
||||
messages: make([]ConsoleMessage, 0),
|
||||
filters: make([]ConsoleFilter, 0),
|
||||
limit: 10,
|
||||
handlers: make([]consoleHandlerRegistration, 0),
|
||||
}
|
||||
watcher.addMessage(ConsoleMessage{Type: "warn", Text: "ignore"})
|
||||
watcher.addMessage(ConsoleMessage{Type: "error", Text: "boom"})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
msg, err := watcher.WaitForError(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("WaitForError returned error: %v", err)
|
||||
}
|
||||
if msg.Text != "boom" {
|
||||
t.Fatalf("WaitForError text = %q, want boom", msg.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_WaitForError_Bad(t *testing.T) {
|
||||
watcher := &ConsoleWatcher{
|
||||
messages: make([]ConsoleMessage, 0),
|
||||
filters: make([]ConsoleFilter, 0),
|
||||
limit: 10,
|
||||
handlers: make([]consoleHandlerRegistration, 0),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
if _, err := watcher.WaitForError(ctx); err == nil {
|
||||
t.Fatal("WaitForError succeeded without an error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_handleConsoleEvent_Good(t *testing.T) {
|
||||
watcher := &ConsoleWatcher{
|
||||
messages: make([]ConsoleMessage, 0),
|
||||
filters: make([]ConsoleFilter, 0),
|
||||
limit: 10,
|
||||
handlers: make([]consoleHandlerRegistration, 0),
|
||||
}
|
||||
|
||||
watcher.handleConsoleEvent(map[string]any{
|
||||
"type": "warning",
|
||||
"args": []any{
|
||||
map[string]any{"value": "alpha"},
|
||||
map[string]any{"description": "beta"},
|
||||
},
|
||||
"stackTrace": map[string]any{
|
||||
"callFrames": []any{
|
||||
map[string]any{
|
||||
"url": "https://example.com/app.js",
|
||||
"lineNumber": float64(12),
|
||||
"columnNumber": float64(34),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
msgs := watcher.Messages()
|
||||
if len(msgs) != 1 {
|
||||
t.Fatalf("handleConsoleEvent stored %d messages, want 1", len(msgs))
|
||||
}
|
||||
if msgs[0].Type != "warning" {
|
||||
t.Fatalf("handleConsoleEvent type = %q, want warning", msgs[0].Type)
|
||||
}
|
||||
if msgs[0].Text != "alpha beta" {
|
||||
t.Fatalf("handleConsoleEvent text = %q, want %q", msgs[0].Text, "alpha beta")
|
||||
}
|
||||
if msgs[0].URL != "https://example.com/app.js" || msgs[0].Line != 12 || msgs[0].Column != 34 {
|
||||
t.Fatalf("handleConsoleEvent stack info = %#v", msgs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_NewExceptionWatcher_Good(t *testing.T) {
|
||||
watcher := NewExceptionWatcher(nil)
|
||||
if watcher == nil {
|
||||
t.Fatal("NewExceptionWatcher returned nil")
|
||||
}
|
||||
if watcher.Count() != 0 {
|
||||
t.Fatalf("NewExceptionWatcher count = %d, want 0", watcher.Count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_NewExceptionWatcher_Good_SubscribesToClient(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
client := newConnectedCDPClient(t, target)
|
||||
watcher := NewExceptionWatcher(&Webview{client: client})
|
||||
|
||||
target.writeJSON(cdpEvent{
|
||||
Method: "Runtime.exceptionThrown",
|
||||
Params: map[string]any{
|
||||
"exceptionDetails": map[string]any{
|
||||
"text": "boom",
|
||||
"lineNumber": float64(1),
|
||||
"columnNumber": float64(2),
|
||||
"url": "https://example.com/app.js",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if watcher.Count() != 1 {
|
||||
t.Fatalf("NewExceptionWatcher subscription count = %d, want 1", watcher.Count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_isWarningType_Good(t *testing.T) {
|
||||
tests := map[string]bool{
|
||||
"warn": true,
|
||||
"warning": true,
|
||||
"ERROR": false,
|
||||
}
|
||||
for raw, want := range tests {
|
||||
if got := isWarningType(raw); got != want {
|
||||
t.Fatalf("isWarningType(%q) = %v, want %v", raw, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
404
webview_methods_test.go
Normal file
404
webview_methods_test.go
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
package webview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newWebviewHarness(t *testing.T, onMessage func(*fakeCDPTarget, cdpMessage)) (*Webview, *fakeCDPTarget) {
|
||||
t.Helper()
|
||||
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
target.onMessage = onMessage
|
||||
|
||||
client := newConnectedCDPClient(t, target)
|
||||
wv := &Webview{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
timeout: time.Second,
|
||||
consoleLogs: make([]ConsoleMessage, 0),
|
||||
consoleLimit: 10,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
return wv, target
|
||||
}
|
||||
|
||||
func TestWebview_Close_Good(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
client := newConnectedCDPClient(t, server.primaryTarget())
|
||||
wv := &Webview{
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
cancel: func() {},
|
||||
consoleLogs: make([]ConsoleMessage, 0),
|
||||
consoleLimit: 10,
|
||||
}
|
||||
|
||||
if err := wv.Close(); err != nil {
|
||||
t.Fatalf("Close returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_New_Good_EnablesConsoleCapture(t *testing.T) {
|
||||
server := newFakeCDPServer(t)
|
||||
target := server.primaryTarget()
|
||||
target.onMessage = func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "Runtime.enable", "Page.enable", "DOM.enable":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q during New", msg.Method)
|
||||
}
|
||||
}
|
||||
|
||||
wv, err := New(WithDebugURL(server.DebugURL()))
|
||||
if err != nil {
|
||||
t.Fatalf("New returned error: %v", err)
|
||||
}
|
||||
defer func() { _ = wv.Close() }()
|
||||
|
||||
target.writeJSON(cdpEvent{
|
||||
Method: "Runtime.consoleAPICalled",
|
||||
Params: map[string]any{
|
||||
"type": "log",
|
||||
"args": []any{map[string]any{"value": "hello"}},
|
||||
},
|
||||
})
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if got := wv.GetConsole(); len(got) != 1 || got[0].Text != "hello" {
|
||||
t.Fatalf("New console capture = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_Navigate_Bad(t *testing.T) {
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
t.Fatalf("unexpected CDP call %q for invalid navigation URL", msg.Method)
|
||||
})
|
||||
|
||||
if err := wv.Navigate("javascript:alert(1)"); err == nil {
|
||||
t.Fatal("Navigate succeeded with dangerous URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_Navigate_Good(t *testing.T) {
|
||||
var methods []string
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
methods = append(methods, msg.Method)
|
||||
switch msg.Method {
|
||||
case "Page.navigate":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
case "Runtime.evaluate":
|
||||
target.replyValue(msg.ID, "complete")
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
if err := wv.Navigate("https://example.com"); err != nil {
|
||||
t.Fatalf("Navigate returned error: %v", err)
|
||||
}
|
||||
if len(methods) != 2 || methods[0] != "Page.navigate" || methods[1] != "Runtime.evaluate" {
|
||||
t.Fatalf("Navigate call order = %v", methods)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_QuerySelectorAndAll_Good(t *testing.T) {
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(21)})
|
||||
case "DOM.querySelectorAll":
|
||||
target.reply(msg.ID, map[string]any{"nodeIds": []any{float64(21), float64(22)}})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "DIV", "attributes": []any{"id", "main"}}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-1"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "<span>hello</span>", "innerText": "hello"}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": []any{float64(1), float64(2), float64(11), float64(2), float64(11), float64(12), float64(1), float64(12)}}})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
elem, err := wv.QuerySelector("#main")
|
||||
if err != nil {
|
||||
t.Fatalf("QuerySelector returned error: %v", err)
|
||||
}
|
||||
if elem.NodeID != 21 || elem.TagName != "DIV" || elem.InnerText != "hello" {
|
||||
t.Fatalf("QuerySelector returned %#v", elem)
|
||||
}
|
||||
if elem.BoundingBox == nil || elem.BoundingBox.Width != 10 || elem.BoundingBox.Height != 10 {
|
||||
t.Fatalf("QuerySelector bounding box = %#v", elem.BoundingBox)
|
||||
}
|
||||
|
||||
all, err := wv.QuerySelectorAll("div.item")
|
||||
if err != nil {
|
||||
t.Fatalf("QuerySelectorAll returned error: %v", err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("QuerySelectorAll len = %d, want 2", len(all))
|
||||
}
|
||||
|
||||
iterated := make([]int, 0)
|
||||
for elem := range wv.QuerySelectorAllAll("div.item") {
|
||||
iterated = append(iterated, elem.NodeID)
|
||||
break
|
||||
}
|
||||
if len(iterated) != 1 || iterated[0] != 21 {
|
||||
t.Fatalf("QuerySelectorAllAll yielded %v, want first node 21", iterated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_ClickAndType_Good(t *testing.T) {
|
||||
var methods []string
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
methods = append(methods, msg.Method)
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(10)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "BUTTON"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-1"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": []any{float64(10), float64(20), float64(30), float64(20), float64(30), float64(40), float64(10), float64(40)}}})
|
||||
case "Input.dispatchMouseEvent", "Input.dispatchKeyEvent":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
case "Runtime.evaluate":
|
||||
target.replyValue(msg.ID, true)
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
if err := wv.Click("#button"); err != nil {
|
||||
t.Fatalf("Click returned error: %v", err)
|
||||
}
|
||||
if err := wv.Type("#input", "ab"); err != nil {
|
||||
t.Fatalf("Type returned error: %v", err)
|
||||
}
|
||||
if len(methods) < 8 {
|
||||
t.Fatalf("Click+Type methods = %v", methods)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_WaitForSelector_Good(t *testing.T) {
|
||||
var calls int
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
calls++
|
||||
if calls == 1 {
|
||||
target.replyValue(msg.ID, false)
|
||||
return
|
||||
}
|
||||
target.replyValue(msg.ID, true)
|
||||
})
|
||||
|
||||
if err := wv.WaitForSelector("#ready"); err != nil {
|
||||
t.Fatalf("WaitForSelector returned error: %v", err)
|
||||
}
|
||||
if calls < 2 {
|
||||
t.Fatalf("WaitForSelector calls = %d, want at least 2", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_ScreenshotAndInfo_Good(t *testing.T) {
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "Page.captureScreenshot":
|
||||
if got := msg.Params["format"]; got != "png" {
|
||||
t.Fatalf("captureScreenshot format = %v, want png", got)
|
||||
}
|
||||
target.reply(msg.ID, map[string]any{"data": base64.StdEncoding.EncodeToString([]byte{0x89, 0x50, 0x4e, 0x47})})
|
||||
case "Runtime.evaluate":
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
switch expr {
|
||||
case "window.location.href":
|
||||
target.replyValue(msg.ID, "https://example.com")
|
||||
case "document.title":
|
||||
target.replyValue(msg.ID, "Example")
|
||||
case "document.documentElement.outerHTML":
|
||||
target.replyValue(msg.ID, "<html></html>")
|
||||
case "document.readyState":
|
||||
target.replyValue(msg.ID, "complete")
|
||||
default:
|
||||
t.Fatalf("unexpected evaluate expression %q", expr)
|
||||
}
|
||||
case "Emulation.setDeviceMetricsOverride", "Emulation.setUserAgentOverride", "Page.reload":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
png, err := wv.Screenshot()
|
||||
if err != nil {
|
||||
t.Fatalf("Screenshot returned error: %v", err)
|
||||
}
|
||||
if len(png) != 4 || png[0] != 0x89 {
|
||||
t.Fatalf("Screenshot bytes = %v", png)
|
||||
}
|
||||
|
||||
if got, err := wv.GetURL(); err != nil || got != "https://example.com" {
|
||||
t.Fatalf("GetURL = %q, %v", got, err)
|
||||
}
|
||||
if got, err := wv.GetTitle(); err != nil || got != "Example" {
|
||||
t.Fatalf("GetTitle = %q, %v", got, err)
|
||||
}
|
||||
if got, err := wv.GetHTML(""); err != nil || got != "<html></html>" {
|
||||
t.Fatalf("GetHTML = %q, %v", got, err)
|
||||
}
|
||||
if err := wv.SetViewport(1440, 900); err != nil {
|
||||
t.Fatalf("SetViewport returned error: %v", err)
|
||||
}
|
||||
if err := wv.SetUserAgent("AgentHarness/1.0"); err != nil {
|
||||
t.Fatalf("SetUserAgent returned error: %v", err)
|
||||
}
|
||||
if err := wv.Reload(); err != nil {
|
||||
t.Fatalf("Reload returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_Console_Good(t *testing.T) {
|
||||
wv := &Webview{
|
||||
consoleLogs: make([]ConsoleMessage, 0),
|
||||
consoleLimit: 2,
|
||||
}
|
||||
|
||||
wv.addConsoleMessage(ConsoleMessage{Text: "one"})
|
||||
wv.addConsoleMessage(ConsoleMessage{Text: "two"})
|
||||
wv.addConsoleMessage(ConsoleMessage{Text: "three"})
|
||||
|
||||
got := wv.GetConsole()
|
||||
if len(got) != 2 || got[0].Text != "two" || got[1].Text != "three" {
|
||||
t.Fatalf("GetConsole = %#v", got)
|
||||
}
|
||||
|
||||
iterated := make([]string, 0)
|
||||
for msg := range wv.GetConsoleAll() {
|
||||
iterated = append(iterated, msg.Text)
|
||||
}
|
||||
if len(iterated) != 2 {
|
||||
t.Fatalf("GetConsoleAll = %#v", iterated)
|
||||
}
|
||||
|
||||
wv.ClearConsole()
|
||||
if got := wv.GetConsole(); len(got) != 0 {
|
||||
t.Fatalf("ClearConsole did not empty logs: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_UploadFileAndDragAndDrop_Good(t *testing.T) {
|
||||
var methods []string
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
methods = append(methods, msg.Method)
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
sel, _ := msg.Params["selector"].(string)
|
||||
switch sel {
|
||||
case "#file":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(41)})
|
||||
case "#source":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(42)})
|
||||
case "#target":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(43)})
|
||||
default:
|
||||
t.Fatalf("unexpected selector %q", sel)
|
||||
}
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "INPUT"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-1"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
nodeID := int(msg.Params["nodeId"].(float64))
|
||||
box := []any{float64(nodeID), float64(nodeID), float64(nodeID + 1), float64(nodeID), float64(nodeID + 1), float64(nodeID + 1), float64(nodeID), float64(nodeID + 1)}
|
||||
target.reply(msg.ID, map[string]any{"model": map[string]any{"content": box}})
|
||||
case "DOM.setFileInputFiles", "Input.dispatchMouseEvent":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
if err := wv.UploadFile("#file", []string{"/tmp/a.txt"}); err != nil {
|
||||
t.Fatalf("UploadFile returned error: %v", err)
|
||||
}
|
||||
if err := wv.DragAndDrop("#source", "#target"); err != nil {
|
||||
t.Fatalf("DragAndDrop returned error: %v", err)
|
||||
}
|
||||
if len(methods) < 10 {
|
||||
t.Fatalf("UploadFile+DragAndDrop methods = %v", methods)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_WaitForSelector_Bad(t *testing.T) {
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
if msg.Method != "Runtime.evaluate" {
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
target.replyValue(msg.ID, false)
|
||||
})
|
||||
wv.timeout = 50 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithTimeout(wv.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := wv.waitForSelector(ctx, "#never"); err == nil {
|
||||
t.Fatal("waitForSelector succeeded without matching element")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebview_Click_Ugly_FallsBackToJS(t *testing.T) {
|
||||
var expressions []string
|
||||
wv, _ := newWebviewHarness(t, func(target *fakeCDPTarget, msg cdpMessage) {
|
||||
switch msg.Method {
|
||||
case "DOM.getDocument":
|
||||
target.reply(msg.ID, map[string]any{"root": map[string]any{"nodeId": float64(1)}})
|
||||
case "DOM.querySelector":
|
||||
target.reply(msg.ID, map[string]any{"nodeId": float64(10)})
|
||||
case "DOM.describeNode":
|
||||
target.reply(msg.ID, map[string]any{"node": map[string]any{"nodeName": "BUTTON"}})
|
||||
case "DOM.resolveNode":
|
||||
target.reply(msg.ID, map[string]any{"object": map[string]any{"objectId": "obj-1"}})
|
||||
case "Runtime.callFunctionOn":
|
||||
target.reply(msg.ID, map[string]any{"result": map[string]any{"value": map[string]any{"innerHTML": "", "innerText": ""}}})
|
||||
case "DOM.getBoxModel":
|
||||
target.reply(msg.ID, map[string]any{})
|
||||
case "Runtime.evaluate":
|
||||
expr, _ := msg.Params["expression"].(string)
|
||||
expressions = append(expressions, expr)
|
||||
target.replyValue(msg.ID, true)
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", msg.Method)
|
||||
}
|
||||
})
|
||||
|
||||
if err := wv.Click("#button"); err != nil {
|
||||
t.Fatalf("Click returned error: %v", err)
|
||||
}
|
||||
if len(expressions) != 1 || !strings.Contains(expressions[0], `document.querySelector("#button")?.click()`) {
|
||||
t.Fatalf("Click fallback expression = %v", expressions)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue