diff --git a/pkg/display/display.go b/pkg/display/display.go index c952d2b..e3aad27 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -16,6 +16,9 @@ import ( // Options holds configuration for the display service. type Options struct{} +// WindowInfo is an alias for window.WindowInfo (backward compatibility). +type WindowInfo = window.WindowInfo + // Service manages windowing, dialogs, and other visual elements. // It orchestrates sub-services (window, systray, menu) via IPC and bridges // IPC actions to WebSocket events for TypeScript apps. @@ -25,9 +28,6 @@ type Service struct { app App config Options configData map[string]map[string]any - windows *window.Manager - tray *systray.Manager - menus *menu.Manager notifier *notifications.NotificationService events *WSEventManager } @@ -80,19 +80,11 @@ func (s *Service) OnStartup(ctx context.Context) error { // HandleIPCEvents is auto-discovered and registered by core.WithService. // It bridges sub-service IPC actions to WebSocket events for TS apps. func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error { - if s.events == nil && s.wailsApp != nil { - return nil // No WS event manager (testing without Wails) - } - switch m := msg.(type) { case core.ActionServiceStartup: // All services have completed OnStartup — safe to PERFORM on sub-services - if s.menus != nil { - s.buildMenu() - } - if s.tray != nil { - s.setupTray() - } + s.buildMenu() + s.setupTray() case window.ActionWindowOpened: if s.events != nil { s.events.Emit(Event{Type: EventWindowCreate, Window: m.Name, @@ -140,22 +132,12 @@ func (s *Service) handleTrayAction(actionID string) { switch actionID { case "open-desktop": // Show all windows - if s.windows != nil { - for _, name := range s.windows.List() { - if pw, ok := s.windows.Get(name); ok { - pw.Show() - } - } + infos := s.ListWindowInfos() + for _, info := range infos { + _, _, _ = s.Core().PERFORM(window.TaskFocus{Name: info.Name}) } case "close-desktop": // Hide all windows — future: add TaskHideWindow - if s.windows != nil { - for _, name := range s.windows.List() { - if pw, ok := s.windows.Get(name); ok { - pw.Hide() - } - } - } case "env-info": if s.app != nil { s.ShowEnvironmentDialog() @@ -207,114 +189,101 @@ func (s *Service) handleConfigTask(c *core.Core, t core.Task) (any, bool, error) } } -// --- Window Management (delegates to window.Manager) --- +// --- Service accessors --- -// OpenWindow creates a new window with the given options. -func (s *Service) OpenWindow(opts ...window.WindowOption) error { - pw, err := s.windows.Open(opts...) +// windowService returns the window service from Core, or nil if not registered. +func (s *Service) windowService() *window.Service { + svc, err := core.ServiceFor[*window.Service](s.Core(), "window") if err != nil { + return nil + } + return svc +} + +// --- Window Management (delegates via IPC) --- + +// OpenWindow creates a new window via IPC. +func (s *Service) OpenWindow(opts ...window.WindowOption) error { + _, _, err := s.Core().PERFORM(window.TaskOpenWindow{Opts: opts}) + return err +} + +// GetWindowInfo returns information about a window via IPC. +func (s *Service) GetWindowInfo(name string) (*window.WindowInfo, error) { + result, handled, err := s.Core().QUERY(window.QueryWindowByName{Name: name}) + if err != nil { + return nil, err + } + if !handled { + return nil, fmt.Errorf("window service not available") + } + info, _ := result.(*window.WindowInfo) + return info, nil +} + +// ListWindowInfos returns information about all tracked windows via IPC. +func (s *Service) ListWindowInfos() []window.WindowInfo { + result, handled, _ := s.Core().QUERY(window.QueryWindowList{}) + if !handled { + return nil + } + list, _ := result.([]window.WindowInfo) + return list +} + +// SetWindowPosition moves a window via IPC. +func (s *Service) SetWindowPosition(name string, x, y int) error { + _, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y}) + return err +} + +// SetWindowSize resizes a window via IPC. +func (s *Service) SetWindowSize(name string, width, height int) error { + _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, W: width, H: height}) + return err +} + +// SetWindowBounds sets both position and size of a window via IPC. +func (s *Service) SetWindowBounds(name string, x, y, width, height int) error { + if _, _, err := s.Core().PERFORM(window.TaskSetPosition{Name: name, X: x, Y: y}); err != nil { return err } - s.trackWindow(pw) - return nil + _, _, err := s.Core().PERFORM(window.TaskSetSize{Name: name, W: width, H: height}) + return err } -// trackWindow attaches event listeners for state persistence and WebSocket events. -func (s *Service) trackWindow(pw window.PlatformWindow) { - if s.events != nil { - s.events.EmitWindowEvent(EventWindowCreate, pw.Name(), map[string]any{ - "name": pw.Name(), - }) - s.events.AttachWindowListeners(pw) - } -} - -// GetWindowInfo returns information about a window by name. -func (s *Service) GetWindowInfo(name string) (*WindowInfo, error) { - pw, ok := s.windows.Get(name) - if !ok { - return nil, fmt.Errorf("window not found: %s", name) - } - x, y := pw.Position() - w, h := pw.Size() - return &WindowInfo{ - Name: name, - X: x, - Y: y, - Width: w, - Height: h, - Maximized: pw.IsMaximised(), - }, nil -} - -// ListWindowInfos returns information about all tracked windows. -func (s *Service) ListWindowInfos() []WindowInfo { - names := s.windows.List() - result := make([]WindowInfo, 0, len(names)) - for _, name := range names { - if pw, ok := s.windows.Get(name); ok { - x, y := pw.Position() - w, h := pw.Size() - result = append(result, WindowInfo{ - Name: name, - X: x, - Y: y, - Width: w, - Height: h, - Maximized: pw.IsMaximised(), - }) - } - } - return result -} - -// SetWindowPosition moves a window to the specified position. -func (s *Service) SetWindowPosition(name string, x, y int) error { - pw, ok := s.windows.Get(name) - if !ok { - return fmt.Errorf("window not found: %s", name) - } - pw.SetPosition(x, y) - s.windows.State().UpdatePosition(name, x, y) - return nil -} - -// SetWindowSize resizes a window. -func (s *Service) SetWindowSize(name string, width, height int) error { - pw, ok := s.windows.Get(name) - if !ok { - return fmt.Errorf("window not found: %s", name) - } - pw.SetSize(width, height) - s.windows.State().UpdateSize(name, width, height) - return nil -} - -// SetWindowBounds sets both position and size of a window. -func (s *Service) SetWindowBounds(name string, x, y, width, height int) error { - pw, ok := s.windows.Get(name) - if !ok { - return fmt.Errorf("window not found: %s", name) - } - pw.SetPosition(x, y) - pw.SetSize(width, height) - return nil -} - -// MaximizeWindow maximizes a window. +// MaximizeWindow maximizes a window via IPC. func (s *Service) MaximizeWindow(name string) error { - pw, ok := s.windows.Get(name) - if !ok { - return fmt.Errorf("window not found: %s", name) - } - pw.Maximise() - s.windows.State().UpdateMaximized(name, true) - return nil + _, _, err := s.Core().PERFORM(window.TaskMaximise{Name: name}) + return err +} + +// MinimizeWindow minimizes a window via IPC. +func (s *Service) MinimizeWindow(name string) error { + _, _, err := s.Core().PERFORM(window.TaskMinimise{Name: name}) + return err +} + +// FocusWindow brings a window to the front via IPC. +func (s *Service) FocusWindow(name string) error { + _, _, err := s.Core().PERFORM(window.TaskFocus{Name: name}) + return err +} + +// CloseWindow closes a window via IPC. +func (s *Service) CloseWindow(name string) error { + _, _, err := s.Core().PERFORM(window.TaskCloseWindow{Name: name}) + return err } // RestoreWindow restores a maximized/minimized window. +// Uses direct Manager access (no IPC task for restore yet). func (s *Service) RestoreWindow(name string) error { - pw, ok := s.windows.Get(name) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } @@ -322,41 +291,14 @@ func (s *Service) RestoreWindow(name string) error { return nil } -// MinimizeWindow minimizes a window. -func (s *Service) MinimizeWindow(name string) error { - pw, ok := s.windows.Get(name) - if !ok { - return fmt.Errorf("window not found: %s", name) - } - pw.Minimise() - return nil -} - -// FocusWindow brings a window to the front. -func (s *Service) FocusWindow(name string) error { - pw, ok := s.windows.Get(name) - if !ok { - return fmt.Errorf("window not found: %s", name) - } - pw.Focus() - return nil -} - -// CloseWindow closes a window by name. -func (s *Service) CloseWindow(name string) error { - pw, ok := s.windows.Get(name) - if !ok { - return fmt.Errorf("window not found: %s", name) - } - s.windows.State().CaptureState(pw) - pw.Close() - s.windows.Remove(name) - return nil -} - // SetWindowVisibility shows or hides a window. +// Uses direct Manager access (no IPC task for visibility yet). func (s *Service) SetWindowVisibility(name string, visible bool) error { - pw, ok := s.windows.Get(name) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } @@ -365,8 +307,13 @@ func (s *Service) SetWindowVisibility(name string, visible bool) error { } // SetWindowAlwaysOnTop sets whether a window stays on top. +// Uses direct Manager access (no IPC task for always-on-top yet). func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error { - pw, ok := s.windows.Get(name) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } @@ -375,8 +322,13 @@ func (s *Service) SetWindowAlwaysOnTop(name string, alwaysOnTop bool) error { } // SetWindowTitle changes a window's title. +// Uses direct Manager access (no IPC task for title yet). func (s *Service) SetWindowTitle(name string, title string) error { - pw, ok := s.windows.Get(name) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } @@ -385,8 +337,13 @@ func (s *Service) SetWindowTitle(name string, title string) error { } // SetWindowFullscreen sets a window to fullscreen mode. +// Uses direct Manager access (no IPC task for fullscreen yet). func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error { - pw, ok := s.windows.Get(name) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } @@ -399,8 +356,13 @@ func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error { } // SetWindowBackgroundColour sets the background colour of a window. +// Uses direct Manager access (no IPC task for background colour yet). func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error { - pw, ok := s.windows.Get(name) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + pw, ok := ws.Manager().Get(name) if !ok { return fmt.Errorf("window not found: %s", name) } @@ -410,11 +372,10 @@ func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error // GetFocusedWindow returns the name of the currently focused window. func (s *Service) GetFocusedWindow() string { - for _, name := range s.windows.List() { - if pw, ok := s.windows.Get(name); ok { - if pw.IsFocused() { - return name - } + infos := s.ListWindowInfos() + for _, info := range infos { + if info.Focused { + return info.Name } } return "" @@ -422,45 +383,40 @@ func (s *Service) GetFocusedWindow() string { // GetWindowTitle returns the title of a window by name. func (s *Service) GetWindowTitle(name string) (string, error) { - _, ok := s.windows.Get(name) - if !ok { + info, err := s.GetWindowInfo(name) + if err != nil { + return "", err + } + if info == nil { return "", fmt.Errorf("window not found: %s", name) } - return name, nil // Wails v3 doesn't expose a title getter + return info.Name, nil // Wails v3 doesn't expose a title getter } // ResetWindowState clears saved window positions. func (s *Service) ResetWindowState() error { - if s.windows != nil { - s.windows.State().Clear() + ws := s.windowService() + if ws != nil { + ws.Manager().State().Clear() } return nil } // GetSavedWindowStates returns all saved window states. func (s *Service) GetSavedWindowStates() map[string]window.WindowState { - if s.windows == nil { + ws := s.windowService() + if ws == nil { return nil } result := make(map[string]window.WindowState) - for _, name := range s.windows.State().ListStates() { - if state, ok := s.windows.State().GetState(name); ok { + for _, name := range ws.Manager().State().ListStates() { + if state, ok := ws.Manager().State().GetState(name); ok { result[name] = state } } return result } -// WindowInfo contains information about a window for MCP. -type WindowInfo struct { - Name string `json:"name"` - X int `json:"x"` - Y int `json:"y"` - Width int `json:"width"` - Height int `json:"height"` - Maximized bool `json:"maximized"` -} - // CreateWindowOptions contains options for creating a new window. type CreateWindowOptions struct { Name string `json:"name"` @@ -473,58 +429,57 @@ type CreateWindowOptions struct { } // CreateWindow creates a new window with the specified options. -func (s *Service) CreateWindow(opts CreateWindowOptions) (*WindowInfo, error) { +func (s *Service) CreateWindow(opts CreateWindowOptions) (*window.WindowInfo, error) { if opts.Name == "" { return nil, fmt.Errorf("window name is required") } - err := s.OpenWindow( - window.WithName(opts.Name), - window.WithTitle(opts.Title), - window.WithURL(opts.URL), - window.WithSize(opts.Width, opts.Height), - window.WithPosition(opts.X, opts.Y), - ) + result, _, err := s.Core().PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{ + window.WithName(opts.Name), + window.WithTitle(opts.Title), + window.WithURL(opts.URL), + window.WithSize(opts.Width, opts.Height), + window.WithPosition(opts.X, opts.Y), + }, + }) if err != nil { return nil, err } - return &WindowInfo{ - Name: opts.Name, - X: opts.X, - Y: opts.Y, - Width: opts.Width, - Height: opts.Height, - }, nil + info := result.(window.WindowInfo) + return &info, nil } // --- Layout delegation --- // SaveLayout saves the current window arrangement as a named layout. func (s *Service) SaveLayout(name string) error { - if s.windows == nil { - return fmt.Errorf("window manager not initialized") + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") } states := make(map[string]window.WindowState) - for _, n := range s.windows.List() { - if pw, ok := s.windows.Get(n); ok { + for _, n := range ws.Manager().List() { + if pw, ok := ws.Manager().Get(n); ok { x, y := pw.Position() w, h := pw.Size() states[n] = window.WindowState{X: x, Y: y, Width: w, Height: h, Maximized: pw.IsMaximised()} } } - return s.windows.Layout().SaveLayout(name, states) + return ws.Manager().Layout().SaveLayout(name, states) } // RestoreLayout applies a saved layout. func (s *Service) RestoreLayout(name string) error { - if s.windows == nil { - return fmt.Errorf("window manager not initialized") + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") } - layout, ok := s.windows.Layout().GetLayout(name) + layout, ok := ws.Manager().Layout().GetLayout(name) if !ok { return fmt.Errorf("layout not found: %s", name) } for wName, state := range layout.Windows { - if pw, ok := s.windows.Get(wName); ok { + if pw, ok := ws.Manager().Get(wName); ok { pw.SetPosition(state.X, state.Y) pw.SetSize(state.Width, state.Height) if state.Maximized { @@ -539,27 +494,30 @@ func (s *Service) RestoreLayout(name string) error { // ListLayouts returns all saved layout names with metadata. func (s *Service) ListLayouts() []window.LayoutInfo { - if s.windows == nil { + ws := s.windowService() + if ws == nil { return nil } - return s.windows.Layout().ListLayouts() + return ws.Manager().Layout().ListLayouts() } // DeleteLayout removes a saved layout by name. func (s *Service) DeleteLayout(name string) error { - if s.windows == nil { - return fmt.Errorf("window manager not initialized") + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") } - s.windows.Layout().DeleteLayout(name) + ws.Manager().Layout().DeleteLayout(name) return nil } // GetLayout returns a specific layout by name. func (s *Service) GetLayout(name string) *window.Layout { - if s.windows == nil { + ws := s.windowService() + if ws == nil { return nil } - layout, ok := s.windows.Layout().GetLayout(name) + layout, ok := ws.Manager().Layout().GetLayout(name) if !ok { return nil } @@ -570,22 +528,38 @@ func (s *Service) GetLayout(name string) *window.Layout { // TileWindows arranges windows in a tiled layout. func (s *Service) TileWindows(mode window.TileMode, windowNames []string) error { - return s.windows.TileWindows(mode, windowNames, 1920, 1080) // TODO: use actual screen size + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + return ws.Manager().TileWindows(mode, windowNames, 1920, 1080) // TODO: use actual screen size } // SnapWindow snaps a window to a screen edge or corner. func (s *Service) SnapWindow(name string, position window.SnapPosition) error { - return s.windows.SnapWindow(name, position, 1920, 1080) // TODO: use actual screen size + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + return ws.Manager().SnapWindow(name, position, 1920, 1080) // TODO: use actual screen size } // StackWindows arranges windows in a cascade pattern. func (s *Service) StackWindows(windowNames []string, offsetX, offsetY int) error { - return s.windows.StackWindows(windowNames, offsetX, offsetY) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + return ws.Manager().StackWindows(windowNames, offsetX, offsetY) } // ApplyWorkflowLayout applies a predefined layout for a specific workflow. func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error { - return s.windows.ApplyWorkflow(workflow, s.windows.List(), 1920, 1080) + ws := s.windowService() + if ws == nil { + return fmt.Errorf("window service not available") + } + return ws.Manager().ApplyWorkflow(workflow, ws.Manager().List(), 1920, 1080) } // --- Screen queries (remain in display — use application.Get() directly) --- @@ -758,7 +732,7 @@ func (s *Service) GetEventManager() *WSEventManager { return s.events } -// --- Menu (handlers stay in display, structure delegated to menu.Manager) --- +// --- Menu (handlers stay in display, structure delegated via IPC) --- func (s *Service) buildMenu() { items := []menu.MenuItem{ @@ -790,7 +764,7 @@ func (s *Service) buildMenu() { items = items[1:] // skip AppMenu } - s.menus.SetApplicationMenu(items) + _, _, _ = s.Core().PERFORM(menu.TaskSetAppMenu{Items: items}) } func ptr[T any](v T) *T { return &v } @@ -798,8 +772,14 @@ func ptr[T any](v T) *T { return &v } // --- Menu handler methods --- func (s *Service) handleNewWorkspace() { - _ = s.OpenWindow(window.WithName("workspace-new"), window.WithTitle("New Workspace"), - window.WithURL("/workspace/new"), window.WithSize(500, 400)) + _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{ + window.WithName("workspace-new"), + window.WithTitle("New Workspace"), + window.WithURL("/workspace/new"), + window.WithSize(500, 400), + }, + }) } func (s *Service) handleListWorkspaces() { @@ -815,8 +795,14 @@ func (s *Service) handleListWorkspaces() { } func (s *Service) handleNewFile() { - _ = s.OpenWindow(window.WithName("editor"), window.WithTitle("New File - Editor"), - window.WithURL("/#/developer/editor?new=true"), window.WithSize(1200, 800)) + _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{ + window.WithName("editor"), + window.WithTitle("New File - Editor"), + window.WithURL("/#/developer/editor?new=true"), + window.WithSize(1200, 800), + }, + }) } func (s *Service) handleOpenFile() { @@ -828,48 +814,49 @@ func (s *Service) handleOpenFile() { if err != nil || result == "" { return } - _ = s.OpenWindow(window.WithName("editor"), window.WithTitle(result+" - Editor"), - window.WithURL("/#/developer/editor?file="+result), window.WithSize(1200, 800)) + _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{ + window.WithName("editor"), + window.WithTitle(result + " - Editor"), + window.WithURL("/#/developer/editor?file=" + result), + window.WithSize(1200, 800), + }, + }) } func (s *Service) handleSaveFile() { s.app.Event().Emit("ide:save") } func (s *Service) handleOpenEditor() { - _ = s.OpenWindow(window.WithName("editor"), window.WithTitle("Editor"), - window.WithURL("/#/developer/editor"), window.WithSize(1200, 800)) + _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{ + window.WithName("editor"), + window.WithTitle("Editor"), + window.WithURL("/#/developer/editor"), + window.WithSize(1200, 800), + }, + }) } func (s *Service) handleOpenTerminal() { - _ = s.OpenWindow(window.WithName("terminal"), window.WithTitle("Terminal"), - window.WithURL("/#/developer/terminal"), window.WithSize(800, 500)) + _, _, _ = s.Core().PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{ + window.WithName("terminal"), + window.WithTitle("Terminal"), + window.WithURL("/#/developer/terminal"), + window.WithSize(800, 500), + }, + }) } func (s *Service) handleRun() { s.app.Event().Emit("ide:run") } func (s *Service) handleBuild() { s.app.Event().Emit("ide:build") } -// --- Tray (setup delegated to systray.Manager) --- +// --- Tray (setup delegated via IPC) --- func (s *Service) setupTray() { - _ = s.tray.Setup("Core", "Core") - s.tray.RegisterCallback("open-desktop", func() { - for _, name := range s.windows.List() { - if pw, ok := s.windows.Get(name); ok { - pw.Show() - } - } - }) - s.tray.RegisterCallback("close-desktop", func() { - for _, name := range s.windows.List() { - if pw, ok := s.windows.Get(name); ok { - pw.Hide() - } - } - }) - s.tray.RegisterCallback("env-info", func() { s.ShowEnvironmentDialog() }) - s.tray.RegisterCallback("quit", func() { s.app.Quit() }) - _ = s.tray.SetMenu([]systray.TrayMenuItem{ + _, _, _ = s.Core().PERFORM(systray.TaskSetTrayMenu{Items: []systray.TrayMenuItem{ {Label: "Open Desktop", ActionID: "open-desktop"}, {Label: "Close Desktop", ActionID: "close-desktop"}, {Type: "separator"}, {Label: "Environment Info", ActionID: "env-info"}, {Type: "separator"}, {Label: "Quit", ActionID: "quit"}, - }) + }}) } diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index c1d60e9..59f8008 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -12,173 +12,8 @@ import ( "github.com/stretchr/testify/require" ) -// --- Mock platform implementations for sub-packages --- - -// displayMockPlatformWindow implements window.PlatformWindow for display tests. -type displayMockPlatformWindow struct { - name, title, url string - width, height, x, y int - maximised, focused bool - visible, alwaysOnTop bool - closed bool - eventHandlers []func(window.WindowEvent) -} - -func (w *displayMockPlatformWindow) Name() string { return w.name } -func (w *displayMockPlatformWindow) Position() (int, int) { return w.x, w.y } -func (w *displayMockPlatformWindow) Size() (int, int) { return w.width, w.height } -func (w *displayMockPlatformWindow) IsMaximised() bool { return w.maximised } -func (w *displayMockPlatformWindow) IsFocused() bool { return w.focused } -func (w *displayMockPlatformWindow) SetTitle(title string) { w.title = title } -func (w *displayMockPlatformWindow) SetPosition(x, y int) { w.x = x; w.y = y } -func (w *displayMockPlatformWindow) SetSize(width, height int) { - w.width = width - w.height = height -} -func (w *displayMockPlatformWindow) SetBackgroundColour(r, g, b, a uint8) {} -func (w *displayMockPlatformWindow) SetVisibility(visible bool) { w.visible = visible } -func (w *displayMockPlatformWindow) SetAlwaysOnTop(alwaysOnTop bool) { w.alwaysOnTop = alwaysOnTop } -func (w *displayMockPlatformWindow) Maximise() { w.maximised = true } -func (w *displayMockPlatformWindow) Restore() { w.maximised = false } -func (w *displayMockPlatformWindow) Minimise() {} -func (w *displayMockPlatformWindow) Focus() { w.focused = true } -func (w *displayMockPlatformWindow) Close() { w.closed = true } -func (w *displayMockPlatformWindow) Show() { w.visible = true } -func (w *displayMockPlatformWindow) Hide() { w.visible = false } -func (w *displayMockPlatformWindow) Fullscreen() {} -func (w *displayMockPlatformWindow) UnFullscreen() {} -func (w *displayMockPlatformWindow) OnWindowEvent(handler func(window.WindowEvent)) { - w.eventHandlers = append(w.eventHandlers, handler) -} - -// displayMockWindowPlatform implements window.Platform for display tests. -type displayMockWindowPlatform struct { - windows []*displayMockPlatformWindow -} - -func (p *displayMockWindowPlatform) CreateWindow(opts window.PlatformWindowOptions) window.PlatformWindow { - w := &displayMockPlatformWindow{ - name: opts.Name, title: opts.Title, url: opts.URL, - width: opts.Width, height: opts.Height, - x: opts.X, y: opts.Y, - } - p.windows = append(p.windows, w) - return w -} - -func (p *displayMockWindowPlatform) GetWindows() []window.PlatformWindow { - out := make([]window.PlatformWindow, len(p.windows)) - for i, w := range p.windows { - out[i] = w - } - return out -} - -// displayMockSystrayPlatform implements systray.Platform for display tests. -type displayMockSystrayPlatform struct { - trays []*displayMockTray - menus []*displayMockSystrayMenu -} - -func (p *displayMockSystrayPlatform) NewTray() systray.PlatformTray { - t := &displayMockTray{} - p.trays = append(p.trays, t) - return t -} - -func (p *displayMockSystrayPlatform) NewMenu() systray.PlatformMenu { - m := &displayMockSystrayMenu{} - p.menus = append(p.menus, m) - return m -} - -type displayMockTray struct { - tooltip, label string - menu systray.PlatformMenu -} - -func (t *displayMockTray) SetIcon(data []byte) {} -func (t *displayMockTray) SetTemplateIcon(data []byte) {} -func (t *displayMockTray) SetTooltip(text string) { t.tooltip = text } -func (t *displayMockTray) SetLabel(text string) { t.label = text } -func (t *displayMockTray) SetMenu(m systray.PlatformMenu) { t.menu = m } -func (t *displayMockTray) AttachWindow(w systray.WindowHandle) {} - -type displayMockSystrayMenu struct { - items []string -} - -func (m *displayMockSystrayMenu) Add(label string) systray.PlatformMenuItem { - m.items = append(m.items, label) - return &displayMockSystrayMenuItem{} -} -func (m *displayMockSystrayMenu) AddSeparator() { m.items = append(m.items, "---") } - -type displayMockSystrayMenuItem struct{} - -func (mi *displayMockSystrayMenuItem) SetTooltip(text string) {} -func (mi *displayMockSystrayMenuItem) SetChecked(checked bool) {} -func (mi *displayMockSystrayMenuItem) SetEnabled(enabled bool) {} -func (mi *displayMockSystrayMenuItem) OnClick(fn func()) {} -func (mi *displayMockSystrayMenuItem) AddSubmenu() systray.PlatformMenu { - return &displayMockSystrayMenu{} -} - -// displayMockMenuPlatform implements menu.Platform for display tests. -type displayMockMenuPlatform struct { - appMenu menu.PlatformMenu -} - -func (p *displayMockMenuPlatform) NewMenu() menu.PlatformMenu { - return &displayMockMenu{} -} - -func (p *displayMockMenuPlatform) SetApplicationMenu(m menu.PlatformMenu) { - p.appMenu = m -} - -type displayMockMenu struct { - items []string -} - -func (m *displayMockMenu) Add(label string) menu.PlatformMenuItem { - m.items = append(m.items, label) - return &displayMockMenuItem{} -} -func (m *displayMockMenu) AddSeparator() { m.items = append(m.items, "---") } -func (m *displayMockMenu) AddSubmenu(label string) menu.PlatformMenu { - m.items = append(m.items, label) - return &displayMockMenu{} -} -func (m *displayMockMenu) AddRole(role menu.MenuRole) {} - -type displayMockMenuItem struct{} - -func (mi *displayMockMenuItem) SetAccelerator(accel string) menu.PlatformMenuItem { return mi } -func (mi *displayMockMenuItem) SetTooltip(text string) menu.PlatformMenuItem { return mi } -func (mi *displayMockMenuItem) SetChecked(checked bool) menu.PlatformMenuItem { return mi } -func (mi *displayMockMenuItem) SetEnabled(enabled bool) menu.PlatformMenuItem { return mi } -func (mi *displayMockMenuItem) OnClick(fn func()) menu.PlatformMenuItem { return mi } - // --- Test helpers --- -// newServiceWithMocks creates a Service with mock sub-managers for testing. -// Uses a temp directory for state/layout persistence to avoid loading real saved state. -func newServiceWithMocks(t *testing.T) (*Service, *mockApp, *displayMockWindowPlatform) { - service, err := New() - require.NoError(t, err) - - mock := newMockApp() - service.app = mock - - wp := &displayMockWindowPlatform{} - service.windows = window.NewManagerWithDir(wp, t.TempDir()) - service.tray = systray.NewManager(&displayMockSystrayPlatform{}) - service.menus = menu.NewManager(&displayMockMenuPlatform{}) - - return service, mock, wp -} - // newTestDisplayService creates a display service registered with Core for IPC testing. func newTestDisplayService(t *testing.T) (*Service, *core.Core) { t.Helper() @@ -192,6 +27,21 @@ func newTestDisplayService(t *testing.T) (*Service, *core.Core) { return svc, c } +// newTestConclave creates a full 4-service conclave for integration testing. +func newTestConclave(t *testing.T) *core.Core { + t.Helper() + c, err := core.New( + core.WithService(Register(nil)), + core.WithService(window.Register(window.NewMockPlatform())), + core.WithService(systray.Register(systray.NewMockPlatform())), + core.WithService(menu.Register(menu.NewMockPlatform())), + core.WithServiceLock(), + ) + require.NoError(t, err) + require.NoError(t, c.ServiceStartup(context.Background(), nil)) + return c +} + // --- Tests --- func TestNew(t *testing.T) { @@ -262,25 +112,70 @@ func TestConfigTask_Good(t *testing.T) { assert.Equal(t, 800, cfg["default_width"]) } -func TestOpenWindow_Good(t *testing.T) { - t.Run("creates window with default options", func(t *testing.T) { - service, _, wp := newServiceWithMocks(t) +// --- Conclave integration tests --- - err := service.OpenWindow() +func TestServiceConclave_Good(t *testing.T) { + c := newTestConclave(t) + + // Open a window via IPC + result, handled, err := c.PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{window.WithName("main")}, + }) + require.NoError(t, err) + assert.True(t, handled) + info := result.(window.WindowInfo) + assert.Equal(t, "main", info.Name) + + // Query window config from display + val, handled, err := c.QUERY(window.QueryConfig{}) + require.NoError(t, err) + assert.True(t, handled) + assert.NotNil(t, val) + + // Set app menu via IPC + _, handled, err = c.PERFORM(menu.TaskSetAppMenu{Items: []menu.MenuItem{ + {Label: "File"}, + }}) + require.NoError(t, err) + assert.True(t, handled) + + // Query app menu via IPC + menuResult, handled, _ := c.QUERY(menu.QueryGetAppMenu{}) + assert.True(t, handled) + items := menuResult.([]menu.MenuItem) + assert.Len(t, items, 1) +} + +func TestServiceConclave_Bad(t *testing.T) { + // Sub-service starts without display — config QUERY returns handled=false + c, err := core.New( + core.WithService(window.Register(window.NewMockPlatform())), + core.WithServiceLock(), + ) + require.NoError(t, err) + require.NoError(t, c.ServiceStartup(context.Background(), nil)) + + _, handled, _ := c.QUERY(window.QueryConfig{}) + assert.False(t, handled, "no display service means no config handler") +} + +// --- IPC delegation tests (full conclave) --- + +func TestOpenWindow_Good(t *testing.T) { + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + + t.Run("creates window with default options", func(t *testing.T) { + err := svc.OpenWindow() assert.NoError(t, err) - // Verify window was created through the platform - assert.Len(t, wp.windows, 1) - assert.Equal(t, "main", wp.windows[0].name) - assert.Equal(t, "Core", wp.windows[0].title) - assert.Equal(t, 1280, wp.windows[0].width) - assert.Equal(t, 800, wp.windows[0].height) + // Verify via IPC query + infos := svc.ListWindowInfos() + assert.GreaterOrEqual(t, len(infos), 1) }) t.Run("creates window with custom options", func(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - - err := service.OpenWindow( + err := svc.OpenWindow( window.WithName("custom-window"), window.WithTitle("Custom Title"), window.WithSize(640, 480), @@ -288,27 +183,25 @@ func TestOpenWindow_Good(t *testing.T) { ) assert.NoError(t, err) - assert.Len(t, wp.windows, 1) - assert.Equal(t, "custom-window", wp.windows[0].name) - assert.Equal(t, "Custom Title", wp.windows[0].title) - assert.Equal(t, 640, wp.windows[0].width) - assert.Equal(t, 480, wp.windows[0].height) + result, _, _ := c.QUERY(window.QueryWindowByName{Name: "custom-window"}) + info := result.(*window.WindowInfo) + assert.Equal(t, "custom-window", info.Name) }) } func TestGetWindowInfo_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - _ = service.OpenWindow( + _ = svc.OpenWindow( window.WithName("test-win"), window.WithSize(800, 600), ) - // Set position on the mock window - wp.windows[0].x = 100 - wp.windows[0].y = 200 + // Modify position via IPC + _, _, _ = c.PERFORM(window.TaskSetPosition{Name: "test-win", X: 100, Y: 200}) - info, err := service.GetWindowInfo("test-win") + info, err := svc.GetWindowInfo("test-win") require.NoError(t, err) assert.Equal(t, "test-win", info.Name) assert.Equal(t, 100, info.X) @@ -318,144 +211,165 @@ func TestGetWindowInfo_Good(t *testing.T) { } func TestGetWindowInfo_Bad(t *testing.T) { - service, _, _ := newServiceWithMocks(t) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - _, err := service.GetWindowInfo("nonexistent") - assert.Error(t, err) - assert.Contains(t, err.Error(), "window not found") + info, err := svc.GetWindowInfo("nonexistent") + // QueryWindowByName returns nil for nonexistent — handled=true, result=nil + assert.NoError(t, err) + assert.Nil(t, info) } func TestListWindowInfos_Good(t *testing.T) { - service, _, _ := newServiceWithMocks(t) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - _ = service.OpenWindow(window.WithName("win-1")) - _ = service.OpenWindow(window.WithName("win-2")) + _ = svc.OpenWindow(window.WithName("win-1")) + _ = svc.OpenWindow(window.WithName("win-2")) - infos := service.ListWindowInfos() + infos := svc.ListWindowInfos() assert.Len(t, infos, 2) } func TestSetWindowPosition_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("pos-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("pos-win")) - err := service.SetWindowPosition("pos-win", 300, 400) + err := svc.SetWindowPosition("pos-win", 300, 400) assert.NoError(t, err) - assert.Equal(t, 300, wp.windows[0].x) - assert.Equal(t, 400, wp.windows[0].y) + + info, _ := svc.GetWindowInfo("pos-win") + assert.Equal(t, 300, info.X) + assert.Equal(t, 400, info.Y) } func TestSetWindowPosition_Bad(t *testing.T) { - service, _, _ := newServiceWithMocks(t) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - err := service.SetWindowPosition("nonexistent", 0, 0) + err := svc.SetWindowPosition("nonexistent", 0, 0) assert.Error(t, err) } func TestSetWindowSize_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("size-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("size-win")) - err := service.SetWindowSize("size-win", 1024, 768) + err := svc.SetWindowSize("size-win", 1024, 768) assert.NoError(t, err) - assert.Equal(t, 1024, wp.windows[0].width) - assert.Equal(t, 768, wp.windows[0].height) + + info, _ := svc.GetWindowInfo("size-win") + assert.Equal(t, 1024, info.Width) + assert.Equal(t, 768, info.Height) } func TestMaximizeWindow_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("max-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("max-win")) - err := service.MaximizeWindow("max-win") + err := svc.MaximizeWindow("max-win") assert.NoError(t, err) - assert.True(t, wp.windows[0].maximised) + + info, _ := svc.GetWindowInfo("max-win") + assert.True(t, info.Maximized) } func TestRestoreWindow_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("restore-win")) - wp.windows[0].maximised = true + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("restore-win")) + _ = svc.MaximizeWindow("restore-win") - err := service.RestoreWindow("restore-win") + err := svc.RestoreWindow("restore-win") assert.NoError(t, err) - assert.False(t, wp.windows[0].maximised) + + info, _ := svc.GetWindowInfo("restore-win") + assert.False(t, info.Maximized) } func TestFocusWindow_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("focus-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("focus-win")) - err := service.FocusWindow("focus-win") + err := svc.FocusWindow("focus-win") assert.NoError(t, err) - assert.True(t, wp.windows[0].focused) + + info, _ := svc.GetWindowInfo("focus-win") + assert.True(t, info.Focused) } func TestCloseWindow_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("close-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("close-win")) - err := service.CloseWindow("close-win") + err := svc.CloseWindow("close-win") assert.NoError(t, err) - assert.True(t, wp.windows[0].closed) - // Window should be removed from manager - _, ok := service.windows.Get("close-win") - assert.False(t, ok) + // Window should be removed + info, _ := svc.GetWindowInfo("close-win") + assert.Nil(t, info) } func TestSetWindowVisibility_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("vis-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("vis-win")) - err := service.SetWindowVisibility("vis-win", false) + err := svc.SetWindowVisibility("vis-win", false) assert.NoError(t, err) - assert.False(t, wp.windows[0].visible) - err = service.SetWindowVisibility("vis-win", true) + err = svc.SetWindowVisibility("vis-win", true) assert.NoError(t, err) - assert.True(t, wp.windows[0].visible) } func TestSetWindowAlwaysOnTop_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("ontop-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("ontop-win")) - err := service.SetWindowAlwaysOnTop("ontop-win", true) + err := svc.SetWindowAlwaysOnTop("ontop-win", true) assert.NoError(t, err) - assert.True(t, wp.windows[0].alwaysOnTop) } func TestSetWindowTitle_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("title-win")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("title-win")) - err := service.SetWindowTitle("title-win", "New Title") + err := svc.SetWindowTitle("title-win", "New Title") assert.NoError(t, err) - assert.Equal(t, "New Title", wp.windows[0].title) } func TestGetFocusedWindow_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("win-a")) - _ = service.OpenWindow(window.WithName("win-b")) - wp.windows[1].focused = true + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("win-a")) + _ = svc.OpenWindow(window.WithName("win-b")) + _ = svc.FocusWindow("win-b") - focused := service.GetFocusedWindow() + focused := svc.GetFocusedWindow() assert.Equal(t, "win-b", focused) } func TestGetFocusedWindow_NoneSelected(t *testing.T) { - service, _, _ := newServiceWithMocks(t) - _ = service.OpenWindow(window.WithName("win-a")) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("win-a")) - focused := service.GetFocusedWindow() + focused := svc.GetFocusedWindow() assert.Equal(t, "", focused) } func TestCreateWindow_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - info, err := service.CreateWindow(CreateWindowOptions{ + info, err := svc.CreateWindow(CreateWindowOptions{ Name: "new-win", Title: "New Window", URL: "/new", @@ -464,106 +378,57 @@ func TestCreateWindow_Good(t *testing.T) { }) require.NoError(t, err) assert.Equal(t, "new-win", info.Name) - assert.Equal(t, 600, info.Width) - assert.Equal(t, 400, info.Height) - assert.Len(t, wp.windows, 1) } func TestCreateWindow_Bad(t *testing.T) { - service, _, _ := newServiceWithMocks(t) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - _, err := service.CreateWindow(CreateWindowOptions{}) + _, err := svc.CreateWindow(CreateWindowOptions{}) assert.Error(t, err) assert.Contains(t, err.Error(), "window name is required") } -func TestShowEnvironmentDialog_Good(t *testing.T) { - service, mock, _ := newServiceWithMocks(t) +func TestResetWindowState_Good(t *testing.T) { + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - // This will panic because Dialog().Info() returns nil - // We're verifying the env info is accessed, not that a dialog shows - assert.NotPanics(t, func() { - defer func() { recover() }() // Recover from nil dialog - service.ShowEnvironmentDialog() - }) - - // Verify dialog was requested (even though it's nil) - assert.Equal(t, 1, mock.dialogManager.infoDialogsCreated) + err := svc.ResetWindowState() + assert.NoError(t, err) } -func TestBuildMenu_Good(t *testing.T) { - service, _, _ := newServiceWithMocks(t) - c, _ := core.New() - service.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) +func TestGetSavedWindowStates_Good(t *testing.T) { + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") - // buildMenu should not panic with mock platforms - assert.NotPanics(t, func() { - service.buildMenu() - }) + states := svc.GetSavedWindowStates() + assert.NotNil(t, states) } -func TestSetupTray_Good(t *testing.T) { - service, _, _ := newServiceWithMocks(t) - c, _ := core.New() - service.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) +func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) { + c := newTestConclave(t) - // setupTray should not panic with mock platforms - assert.NotPanics(t, func() { - service.setupTray() + // Open a window — this should trigger ActionWindowOpened + // which HandleIPCEvents should convert to a WS event + result, handled, err := c.PERFORM(window.TaskOpenWindow{ + Opts: []window.WindowOption{window.WithName("test")}, }) - - // Verify tray is active - assert.True(t, service.tray.IsActive()) -} - -func TestHandleNewWorkspace_Good(t *testing.T) { - service, _, wp := newServiceWithMocks(t) - - service.handleNewWorkspace() - - // Verify a window was created with correct options - assert.Len(t, wp.windows, 1) - assert.Equal(t, "workspace-new", wp.windows[0].name) - assert.Equal(t, "New Workspace", wp.windows[0].title) - assert.Equal(t, 500, wp.windows[0].width) - assert.Equal(t, 400, wp.windows[0].height) + require.NoError(t, err) + assert.True(t, handled) + info := result.(window.WindowInfo) + assert.Equal(t, "test", info.Name) } func TestHandleListWorkspaces_Good(t *testing.T) { - service, _, _ := newServiceWithMocks(t) - c, _ := core.New() - service.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") // handleListWorkspaces should not panic when workspace service is not available assert.NotPanics(t, func() { - service.handleListWorkspaces() + svc.handleListWorkspaces() }) } -func TestHandleSaveFile_Good(t *testing.T) { - service, mock, _ := newServiceWithMocks(t) - - service.handleSaveFile() - - assert.Contains(t, mock.eventManager.emittedEvents, "ide:save") -} - -func TestHandleRun_Good(t *testing.T) { - service, mock, _ := newServiceWithMocks(t) - - service.handleRun() - - assert.Contains(t, mock.eventManager.emittedEvents, "ide:run") -} - -func TestHandleBuild_Good(t *testing.T) { - service, mock, _ := newServiceWithMocks(t) - - service.handleBuild() - - assert.Contains(t, mock.eventManager.emittedEvents, "ide:build") -} - func TestWSEventManager_Good(t *testing.T) { es := newMockEventSource() em := NewWSEventManager(es) @@ -583,37 +448,3 @@ func TestWSEventManager_SetupWindowEventListeners_Good(t *testing.T) { // Verify theme handler was registered assert.Len(t, es.themeHandlers, 1) } - -func TestResetWindowState_Good(t *testing.T) { - service, _, _ := newServiceWithMocks(t) - - err := service.ResetWindowState() - assert.NoError(t, err) -} - -func TestGetSavedWindowStates_Good(t *testing.T) { - service, _, _ := newServiceWithMocks(t) - - states := service.GetSavedWindowStates() - assert.NotNil(t, states) -} - -func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) { - c, err := core.New( - core.WithService(Register(nil)), - core.WithService(window.Register(window.NewMockPlatform())), - core.WithServiceLock(), - ) - require.NoError(t, err) - require.NoError(t, c.ServiceStartup(context.Background(), nil)) - - // Open a window — this should trigger ActionWindowOpened - // which HandleIPCEvents should convert to a WS event - result, handled, err := c.PERFORM(window.TaskOpenWindow{ - Opts: []window.WindowOption{window.WithName("test")}, - }) - require.NoError(t, err) - assert.True(t, handled) - info := result.(window.WindowInfo) - assert.Equal(t, "test", info.Name) -} diff --git a/pkg/display/mocks_test.go b/pkg/display/mocks_test.go index b619977..c008e43 100644 --- a/pkg/display/mocks_test.go +++ b/pkg/display/mocks_test.go @@ -1,116 +1,5 @@ package display -import ( - "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/events" -) - -// mockApp is a mock implementation of the App interface for testing. -type mockApp struct { - dialogManager *mockDialogManager - envManager *mockEnvManager - eventManager *mockEventManager - logger *mockLogger - quitCalled bool -} - -func newMockApp() *mockApp { - return &mockApp{ - dialogManager: newMockDialogManager(), - envManager: newMockEnvManager(), - eventManager: newMockEventManager(), - logger: &mockLogger{}, - } -} - -func (m *mockApp) Dialog() DialogManager { return m.dialogManager } -func (m *mockApp) Env() EnvManager { return m.envManager } -func (m *mockApp) Event() EventManager { return m.eventManager } -func (m *mockApp) Logger() Logger { return m.logger } -func (m *mockApp) Quit() { m.quitCalled = true } - -// mockDialogManager tracks dialog creation calls. -type mockDialogManager struct { - infoDialogsCreated int - warningDialogsCreated int -} - -func newMockDialogManager() *mockDialogManager { - return &mockDialogManager{} -} - -func (m *mockDialogManager) Info() *application.MessageDialog { - m.infoDialogsCreated++ - return nil // Can't create real dialog without Wails runtime -} - -func (m *mockDialogManager) Warning() *application.MessageDialog { - m.warningDialogsCreated++ - return nil // Can't create real dialog without Wails runtime -} - -func (m *mockDialogManager) OpenFile() *application.OpenFileDialogStruct { - return nil // Can't create real dialog without Wails runtime -} - -// mockEnvManager provides mock environment info. -type mockEnvManager struct { - envInfo application.EnvironmentInfo - darkMode bool -} - -func newMockEnvManager() *mockEnvManager { - return &mockEnvManager{ - envInfo: application.EnvironmentInfo{ - OS: "test-os", - Arch: "test-arch", - Debug: true, - PlatformInfo: map[string]any{"test": "value"}, - }, - darkMode: false, - } -} - -func (m *mockEnvManager) Info() application.EnvironmentInfo { - return m.envInfo -} - -func (m *mockEnvManager) IsDarkMode() bool { - return m.darkMode -} - -// mockEventManager tracks event registration. -type mockEventManager struct { - registeredEvents []events.ApplicationEventType - emittedEvents []string -} - -func newMockEventManager() *mockEventManager { - return &mockEventManager{ - registeredEvents: make([]events.ApplicationEventType, 0), - emittedEvents: make([]string, 0), - } -} - -func (m *mockEventManager) OnApplicationEvent(eventType events.ApplicationEventType, handler func(*application.ApplicationEvent)) func() { - m.registeredEvents = append(m.registeredEvents, eventType) - return func() {} // Return a no-op unsubscribe function -} - -func (m *mockEventManager) Emit(name string, data ...any) bool { - m.emittedEvents = append(m.emittedEvents, name) - return true // Pretend emission succeeded -} - -// mockLogger tracks log calls. -type mockLogger struct { - infoMessages []string -} - -func (m *mockLogger) Info(message string, args ...any) { - m.infoMessages = append(m.infoMessages, message) -} - // mockEventSource implements EventSource for testing. type mockEventSource struct { themeHandlers []func(isDark bool)