From 683fe8f85e1a145f449c698c9492abe73af287f8 Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 17 Apr 2026 18:08:38 +0100 Subject: [PATCH] Add GUI coverage tests --- pkg/browser/service_test.go | 79 +++++++++++++ pkg/dialog/service_test.go | 158 +++++++++++++++++++++++++ pkg/display/api_test.go | 222 ++++++++++++++++++++++++++++++++++++ 3 files changed, 459 insertions(+) diff --git a/pkg/browser/service_test.go b/pkg/browser/service_test.go index a9122ca5..d1235f9c 100644 --- a/pkg/browser/service_test.go +++ b/pkg/browser/service_test.go @@ -4,6 +4,7 @@ package browser import ( "context" core "dappco.re/go/core" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -127,3 +128,81 @@ func TestTaskOpenURL_Bad_NoService(t *testing.T) { )) assert.False(t, r.OK) } + +func TestService_validatedOpenURL_Good(t *testing.T) { + cases := []struct { + name string + raw string + want string + }{ + { + name: "trimmed", + raw: " https://example.com ", + want: "https://example.com", + }, + { + name: "pathAndQuery", + raw: "https://example.com/docs?q=core", + want: "https://example.com/docs?q=core", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := validatedOpenURL(tc.raw) + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestService_validatedOpenURL_Bad(t *testing.T) { + cases := []string{ + "", + " ", + "example.com", + "ftp://example.com", + "https://user:pass@example.com", + } + + for _, raw := range cases { + t.Run(raw, func(t *testing.T) { + got, err := validatedOpenURL(raw) + require.Error(t, err) + assert.Empty(t, got) + }) + } +} + +func TestService_validatedOpenURL_Ugly(t *testing.T) { + got, err := validatedOpenURL("https://example.com/\x00") + require.Error(t, err) + assert.Empty(t, got) +} + +func TestService_validatedOpenFilePath_Good(t *testing.T) { + got, err := validatedOpenFilePath("/tmp/../tmp/report.txt") + require.NoError(t, err) + assert.Equal(t, filepath.Clean("/tmp/report.txt"), got) +} + +func TestService_validatedOpenFilePath_Bad(t *testing.T) { + cases := []string{ + "", + "relative/report.txt", + } + + for _, raw := range cases { + t.Run(raw, func(t *testing.T) { + got, err := validatedOpenFilePath(raw) + require.Error(t, err) + assert.Empty(t, got) + }) + } +} + +func TestService_validatedOpenFilePath_Ugly(t *testing.T) { + got, err := validatedOpenFilePath("/tmp/\x00report.txt") + require.Error(t, err) + assert.Empty(t, got) +} diff --git a/pkg/dialog/service_test.go b/pkg/dialog/service_test.go index 78ebddaa..001954a6 100644 --- a/pkg/dialog/service_test.go +++ b/pkg/dialog/service_test.go @@ -3,9 +3,12 @@ package dialog import ( "context" + "strings" "testing" core "dappco.re/go/core" + "forge.lthn.ai/core/gui/pkg/webview" + "forge.lthn.ai/core/gui/pkg/window" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -293,6 +296,42 @@ func TestService_TaskError_Good(t *testing.T) { assert.Equal(t, "could not write file: permission denied", mock.lastMsgOpts.Message) } +func TestService_TaskPrompt_Good(t *testing.T) { + _, c := newTestService(t) + + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case window.QueryWindowList: + return core.Result{Value: []window.WindowInfo{ + {Name: "editor", Focused: true}, + }, OK: true} + default: + return core.Result{} + } + }) + + var script string + c.Action("gui.webview.eval", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(webview.TaskEvaluate) + script = task.Script + assert.Equal(t, "editor", task.Window) + return core.Result{Value: "draft", OK: true} + }) + + r := taskRun(c, "dialog.prompt", TaskPrompt{ + Title: "Rename", + Message: "Enter a new name", + DefaultValue: "draft", + }) + require.True(t, r.OK) + result := r.Value.(PromptResult) + assert.Equal(t, "draft", result.Value) + assert.True(t, result.Confirmed) + assert.Contains(t, script, "window.prompt(") + assert.Contains(t, script, "Rename") + assert.Contains(t, script, "Enter a new name") +} + // --- Bad path tests --- func TestService_TaskOpenFile_Bad(t *testing.T) { @@ -388,3 +427,122 @@ func TestService_UnknownTask_Ugly(t *testing.T) { r := c.Action("dialog.nonexistent").Run(context.Background(), core.NewOptions()) assert.False(t, r.OK) } + +func TestService_promptOptionsFrom_Good(t *testing.T) { + got, err := promptOptionsFrom(core.NewOptions( + core.Option{Key: "task", Value: TaskPrompt{ + Title: "Rename", + Message: "Enter a new name", + DefaultValue: "draft", + }}, + )) + require.NoError(t, err) + assert.Equal(t, TaskPrompt{ + Title: "Rename", + Message: "Enter a new name", + DefaultValue: "draft", + }, got) +} + +func TestService_promptOptionsFrom_Bad(t *testing.T) { + got, err := promptOptionsFrom(core.NewOptions( + core.Option{Key: "title", Value: "Rename"}, + core.Option{Key: "message", Value: "Enter a new name"}, + core.Option{Key: "defaultValue", Value: "draft"}, + )) + require.NoError(t, err) + assert.Equal(t, TaskPrompt{ + Title: "Rename", + Message: "Enter a new name", + DefaultValue: "draft", + }, got) +} + +func TestService_promptOptionsFrom_Ugly(t *testing.T) { + got, err := promptOptionsFrom(core.NewOptions()) + require.NoError(t, err) + assert.Zero(t, got) +} + +func TestService_promptWindowName_Good(t *testing.T) { + _, c := newTestService(t) + svc := core.MustServiceFor[*Service](c, "dialog") + + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case window.QueryWindowList: + return core.Result{Value: []window.WindowInfo{ + {Name: "editor"}, + {Name: "preview", Focused: true}, + }, OK: true} + default: + return core.Result{} + } + }) + + got, err := svc.promptWindowName() + require.NoError(t, err) + assert.Equal(t, "preview", got) +} + +func TestService_promptWindowName_Bad(t *testing.T) { + _, c := newTestService(t) + svc := core.MustServiceFor[*Service](c, "dialog") + + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case window.QueryWindowList: + return core.Result{Value: "unexpected", OK: true} + default: + return core.Result{} + } + }) + + got, err := svc.promptWindowName() + require.Error(t, err) + assert.Empty(t, got) +} + +func TestService_promptWindowName_Ugly(t *testing.T) { + _, c := newTestService(t) + svc := core.MustServiceFor[*Service](c, "dialog") + + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case window.QueryWindowList: + return core.Result{Value: []window.WindowInfo{ + {Name: "editor"}, + {Name: "terminal"}, + }, OK: true} + default: + return core.Result{} + } + }) + + got, err := svc.promptWindowName() + require.NoError(t, err) + assert.Equal(t, "editor", got) +} + +func TestService_promptScript_Good(t *testing.T) { + script := promptScript("Rename", "Enter a new name", "draft") + assert.Contains(t, script, "window.prompt(") + assert.Contains(t, script, "Rename") + assert.Contains(t, script, "Enter a new name") + assert.Contains(t, script, "draft") +} + +func TestService_promptScript_Bad(t *testing.T) { + script := promptScript("Rename", "", "") + assert.Contains(t, script, "window.prompt(") + assert.Contains(t, script, "Rename") + assert.NotContains(t, script, "Enter a new name") +} + +func TestService_promptScript_Ugly(t *testing.T) { + script := promptScript("", "Line 1\nLine 2", "\"quoted\"") + assert.Contains(t, script, "Line 1") + assert.Contains(t, script, "Line 2") + assert.Contains(t, script, "quoted") + assert.True(t, strings.Contains(script, "window.prompt(")) +} diff --git a/pkg/display/api_test.go b/pkg/display/api_test.go index 2e803248..149a5e6b 100644 --- a/pkg/display/api_test.go +++ b/pkg/display/api_test.go @@ -1,10 +1,12 @@ package display import ( + "bytes" "context" "testing" core "dappco.re/go/core" + "forge.lthn.ai/core/gui/pkg/clipboard" "forge.lthn.ai/core/gui/pkg/dialog" "forge.lthn.ai/core/gui/pkg/environment" "forge.lthn.ai/core/gui/pkg/screen" @@ -309,3 +311,223 @@ func TestDisplayAPI_GetTheme_Ugly(t *testing.T) { assert.Nil(t, svc.GetTheme()) assert.Empty(t, svc.GetSystemTheme()) } + +func TestDisplayAPI_SaveFileDialog_Good(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.saveFile", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(dialog.TaskSaveFile) + assert.Equal(t, "Export", task.Options.Title) + assert.Equal(t, "/tmp", task.Options.Directory) + assert.Equal(t, "data.json", task.Options.Filename) + require.Len(t, task.Options.Filters, 1) + assert.Equal(t, "JSON", task.Options.Filters[0].DisplayName) + return core.Result{Value: "/exports/data.json", OK: true} + }) + + path, err := svc.SaveFileDialog(SaveFileOptions{ + Title: "Export", + DefaultDirectory: "/tmp", + DefaultFilename: "data.json", + Filters: []FileFilter{{DisplayName: "JSON", Pattern: "*.json"}}, + }) + + require.NoError(t, err) + assert.Equal(t, "/exports/data.json", path) +} + +func TestDisplayAPI_SaveFileDialog_Bad(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.saveFile", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: assert.AnError, OK: false} + }) + + path, err := svc.SaveFileDialog(SaveFileOptions{}) + + require.Error(t, err) + assert.Empty(t, path) +} + +func TestDisplayAPI_SaveFileDialog_Ugly(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.saveFile", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: 42, OK: true} + }) + + path, err := svc.SaveFileDialog(SaveFileOptions{}) + + require.Error(t, err) + assert.Empty(t, path) +} + +func TestDisplayAPI_OpenDirectoryDialog_Good(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.openDirectory", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(dialog.TaskOpenDirectory) + assert.Equal(t, "Choose", task.Options.Title) + assert.Equal(t, "/var", task.Options.Directory) + return core.Result{Value: "/var/data", OK: true} + }) + + path, err := svc.OpenDirectoryDialog(OpenDirectoryOptions{ + Title: "Choose", + DefaultDirectory: "/var", + }) + + require.NoError(t, err) + assert.Equal(t, "/var/data", path) +} + +func TestDisplayAPI_OpenDirectoryDialog_Bad(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.openDirectory", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: assert.AnError, OK: false} + }) + + path, err := svc.OpenDirectoryDialog(OpenDirectoryOptions{}) + + require.Error(t, err) + assert.Empty(t, path) +} + +func TestDisplayAPI_OpenDirectoryDialog_Ugly(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.openDirectory", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: 42, OK: true} + }) + + path, err := svc.OpenDirectoryDialog(OpenDirectoryOptions{}) + + require.Error(t, err) + assert.Empty(t, path) +} + +func TestDisplayAPI_PromptDialog_Good(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.prompt", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(dialog.TaskPrompt) + assert.Equal(t, "Rename", task.Title) + assert.Equal(t, "Enter a new name", task.Message) + return core.Result{Value: dialog.PromptResult{Value: "draft", Confirmed: true}, OK: true} + }) + + value, confirmed, err := svc.PromptDialog("Rename", "Enter a new name") + + require.NoError(t, err) + assert.True(t, confirmed) + assert.Equal(t, "draft", value) +} + +func TestDisplayAPI_PromptDialog_Bad(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.prompt", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: assert.AnError, OK: false} + }) + + value, confirmed, err := svc.PromptDialog("Rename", "Enter a new name") + + require.Error(t, err) + assert.False(t, confirmed) + assert.Empty(t, value) +} + +func TestDisplayAPI_PromptDialog_Ugly(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("dialog.prompt", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: 42, OK: true} + }) + + value, confirmed, err := svc.PromptDialog("Rename", "Enter a new name") + + require.Error(t, err) + assert.False(t, confirmed) + assert.Empty(t, value) +} + +func TestDisplayAPI_ReadClipboardImage_Good(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + payload := []byte{1, 2, 3} + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case clipboard.QueryImage: + return core.Result{Value: clipboard.ImageContent{Data: payload, HasImage: true}, OK: true} + default: + return core.Result{} + } + }) + + got, err := svc.ReadClipboardImage() + + require.NoError(t, err) + require.Equal(t, []byte{1, 2, 3}, got) + payload[0] = 9 + assert.Equal(t, byte(1), got[0]) +} + +func TestDisplayAPI_ReadClipboardImage_Bad(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case clipboard.QueryImage: + return core.Result{Value: clipboard.ImageContent{HasImage: false}, OK: true} + default: + return core.Result{} + } + }) + + got, err := svc.ReadClipboardImage() + + require.NoError(t, err) + assert.Nil(t, got) +} + +func TestDisplayAPI_ReadClipboardImage_Ugly(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case clipboard.QueryImage: + return core.Result{Value: "unexpected", OK: true} + default: + return core.Result{} + } + }) + + got, err := svc.ReadClipboardImage() + + require.Error(t, err) + assert.Nil(t, got) +} + +func TestDisplayAPI_WriteClipboardImage_Good(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + var got []byte + c.Action("clipboard.setImage", func(_ context.Context, opts core.Options) core.Result { + got = append([]byte(nil), opts.Get("data").Value.([]byte)...) + return core.Result{OK: true} + }) + + input := []byte{4, 5, 6} + err := svc.WriteClipboardImage(input) + + require.NoError(t, err) + input[0] = 9 + assert.True(t, bytes.Equal([]byte{4, 5, 6}, got)) +} + +func TestDisplayAPI_WriteClipboardImage_Bad(t *testing.T) { + svc, _ := newTestDisplayAPIService(t) + + err := svc.WriteClipboardImage(nil) + + require.Error(t, err) +} + +func TestDisplayAPI_WriteClipboardImage_Ugly(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action("clipboard.setImage", func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: assert.AnError, OK: false} + }) + + err := svc.WriteClipboardImage([]byte{1}) + + require.Error(t, err) +}