diff --git a/.core/TODO.md b/.core/TODO.md index 1518bad2..e69de29b 100644 --- a/.core/TODO.md +++ b/.core/TODO.md @@ -1,2 +0,0 @@ -- @coverage pkg/display/api.go:286 — Tray, clipboard, notification, and theme wrapper methods still need direct unit coverage for their remaining branches. -- @coverage pkg/display/display.go:1469 — Layout delegation wrappers for delete/tile/snap/stack/workflow/screen-space/pair-arrangement still need unit coverage. diff --git a/pkg/display/api_wrappers_test.go b/pkg/display/api_wrappers_test.go new file mode 100644 index 00000000..2a66f3e3 --- /dev/null +++ b/pkg/display/api_wrappers_test.go @@ -0,0 +1,371 @@ +package display + +import ( + "context" + "testing" + + core "dappco.re/go/core" + "forge.lthn.ai/core/gui/pkg/clipboard" + "forge.lthn.ai/core/gui/pkg/environment" + "forge.lthn.ai/core/gui/pkg/notification" + "forge.lthn.ai/core/gui/pkg/systray" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type errorOnlyWrapperCase struct { + name string + action string + call func(*Service) error + setupGood func(*testing.T, *core.Core) +} + +func runErrorOnlyWrapperCase(t *testing.T, tc errorOnlyWrapperCase) { + t.Helper() + + t.Run("good", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + if tc.setupGood != nil { + tc.setupGood(t, c) + } + require.NoError(t, tc.call(svc)) + }) + + t.Run("bad", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action(tc.action, func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: assert.AnError, OK: false} + }) + + err := tc.call(svc) + require.Error(t, err) + assert.Equal(t, assert.AnError, err) + }) + + t.Run("ugly", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action(tc.action, func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: "unexpected", OK: false} + }) + + err := tc.call(svc) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.action) + }) +} + +func TestDisplayAPI_TrayWrappers(t *testing.T) { + cases := []errorOnlyWrapperCase{ + { + name: "SetTrayTooltip", + action: "systray.setTooltip", + call: func(svc *Service) error { + return svc.SetTrayTooltip("Helper tooltip") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("systray.setTooltip", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(systray.TaskSetTrayTooltip) + assert.Equal(t, "Helper tooltip", task.Tooltip) + return core.Result{OK: true} + }) + }, + }, + { + name: "SetTrayLabel", + action: "systray.setLabel", + call: func(svc *Service) error { + return svc.SetTrayLabel("Launcher") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("systray.setLabel", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(systray.TaskSetTrayLabel) + assert.Equal(t, "Launcher", task.Label) + return core.Result{OK: true} + }) + }, + }, + { + name: "SetTrayMenu", + action: "systray.setMenu", + call: func(svc *Service) error { + return svc.SetTrayMenu([]TrayMenuItem{ + {Label: "Open", ActionID: "open"}, + { + Label: "More", + ActionID: "more", + Children: []TrayMenuItem{{Label: "Nested", ActionID: "nested"}}, + }, + }) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("systray.setMenu", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(systray.TaskSetTrayMenu) + require.Len(t, task.Items, 2) + assert.Equal(t, "Open", task.Items[0].Label) + require.Len(t, task.Items[1].Submenu, 1) + assert.Equal(t, "nested", task.Items[1].Submenu[0].ActionID) + return core.Result{OK: true} + }) + }, + }, + { + name: "ShowTrayMessage", + action: "systray.showMessage", + call: func(svc *Service) error { + return svc.ShowTrayMessage("Status", "Task complete") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("systray.showMessage", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(systray.TaskShowMessage) + assert.Equal(t, "Status", task.Title) + assert.Equal(t, "Task complete", task.Message) + return core.Result{OK: true} + }) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + runErrorOnlyWrapperCase(t, tc) + }) + } +} + +func TestDisplayAPI_ClipboardWrappers(t *testing.T) { + t.Run("ClearClipboard", func(t *testing.T) { + runErrorOnlyWrapperCase(t, errorOnlyWrapperCase{ + name: "ClearClipboard", + action: "clipboard.clear", + call: func(svc *Service) error { + return svc.ClearClipboard() + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("clipboard.clear", func(_ context.Context, opts core.Options) core.Result { + assert.Equal(t, 0, opts.Len()) + return core.Result{OK: true} + }) + }, + }) + }) + + t.Run("HasClipboard", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case clipboard.QueryText: + return core.Result{ + Value: clipboard.ClipboardContent{ + Text: "present", + HasContent: true, + }, + OK: true, + } + default: + return core.Result{} + } + }) + assert.True(t, svc.HasClipboard()) + + svc, c = newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case clipboard.QueryText: + return core.Result{ + Value: clipboard.ClipboardContent{ + Text: "", + HasContent: false, + }, + OK: true, + } + default: + return core.Result{} + } + }) + assert.False(t, svc.HasClipboard()) + + svc, c = newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case clipboard.QueryText: + return core.Result{OK: false} + default: + return core.Result{} + } + }) + assert.False(t, svc.HasClipboard()) + }) +} + +func TestDisplayAPI_NotificationWrappers(t *testing.T) { + cases := []errorOnlyWrapperCase{ + { + name: "ShowNotification", + action: "notification.send", + call: func(svc *Service) error { + return svc.ShowNotification(NotificationOptions{ + ID: "alert-1", + Title: "Deploy", + Message: "Done", + Subtitle: "CI", + }) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("notification.send", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(notification.TaskSend) + assert.Equal(t, notification.NotificationOptions{ + ID: "alert-1", + Title: "Deploy", + Message: "Done", + Subtitle: "CI", + }, task.Options) + return core.Result{OK: true} + }) + }, + }, + { + name: "ShowInfoNotification", + action: "notification.send", + call: func(svc *Service) error { + return svc.ShowInfoNotification("Info", "Ready") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("notification.send", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(notification.TaskSend) + assert.Equal(t, notification.NotificationOptions{ + Title: "Info", + Message: "Ready", + }, task.Options) + return core.Result{OK: true} + }) + }, + }, + { + name: "ShowWarningNotification", + action: "notification.send", + call: func(svc *Service) error { + return svc.ShowWarningNotification("Warn", "Careful") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("notification.send", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(notification.TaskSend) + assert.Equal(t, notification.NotificationOptions{ + Title: "Warn", + Message: "Careful", + Severity: notification.SeverityWarning, + }, task.Options) + return core.Result{OK: true} + }) + }, + }, + { + name: "ShowErrorNotification", + action: "notification.send", + call: func(svc *Service) error { + return svc.ShowErrorNotification("Error", "Failed") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("notification.send", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(notification.TaskSend) + assert.Equal(t, notification.NotificationOptions{ + Title: "Error", + Message: "Failed", + Severity: notification.SeverityError, + }, task.Options) + return core.Result{OK: true} + }) + }, + }, + { + name: "ClearNotifications", + action: "notification.clear", + call: func(svc *Service) error { + return svc.ClearNotifications() + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("notification.clear", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(notification.TaskClear) + assert.Empty(t, task.ID) + return core.Result{OK: true} + }) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + runErrorOnlyWrapperCase(t, tc) + }) + } +} + +func TestDisplayAPI_ThemeWrapper(t *testing.T) { + runErrorOnlyWrapperCase(t, errorOnlyWrapperCase{ + name: "SetTheme", + action: "environment.setTheme", + call: func(svc *Service) error { + return svc.SetTheme("system") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("environment.setTheme", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(environment.TaskSetTheme) + assert.Equal(t, "system", task.Theme) + return core.Result{OK: true} + }) + }, + }) +} + +func TestDisplayAPI_GetTrayInfo(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case systray.QueryInfo: + return core.Result{ + Value: map[string]any{ + "tooltip": "Ready", + }, + OK: true, + } + default: + return core.Result{} + } + }) + + info := svc.GetTrayInfo() + require.NotNil(t, info) + assert.Equal(t, "Ready", info["tooltip"]) + + svc, c = newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case systray.QueryInfo: + return core.Result{Value: "unexpected", OK: true} + default: + return core.Result{} + } + }) + assert.Nil(t, svc.GetTrayInfo()) + + svc, c = newTestDisplayAPIService(t) + c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { + switch q.(type) { + case systray.QueryInfo: + return core.Result{OK: false} + default: + return core.Result{} + } + }) + assert.Nil(t, svc.GetTrayInfo()) +} diff --git a/pkg/display/display_layout_wrappers_test.go b/pkg/display/display_layout_wrappers_test.go new file mode 100644 index 00000000..82d0d940 --- /dev/null +++ b/pkg/display/display_layout_wrappers_test.go @@ -0,0 +1,279 @@ +package display + +import ( + "context" + "testing" + + core "dappco.re/go/core" + "forge.lthn.ai/core/gui/pkg/window" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type layoutResultWrapperCase struct { + name string + action string + zero any + call func(*Service) (any, error) + setupGood func(*testing.T, *core.Core) + wantGood func(*testing.T, any) +} + +func runLayoutResultWrapperCase(t *testing.T, tc layoutResultWrapperCase) { + t.Helper() + + t.Run("good", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + if tc.setupGood != nil { + tc.setupGood(t, c) + } + + got, err := tc.call(svc) + require.NoError(t, err) + tc.wantGood(t, got) + }) + + t.Run("bad", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action(tc.action, func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: assert.AnError, OK: false} + }) + + got, err := tc.call(svc) + require.Error(t, err) + assert.Equal(t, tc.zero, got) + assert.Equal(t, assert.AnError, err) + }) + + t.Run("ugly-action", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action(tc.action, func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: "unexpected", OK: false} + }) + + got, err := tc.call(svc) + require.Error(t, err) + assert.Equal(t, tc.zero, got) + assert.Contains(t, err.Error(), tc.action) + }) + + t.Run("ugly-type", func(t *testing.T) { + svc, c := newTestDisplayAPIService(t) + c.Action(tc.action, func(_ context.Context, _ core.Options) core.Result { + return core.Result{Value: "unexpected", OK: true} + }) + + got, err := tc.call(svc) + require.Error(t, err) + assert.Equal(t, tc.zero, got) + assert.Contains(t, err.Error(), "unexpected result type") + }) +} + +func TestDisplay_LayoutDelegationWrappers(t *testing.T) { + errorCases := []errorOnlyWrapperCase{ + { + name: "DeleteLayout", + action: "window.deleteLayout", + call: func(svc *Service) error { + return svc.DeleteLayout("development") + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.deleteLayout", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskDeleteLayout) + assert.Equal(t, "development", task.Name) + return core.Result{OK: true} + }) + }, + }, + { + name: "TileWindows", + action: "window.tileWindows", + call: func(svc *Service) error { + return svc.TileWindows(window.TileModeGrid, []string{"editor", "terminal"}) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.tileWindows", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskTileWindows) + assert.Equal(t, window.TileModeGrid.String(), task.Mode) + assert.Equal(t, []string{"editor", "terminal"}, task.Windows) + return core.Result{OK: true} + }) + }, + }, + { + name: "SnapWindow", + action: "window.snapWindow", + call: func(svc *Service) error { + return svc.SnapWindow("preview", window.SnapCenter) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.snapWindow", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskSnapWindow) + assert.Equal(t, "preview", task.Name) + assert.Equal(t, window.SnapCenter.String(), task.Position) + return core.Result{OK: true} + }) + }, + }, + { + name: "StackWindows", + action: "window.stackWindows", + call: func(svc *Service) error { + return svc.StackWindows([]string{"editor", "preview"}, 24, 18) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.stackWindows", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskStackWindows) + assert.Equal(t, []string{"editor", "preview"}, task.Windows) + assert.Equal(t, 24, task.OffsetX) + assert.Equal(t, 18, task.OffsetY) + return core.Result{OK: true} + }) + }, + }, + { + name: "ApplyWorkflowLayout", + action: "window.applyWorkflow", + call: func(svc *Service) error { + return svc.ApplyWorkflowLayout(window.WorkflowCoding) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.applyWorkflow", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskApplyWorkflow) + assert.Equal(t, window.WorkflowCoding.String(), task.Workflow) + return core.Result{OK: true} + }) + }, + }, + } + + for _, tc := range errorCases { + t.Run(tc.name, func(t *testing.T) { + runErrorOnlyWrapperCase(t, tc) + }) + } + + runLayoutResultWrapperCase(t, layoutResultWrapperCase{ + name: "LayoutBesideEditor", + action: "window.layoutBesideEditor", + zero: window.LayoutBesideEditorResult{}, + call: func(svc *Service) (any, error) { + return svc.LayoutBesideEditor("preview", "code", "right", 0.62) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.layoutBesideEditor", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskLayoutBesideEditor) + assert.Equal(t, "preview", task.Name) + assert.Equal(t, "code", task.Editor) + assert.Equal(t, "right", task.Side) + assert.InDelta(t, 0.62, task.Ratio, 0.0001) + return core.Result{ + Value: window.LayoutBesideEditorResult{ + Editor: "code", + EditorBounds: window.WindowBounds{ + X: 10, Y: 20, Width: 640, Height: 800, + }, + WindowBounds: window.WindowBounds{ + X: 650, Y: 20, Width: 640, Height: 800, + }, + Side: "right", + ScreenID: "screen-1", + }, + OK: true, + } + }) + }, + wantGood: func(t *testing.T, got any) { + t.Helper() + result := got.(window.LayoutBesideEditorResult) + assert.Equal(t, "code", result.Editor) + assert.Equal(t, "right", result.Side) + assert.Equal(t, "screen-1", result.ScreenID) + }, + }) + + runLayoutResultWrapperCase(t, layoutResultWrapperCase{ + name: "FindScreenSpace", + action: "window.findSpace", + zero: window.ScreenSpace{}, + call: func(svc *Service) (any, error) { + return svc.FindScreenSpace("screen-1", 800, 600, 24) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.findSpace", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskScreenFindSpace) + assert.Equal(t, "screen-1", task.ScreenID) + assert.Equal(t, 800, task.Width) + assert.Equal(t, 600, task.Height) + assert.Equal(t, 24, task.Padding) + return core.Result{ + Value: window.ScreenSpace{ + ScreenID: "screen-1", + X: 100, + Y: 120, + Width: 800, + Height: 600, + }, + OK: true, + } + }) + }, + wantGood: func(t *testing.T, got any) { + t.Helper() + space := got.(window.ScreenSpace) + assert.Equal(t, "screen-1", space.ScreenID) + assert.Equal(t, 100, space.X) + assert.Equal(t, 120, space.Y) + assert.Equal(t, 800, space.Width) + assert.Equal(t, 600, space.Height) + }, + }) + + runLayoutResultWrapperCase(t, layoutResultWrapperCase{ + name: "ArrangeWindowPair", + action: "window.arrangePair", + zero: window.PairArrangement{}, + call: func(svc *Service) (any, error) { + return svc.ArrangeWindowPair("editor", "preview", "screen-1", 0.55) + }, + setupGood: func(t *testing.T, c *core.Core) { + t.Helper() + c.Action("window.arrangePair", func(_ context.Context, opts core.Options) core.Result { + task := opts.Get("task").Value.(window.TaskWindowArrangePair) + assert.Equal(t, "editor", task.Primary) + assert.Equal(t, "preview", task.Secondary) + assert.Equal(t, "screen-1", task.ScreenID) + assert.InDelta(t, 0.55, task.Ratio, 0.0001) + return core.Result{ + Value: window.PairArrangement{ + Primary: window.WindowBounds{ + X: 0, Y: 0, Width: 800, Height: 600, + }, + Secondary: window.WindowBounds{ + X: 800, Y: 0, Width: 800, Height: 600, + }, + Orientation: "horizontal", + ScreenID: "screen-1", + }, + OK: true, + } + }) + }, + wantGood: func(t *testing.T, got any) { + t.Helper() + arrangement := got.(window.PairArrangement) + assert.Equal(t, "horizontal", arrangement.Orientation) + assert.Equal(t, "screen-1", arrangement.ScreenID) + assert.Equal(t, 800, arrangement.Primary.Width) + assert.Equal(t, 800, arrangement.Secondary.Width) + }, + }) +}