package display import ( "context" "os" "path/filepath" "testing" "forge.lthn.ai/core/go/pkg/core" "forge.lthn.ai/core/gui/pkg/menu" "forge.lthn.ai/core/gui/pkg/systray" "forge.lthn.ai/core/gui/pkg/window" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // --- Test helpers --- // newTestDisplayService creates a display service registered with Core for IPC testing. func newTestDisplayService(t *testing.T) (*Service, *core.Core) { t.Helper() c, err := core.New( core.WithService(Register(nil)), core.WithServiceLock(), ) require.NoError(t, err) require.NoError(t, c.ServiceStartup(context.Background(), nil)) svc := core.MustServiceFor[*Service](c, "display") 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 } func requireCreateWindow(t *testing.T, svc *Service, options CreateWindowOptions) { t.Helper() _, err := svc.CreateWindow(options) require.NoError(t, err) } // --- Tests --- func TestNewService_Good(t *testing.T) { service, err := NewService() assert.NoError(t, err) assert.NotNil(t, service) } func TestNewService_Good_IndependentInstances(t *testing.T) { service1, err1 := NewService() service2, err2 := NewService() assert.NoError(t, err1) assert.NoError(t, err2) assert.NotSame(t, service1, service2) } func TestRegister_Good(t *testing.T) { factory := Register(nil) // nil wailsApp for testing assert.NotNil(t, factory) c, err := core.New( core.WithService(factory), core.WithServiceLock(), ) require.NoError(t, err) require.NoError(t, c.ServiceStartup(context.Background(), nil)) svc := core.MustServiceFor[*Service](c, "display") assert.NotNil(t, svc) } func TestConfigQuery_Good(t *testing.T) { svc, c := newTestDisplayService(t) // Set window config svc.configData["window"] = map[string]any{ "default_width": 1024, } result, handled, err := c.QUERY(window.QueryConfig{}) require.NoError(t, err) assert.True(t, handled) cfg := result.(map[string]any) assert.Equal(t, 1024, cfg["default_width"]) } func TestConfigQuery_Bad(t *testing.T) { // No display service — window config query returns handled=false c, err := core.New(core.WithServiceLock()) require.NoError(t, err) _, handled, _ := c.QUERY(window.QueryConfig{}) assert.False(t, handled) } func TestConfigTask_Good(t *testing.T) { _, c := newTestDisplayService(t) newCfg := map[string]any{"default_width": 800} _, handled, err := c.PERFORM(window.TaskSaveConfig{Config: newCfg}) require.NoError(t, err) assert.True(t, handled) // Verify config was saved result, _, _ := c.QUERY(window.QueryConfig{}) cfg := result.(map[string]any) assert.Equal(t, 800, cfg["default_width"]) } // --- Conclave integration tests --- func TestServiceConclave_Good(t *testing.T) { c := newTestConclave(t) // Open a window via IPC result, handled, err := c.PERFORM(window.TaskOpenWindow{ Window: &window.Window{Name: "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_Compatibility_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 via IPC query infos := svc.ListWindowInfos() assert.GreaterOrEqual(t, len(infos), 1) }) t.Run("creates window with declarative options", func(t *testing.T) { info, err := svc.CreateWindow(CreateWindowOptions{ Name: "custom-window", Title: "Custom Title", URL: "/custom", Width: 640, Height: 480, }) require.NoError(t, err) assert.Equal(t, "custom-window", info.Name) }) } func TestGetWindowInfo_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "test-win", Width: 800, Height: 600}) // Modify position via IPC _, _, _ = c.PERFORM(window.TaskSetPosition{Name: "test-win", X: 100, Y: 200}) info, err := svc.GetWindowInfo("test-win") require.NoError(t, err) assert.Equal(t, "test-win", info.Name) assert.Equal(t, 100, info.X) assert.Equal(t, 200, info.Y) assert.Equal(t, 800, info.Width) assert.Equal(t, 600, info.Height) } func TestGetWindowInfo_Bad(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") 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) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-1"}) requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-2"}) infos := svc.ListWindowInfos() assert.Len(t, infos, 2) } func TestSetWindowPosition_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "pos-win"}) err := svc.SetWindowPosition("pos-win", 300, 400) assert.NoError(t, err) info, _ := svc.GetWindowInfo("pos-win") assert.Equal(t, 300, info.X) assert.Equal(t, 400, info.Y) } func TestSetWindowPosition_Bad(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") err := svc.SetWindowPosition("nonexistent", 0, 0) assert.Error(t, err) } func TestSetWindowSize_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "size-win"}) err := svc.SetWindowSize("size-win", 1024, 768) assert.NoError(t, err) info, _ := svc.GetWindowInfo("size-win") assert.Equal(t, 1024, info.Width) assert.Equal(t, 768, info.Height) } func TestMaximizeWindow_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "max-win"}) err := svc.MaximizeWindow("max-win") assert.NoError(t, err) info, _ := svc.GetWindowInfo("max-win") assert.True(t, info.Maximized) } func TestRestoreWindow_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "restore-win"}) _ = svc.MaximizeWindow("restore-win") err := svc.RestoreWindow("restore-win") assert.NoError(t, err) info, _ := svc.GetWindowInfo("restore-win") assert.False(t, info.Maximized) } func TestFocusWindow_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "focus-win"}) err := svc.FocusWindow("focus-win") assert.NoError(t, err) info, _ := svc.GetWindowInfo("focus-win") assert.True(t, info.Focused) } func TestCloseWindow_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "close-win"}) err := svc.CloseWindow("close-win") assert.NoError(t, err) // Window should be removed info, _ := svc.GetWindowInfo("close-win") assert.Nil(t, info) } func TestSetWindowVisibility_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "vis-win"}) err := svc.SetWindowVisibility("vis-win", false) assert.NoError(t, err) err = svc.SetWindowVisibility("vis-win", true) assert.NoError(t, err) } func TestSetWindowAlwaysOnTop_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "ontop-win"}) err := svc.SetWindowAlwaysOnTop("ontop-win", true) assert.NoError(t, err) } func TestSetWindowTitle_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "title-win"}) err := svc.SetWindowTitle("title-win", "New Title") assert.NoError(t, err) } func TestGetFocusedWindow_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-a"}) requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-b"}) _ = svc.FocusWindow("win-b") focused := svc.GetFocusedWindow() assert.Equal(t, "win-b", focused) } func TestGetFocusedWindow_Good_NoneSelected(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-a"}) focused := svc.GetFocusedWindow() assert.Equal(t, "", focused) } func TestCreateWindow_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") info, err := svc.CreateWindow(CreateWindowOptions{ Name: "new-win", Title: "New Window", URL: "/new", Width: 600, Height: 400, }) require.NoError(t, err) assert.Equal(t, "new-win", info.Name) } func TestCreateWindow_Bad(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") _, err := svc.CreateWindow(CreateWindowOptions{}) assert.Error(t, err) assert.Contains(t, err.Error(), "window name is required") } func TestResetWindowState_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") err := svc.ResetWindowState() assert.NoError(t, err) } func TestGetSavedWindowStates_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") states := svc.GetSavedWindowStates() assert.NotNil(t, states) } func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) { c := newTestConclave(t) // Open a window — this should trigger ActionWindowOpened // which HandleIPCEvents should convert to a WS event result, handled, err := c.PERFORM(window.TaskOpenWindow{ Window: &window.Window{Name: "test"}, }) require.NoError(t, err) assert.True(t, handled) info := result.(window.WindowInfo) assert.Equal(t, "test", info.Name) } func TestHandleListWorkspaces_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") // handleListWorkspaces should not panic when workspace service is not available assert.NotPanics(t, func() { svc.handleListWorkspaces() }) } func TestWSEventManager_Good(t *testing.T) { em := NewWSEventManager() defer em.Close() assert.NotNil(t, em) assert.Equal(t, 0, em.ConnectedClients()) } // --- Config file loading tests --- func TestLoadConfig_Good(t *testing.T) { // Create temp config file dir := t.TempDir() cfgPath := filepath.Join(dir, ".core", "gui", "config.yaml") require.NoError(t, os.MkdirAll(filepath.Dir(cfgPath), 0o755)) require.NoError(t, os.WriteFile(cfgPath, []byte(` window: default_width: 1280 default_height: 720 systray: tooltip: "Test App" menu: show_dev_tools: false `), 0o644)) s, _ := NewService() s.loadConfigFrom(cfgPath) // Verify configData was populated from file assert.Equal(t, 1280, s.configData["window"]["default_width"]) assert.Equal(t, "Test App", s.configData["systray"]["tooltip"]) assert.Equal(t, false, s.configData["menu"]["show_dev_tools"]) } func TestLoadConfig_Bad_MissingFile(t *testing.T) { s, _ := NewService() s.loadConfigFrom(filepath.Join(t.TempDir(), "nonexistent.yaml")) // Should not panic, configData stays at empty defaults assert.Empty(t, s.configData["window"]) assert.Empty(t, s.configData["systray"]) assert.Empty(t, s.configData["menu"]) } func TestHandleConfigTask_Persists_Good(t *testing.T) { dir := t.TempDir() cfgPath := filepath.Join(dir, "config.yaml") s, _ := NewService() s.loadConfigFrom(cfgPath) // Creates empty config (file doesn't exist yet) // Simulate a TaskSaveConfig through the handler c, _ := core.New( core.WithService(func(c *core.Core) (any, error) { s.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) return s, nil }), core.WithServiceLock(), ) c.ServiceStartup(context.Background(), nil) _, handled, err := c.PERFORM(window.TaskSaveConfig{ Config: map[string]any{"default_width": 1920}, }) require.NoError(t, err) assert.True(t, handled) // Verify file was written data, err := os.ReadFile(cfgPath) require.NoError(t, err) assert.Contains(t, string(data), "default_width") }