Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath, errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim, core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(), core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives. Framework boundary exceptions preserved where stdlib types are required by external interfaces (Gin, net/http, CGo, Wails, bubbletea). Co-Authored-By: Virgil <virgil@lethean.io>
395 lines
12 KiB
Go
395 lines
12 KiB
Go
// pkg/webview/service_test.go
|
|
package webview
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"strings"
|
|
"testing"
|
|
|
|
"forge.lthn.ai/core/go/pkg/core"
|
|
"dappco.re/go/core/gui/pkg/window"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockConnector struct {
|
|
url string
|
|
title string
|
|
html string
|
|
evalResult any
|
|
evalFn func(script string) (any, error)
|
|
screenshot []byte
|
|
console []ConsoleMessage
|
|
elements []*ElementInfo
|
|
closed bool
|
|
pdfBytes []byte
|
|
printCalled bool
|
|
|
|
lastClickSel string
|
|
lastTypeSel string
|
|
lastTypeText string
|
|
lastNavURL string
|
|
lastHoverSel string
|
|
lastSelectSel string
|
|
lastSelectVal string
|
|
lastCheckSel string
|
|
lastCheckVal bool
|
|
lastUploadSel string
|
|
lastUploadPaths []string
|
|
lastViewportW int
|
|
lastViewportH int
|
|
consoleClearCalled bool
|
|
lastEvalScript string
|
|
}
|
|
|
|
func (m *mockConnector) Navigate(url string) error { m.lastNavURL = url; return nil }
|
|
func (m *mockConnector) Click(sel string) error { m.lastClickSel = sel; return nil }
|
|
func (m *mockConnector) Type(sel, text string) error {
|
|
m.lastTypeSel = sel
|
|
m.lastTypeText = text
|
|
return nil
|
|
}
|
|
func (m *mockConnector) Hover(sel string) error { m.lastHoverSel = sel; return nil }
|
|
func (m *mockConnector) Select(sel, val string) error {
|
|
m.lastSelectSel = sel
|
|
m.lastSelectVal = val
|
|
return nil
|
|
}
|
|
func (m *mockConnector) Check(sel string, c bool) error {
|
|
m.lastCheckSel = sel
|
|
m.lastCheckVal = c
|
|
return nil
|
|
}
|
|
func (m *mockConnector) Evaluate(s string) (any, error) {
|
|
m.lastEvalScript = s
|
|
if m.evalFn != nil {
|
|
return m.evalFn(s)
|
|
}
|
|
return m.evalResult, nil
|
|
}
|
|
func (m *mockConnector) Screenshot() ([]byte, error) { return m.screenshot, nil }
|
|
func (m *mockConnector) GetURL() (string, error) { return m.url, nil }
|
|
func (m *mockConnector) GetTitle() (string, error) { return m.title, nil }
|
|
func (m *mockConnector) GetHTML(sel string) (string, error) { return m.html, nil }
|
|
func (m *mockConnector) ClearConsole() { m.consoleClearCalled = true }
|
|
func (m *mockConnector) Print() error { m.printCalled = true; return nil }
|
|
func (m *mockConnector) Close() error { m.closed = true; return nil }
|
|
func (m *mockConnector) SetViewport(w, h int) error {
|
|
m.lastViewportW = w
|
|
m.lastViewportH = h
|
|
return nil
|
|
}
|
|
func (m *mockConnector) PrintToPDF() ([]byte, error) {
|
|
if len(m.pdfBytes) == 0 {
|
|
return []byte("%PDF-1.4\n"), nil
|
|
}
|
|
return m.pdfBytes, nil
|
|
}
|
|
func (m *mockConnector) UploadFile(sel string, p []string) error {
|
|
m.lastUploadSel = sel
|
|
m.lastUploadPaths = p
|
|
return nil
|
|
}
|
|
|
|
func (m *mockConnector) QuerySelector(sel string) (*ElementInfo, error) {
|
|
if len(m.elements) > 0 {
|
|
return m.elements[0], nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockConnector) QuerySelectorAll(sel string) ([]*ElementInfo, error) {
|
|
return m.elements, nil
|
|
}
|
|
|
|
func (m *mockConnector) GetConsole() []ConsoleMessage { return m.console }
|
|
|
|
func newTestService(t *testing.T, mock *mockConnector) (*Service, *core.Core) {
|
|
t.Helper()
|
|
factory := Register(Options{})
|
|
c, err := core.New(
|
|
core.WithService(window.Register(window.NewMockPlatform())),
|
|
core.WithService(factory),
|
|
core.WithServiceLock(),
|
|
)
|
|
require.NoError(t, err)
|
|
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
|
svc := core.MustServiceFor[*Service](c, "webview")
|
|
// Inject mock connector
|
|
svc.newConn = func(_, _ string) (connector, error) { return mock, nil }
|
|
return svc, c
|
|
}
|
|
|
|
func TestRegister_Good(t *testing.T) {
|
|
svc, _ := newTestService(t, &mockConnector{})
|
|
assert.NotNil(t, svc)
|
|
}
|
|
|
|
func TestQueryURL_Good(t *testing.T) {
|
|
_, c := newTestService(t, &mockConnector{url: "https://example.com"})
|
|
result, handled, err := c.QUERY(QueryURL{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, "https://example.com", result)
|
|
}
|
|
|
|
func TestQueryTitle_Good(t *testing.T) {
|
|
_, c := newTestService(t, &mockConnector{title: "Test Page"})
|
|
result, handled, err := c.QUERY(QueryTitle{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, "Test Page", result)
|
|
}
|
|
|
|
func TestQueryConsole_Good(t *testing.T) {
|
|
mock := &mockConnector{console: []ConsoleMessage{
|
|
{Type: "log", Text: "hello"},
|
|
{Type: "error", Text: "oops"},
|
|
{Type: "log", Text: "world"},
|
|
}}
|
|
_, c := newTestService(t, mock)
|
|
result, handled, err := c.QUERY(QueryConsole{Window: "main", Level: "error", Limit: 10})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
msgs, _ := result.([]ConsoleMessage)
|
|
assert.Len(t, msgs, 1)
|
|
assert.Equal(t, "oops", msgs[0].Text)
|
|
}
|
|
|
|
func TestQueryConsole_Good_Limit(t *testing.T) {
|
|
mock := &mockConnector{console: []ConsoleMessage{
|
|
{Type: "log", Text: "a"},
|
|
{Type: "log", Text: "b"},
|
|
{Type: "log", Text: "c"},
|
|
}}
|
|
_, c := newTestService(t, mock)
|
|
result, _, _ := c.QUERY(QueryConsole{Window: "main", Limit: 2})
|
|
msgs, _ := result.([]ConsoleMessage)
|
|
assert.Len(t, msgs, 2)
|
|
assert.Equal(t, "b", msgs[0].Text) // last 2
|
|
}
|
|
|
|
func TestQueryExceptions_Good(t *testing.T) {
|
|
_, c := newTestService(t, &mockConnector{})
|
|
|
|
require.NoError(t, c.ACTION(ActionException{
|
|
Window: "main",
|
|
Exception: ExceptionInfo{
|
|
Text: "boom",
|
|
URL: "https://example.com/app.js",
|
|
Line: 12,
|
|
Column: 4,
|
|
StackTrace: "Error: boom",
|
|
},
|
|
}))
|
|
|
|
result, handled, err := c.QUERY(QueryExceptions{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
exceptions, _ := result.([]ExceptionInfo)
|
|
require.Len(t, exceptions, 1)
|
|
assert.Equal(t, "boom", exceptions[0].Text)
|
|
assert.Equal(t, 12, exceptions[0].Line)
|
|
}
|
|
|
|
func TestTaskEvaluate_Good(t *testing.T) {
|
|
_, c := newTestService(t, &mockConnector{evalResult: 42})
|
|
result, handled, err := c.PERFORM(TaskEvaluate{Window: "main", Script: "21*2"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, 42, result)
|
|
}
|
|
|
|
func TestTaskClick_Good(t *testing.T) {
|
|
mock := &mockConnector{}
|
|
_, c := newTestService(t, mock)
|
|
_, handled, err := c.PERFORM(TaskClick{Window: "main", Selector: "#btn"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, "#btn", mock.lastClickSel)
|
|
}
|
|
|
|
func TestTaskNavigate_Good(t *testing.T) {
|
|
mock := &mockConnector{}
|
|
_, c := newTestService(t, mock)
|
|
_, handled, err := c.PERFORM(TaskNavigate{Window: "main", URL: "https://example.com"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, "https://example.com", mock.lastNavURL)
|
|
}
|
|
|
|
func TestTaskScreenshot_Good(t *testing.T) {
|
|
mock := &mockConnector{screenshot: []byte{0x89, 0x50, 0x4E, 0x47}}
|
|
_, c := newTestService(t, mock)
|
|
result, handled, err := c.PERFORM(TaskScreenshot{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
sr, ok := result.(ScreenshotResult)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "image/png", sr.MimeType)
|
|
assert.NotEmpty(t, sr.Base64)
|
|
}
|
|
|
|
func TestTaskScreenshotElement_Good(t *testing.T) {
|
|
img := image.NewRGBA(image.Rect(0, 0, 4, 4))
|
|
for y := 0; y < 4; y++ {
|
|
for x := 0; x < 4; x++ {
|
|
img.SetRGBA(x, y, color.RGBA{R: uint8(x * 40), G: uint8(y * 40), B: 200, A: 255})
|
|
}
|
|
}
|
|
var buf bytes.Buffer
|
|
require.NoError(t, png.Encode(&buf, img))
|
|
|
|
mock := &mockConnector{
|
|
screenshot: buf.Bytes(),
|
|
evalFn: func(script string) (any, error) {
|
|
return map[string]any{
|
|
"left": 1.0,
|
|
"top": 1.0,
|
|
"width": 2.0,
|
|
"height": 2.0,
|
|
"devicePixelRatio": 1.0,
|
|
}, nil
|
|
},
|
|
}
|
|
_, c := newTestService(t, mock)
|
|
|
|
result, handled, err := c.PERFORM(TaskScreenshotElement{Window: "main", Selector: "#card"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
sr, ok := result.(ScreenshotResult)
|
|
require.True(t, ok)
|
|
|
|
raw, err := base64.StdEncoding.DecodeString(sr.Base64)
|
|
require.NoError(t, err)
|
|
decoded, err := png.Decode(bytes.NewReader(raw))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, image.Rect(0, 0, 2, 2), decoded.Bounds())
|
|
}
|
|
|
|
func TestTaskClearConsole_Good(t *testing.T) {
|
|
mock := &mockConnector{}
|
|
_, c := newTestService(t, mock)
|
|
_, handled, err := c.PERFORM(TaskClearConsole{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.True(t, mock.consoleClearCalled)
|
|
}
|
|
|
|
func TestTaskDevTools_Good(t *testing.T) {
|
|
_, c := newTestService(t, &mockConnector{})
|
|
_, _, err := c.PERFORM(window.TaskOpenWindow{Opts: []window.WindowOption{window.WithName("main")}})
|
|
require.NoError(t, err)
|
|
_, handled, err := c.PERFORM(TaskOpenDevTools{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
_, handled, err = c.PERFORM(TaskCloseDevTools{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
}
|
|
|
|
func TestDiagnosticsQueries_Good(t *testing.T) {
|
|
mock := &mockConnector{
|
|
evalFn: func(script string) (any, error) {
|
|
switch {
|
|
case strings.Contains(script, "getComputedStyle"):
|
|
return map[string]any{"color": "rgb(1, 2, 3)"}, nil
|
|
case strings.Contains(script, "performance.getEntriesByType(\"navigation\")"):
|
|
return map[string]any{
|
|
"navigationStart": 1.0,
|
|
"domContentLoaded": 2.0,
|
|
"loadEventEnd": 3.0,
|
|
"firstPaint": 4.0,
|
|
"firstContentfulPaint": 5.0,
|
|
"usedJSHeapSize": 6.0,
|
|
"totalJSHeapSize": 7.0,
|
|
}, nil
|
|
case strings.Contains(script, "performance.getEntriesByType(\"resource\")"):
|
|
return []any{
|
|
map[string]any{"name": "app.js", "entryType": "resource", "initiatorType": "script"},
|
|
}, nil
|
|
case strings.Contains(script, "window.__coreNetworkLog"):
|
|
return []any{
|
|
map[string]any{"url": "https://example.com", "method": "GET", "status": 200, "resource": "fetch"},
|
|
}, nil
|
|
default:
|
|
return nil, nil
|
|
}
|
|
},
|
|
}
|
|
_, c := newTestService(t, mock)
|
|
|
|
style, handled, err := c.QUERY(QueryComputedStyle{Window: "main", Selector: "#app"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, "rgb(1, 2, 3)", style.(map[string]string)["color"])
|
|
|
|
perf, handled, err := c.QUERY(QueryPerformance{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Equal(t, 1.0, perf.(PerformanceMetrics).NavigationStart)
|
|
|
|
resources, handled, err := c.QUERY(QueryResources{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Len(t, resources.([]ResourceEntry), 1)
|
|
|
|
network, handled, err := c.QUERY(QueryNetwork{Window: "main", Limit: 10})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Len(t, network.([]NetworkEntry), 1)
|
|
}
|
|
|
|
func TestDiagnosticsTasks_Good(t *testing.T) {
|
|
mock := &mockConnector{pdfBytes: []byte("%PDF-1.7")}
|
|
_, c := newTestService(t, mock)
|
|
|
|
_, handled, err := c.PERFORM(TaskHighlight{Window: "main", Selector: "#app", Colour: "#00ff00"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Contains(t, mock.lastEvalScript, "outline")
|
|
|
|
_, handled, err = c.PERFORM(TaskInjectNetworkLogging{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.Contains(t, mock.lastEvalScript, "__coreNetworkLog")
|
|
|
|
_, handled, err = c.PERFORM(TaskClearNetworkLog{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
|
|
_, handled, err = c.PERFORM(TaskPrint{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
assert.True(t, mock.printCalled)
|
|
|
|
result, handled, err := c.PERFORM(TaskExportPDF{Window: "main"})
|
|
require.NoError(t, err)
|
|
assert.True(t, handled)
|
|
pdf, ok := result.(PDFResult)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "application/pdf", pdf.MimeType)
|
|
assert.NotEmpty(t, pdf.Base64)
|
|
}
|
|
|
|
func TestConnectionCleanup_Good(t *testing.T) {
|
|
mock := &mockConnector{}
|
|
_, c := newTestService(t, mock)
|
|
// Access creates connection
|
|
_, _, _ = c.QUERY(QueryURL{Window: "main"})
|
|
assert.False(t, mock.closed)
|
|
// Window close action triggers cleanup
|
|
_ = c.ACTION(window.ActionWindowClosed{Name: "main"})
|
|
assert.True(t, mock.closed)
|
|
}
|
|
|
|
func TestQueryURL_Bad_NoService(t *testing.T) {
|
|
c, _ := core.New(core.WithServiceLock())
|
|
_, handled, _ := c.QUERY(QueryURL{Window: "main"})
|
|
assert.False(t, handled)
|
|
}
|