[agent/codex:gpt-5.4-mini] Update the code against the AX design principles in ~/spec/r... #6

Merged
Virgil merged 1 commit from agent/update-the-code-against-the-ax-design-pr into dev 2026-03-31 13:20:19 +00:00
18 changed files with 267 additions and 191 deletions

View file

@ -43,9 +43,9 @@ type Service struct {
events *WSEventManager
}
// New returns a display Service with empty config sections.
// s, _ := display.New(); s.loadConfigFrom("/path/to/config.yaml")
func New() (*Service, error) {
// NewService returns a display Service with empty config sections.
// svc, _ := display.NewService(); _, _ = svc.CreateWindow(display.CreateWindowOptions{Name: "settings", URL: "/settings", Width: 800, Height: 600})
func NewService() (*Service, error) {
return &Service{
configData: map[string]map[string]any{
"window": {},
@ -55,12 +55,17 @@ func New() (*Service, error) {
}, nil
}
// Deprecated: use NewService().
func New() (*Service, error) {
return NewService()
}
// Register binds the display service to a Core instance.
// core.WithService(display.Register(app)) // production (Wails app)
// core.WithService(display.Register(nil)) // tests (no Wails runtime)
func Register(wailsApp *application.App) func(*core.Core) (any, error) {
return func(c *core.Core) (any, error) {
s, err := New()
s, err := NewService()
if err != nil {
return nil, err
}
@ -584,7 +589,7 @@ func (s *Service) windowService() *window.Service {
// --- Window Management (delegates via IPC) ---
// OpenWindow creates a new window via IPC.
// Deprecated: use CreateWindow(display.CreateWindowOptions{Name: "settings", URL: "/settings", Width: 800, Height: 600}).
func (s *Service) OpenWindow(options ...window.WindowOption) error {
spec, err := window.ApplyOptions(options...)
if err != nil {

View file

@ -44,17 +44,23 @@ func newTestConclave(t *testing.T) *core.Core {
return c
}
func requireCreateWindow(t *testing.T, svc *Service, options CreateWindowOptions) {
t.Helper()
_, err := svc.CreateWindow(options)
require.NoError(t, err)
}
// --- Tests ---
func TestNew_Good(t *testing.T) {
service, err := New()
func TestNewService_Good(t *testing.T) {
service, err := NewService()
assert.NoError(t, err)
assert.NotNil(t, service)
}
func TestNew_Good_IndependentInstances(t *testing.T) {
service1, err1 := New()
service2, err2 := New()
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)
@ -161,7 +167,7 @@ func TestServiceConclave_Bad(t *testing.T) {
// --- IPC delegation tests (full conclave) ---
func TestOpenWindow_Good(t *testing.T) {
func TestOpenWindow_Compatibility_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
@ -174,17 +180,16 @@ func TestOpenWindow_Good(t *testing.T) {
assert.GreaterOrEqual(t, len(infos), 1)
})
t.Run("creates window with custom options", func(t *testing.T) {
err := svc.OpenWindow(
window.WithName("custom-window"),
window.WithTitle("Custom Title"),
window.WithSize(640, 480),
window.WithURL("/custom"),
)
assert.NoError(t, err)
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)
result, _, _ := c.QUERY(window.QueryWindowByName{Name: "custom-window"})
info := result.(*window.WindowInfo)
assert.Equal(t, "custom-window", info.Name)
})
}
@ -193,10 +198,7 @@ func TestGetWindowInfo_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(
window.WithName("test-win"),
window.WithSize(800, 600),
)
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})
@ -224,8 +226,8 @@ func TestListWindowInfos_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("win-1"))
_ = svc.OpenWindow(window.WithName("win-2"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-1"})
requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-2"})
infos := svc.ListWindowInfos()
assert.Len(t, infos, 2)
@ -234,7 +236,7 @@ func TestListWindowInfos_Good(t *testing.T) {
func TestSetWindowPosition_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("pos-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "pos-win"})
err := svc.SetWindowPosition("pos-win", 300, 400)
assert.NoError(t, err)
@ -255,7 +257,7 @@ func TestSetWindowPosition_Bad(t *testing.T) {
func TestSetWindowSize_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("size-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "size-win"})
err := svc.SetWindowSize("size-win", 1024, 768)
assert.NoError(t, err)
@ -268,7 +270,7 @@ func TestSetWindowSize_Good(t *testing.T) {
func TestMaximizeWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("max-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "max-win"})
err := svc.MaximizeWindow("max-win")
assert.NoError(t, err)
@ -280,7 +282,7 @@ func TestMaximizeWindow_Good(t *testing.T) {
func TestRestoreWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("restore-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "restore-win"})
_ = svc.MaximizeWindow("restore-win")
err := svc.RestoreWindow("restore-win")
@ -293,7 +295,7 @@ func TestRestoreWindow_Good(t *testing.T) {
func TestFocusWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("focus-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "focus-win"})
err := svc.FocusWindow("focus-win")
assert.NoError(t, err)
@ -305,7 +307,7 @@ func TestFocusWindow_Good(t *testing.T) {
func TestCloseWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("close-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "close-win"})
err := svc.CloseWindow("close-win")
assert.NoError(t, err)
@ -318,7 +320,7 @@ func TestCloseWindow_Good(t *testing.T) {
func TestSetWindowVisibility_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("vis-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "vis-win"})
err := svc.SetWindowVisibility("vis-win", false)
assert.NoError(t, err)
@ -330,7 +332,7 @@ func TestSetWindowVisibility_Good(t *testing.T) {
func TestSetWindowAlwaysOnTop_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("ontop-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "ontop-win"})
err := svc.SetWindowAlwaysOnTop("ontop-win", true)
assert.NoError(t, err)
@ -339,7 +341,7 @@ func TestSetWindowAlwaysOnTop_Good(t *testing.T) {
func TestSetWindowTitle_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("title-win"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "title-win"})
err := svc.SetWindowTitle("title-win", "New Title")
assert.NoError(t, err)
@ -348,8 +350,8 @@ func TestSetWindowTitle_Good(t *testing.T) {
func TestGetFocusedWindow_Good(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("win-a"))
_ = svc.OpenWindow(window.WithName("win-b"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-a"})
requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-b"})
_ = svc.FocusWindow("win-b")
focused := svc.GetFocusedWindow()
@ -359,7 +361,7 @@ func TestGetFocusedWindow_Good(t *testing.T) {
func TestGetFocusedWindow_Good_NoneSelected(t *testing.T) {
c := newTestConclave(t)
svc := core.MustServiceFor[*Service](c, "display")
_ = svc.OpenWindow(window.WithName("win-a"))
requireCreateWindow(t, svc, CreateWindowOptions{Name: "win-a"})
focused := svc.GetFocusedWindow()
assert.Equal(t, "", focused)
@ -454,7 +456,7 @@ menu:
show_dev_tools: false
`), 0o644))
s, _ := New()
s, _ := NewService()
s.loadConfigFrom(cfgPath)
// Verify configData was populated from file
@ -464,7 +466,7 @@ menu:
}
func TestLoadConfig_Bad_MissingFile(t *testing.T) {
s, _ := New()
s, _ := NewService()
s.loadConfigFrom(filepath.Join(t.TempDir(), "nonexistent.yaml"))
// Should not panic, configData stays at empty defaults
@ -477,7 +479,7 @@ func TestHandleConfigTask_Persists_Good(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.yaml")
s, _ := New()
s, _ := NewService()
s.loadConfigFrom(cfgPath) // Creates empty config (file doesn't exist yet)
// Simulate a TaskSaveConfig through the handler

View file

@ -14,13 +14,13 @@ import (
func TestSubsystem_Good_Name(t *testing.T) {
c, _ := core.New(core.WithServiceLock())
sub := New(c)
sub := NewSubsystem(c)
assert.Equal(t, "display", sub.Name())
}
func TestSubsystem_Good_RegisterTools(t *testing.T) {
c, _ := core.New(core.WithServiceLock())
sub := New(c)
sub := NewSubsystem(c)
// RegisterTools should not panic with a real mcp.Server
server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)
assert.NotPanics(t, func() { sub.RegisterTools(server) })
@ -34,7 +34,7 @@ type mockClipPlatform struct {
}
func (m *mockClipPlatform) Text() (string, bool) { return m.text, m.ok }
func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true }
func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true }
func TestMCP_Good_ClipboardRoundTrip(t *testing.T) {
c, err := core.New(

View file

@ -11,12 +11,17 @@ type Subsystem struct {
core *core.Core
}
// New(c) creates a display MCP subsystem backed by a Core instance.
// sub := mcp.New(c); sub.RegisterTools(server)
func New(c *core.Core) *Subsystem {
// NewSubsystem creates the display MCP bridge for a Core instance.
// sub := mcp.NewSubsystem(c); sub.RegisterTools(server)
func NewSubsystem(c *core.Core) *Subsystem {
return &Subsystem{core: c}
}
// Deprecated: use NewSubsystem(c).
func New(c *core.Core) *Subsystem {
return NewSubsystem(c)
}
func (s *Subsystem) Name() string { return "display" }
func (s *Subsystem) RegisterTools(server *mcp.Server) {

View file

@ -6,7 +6,6 @@ import (
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/gui/pkg/clipboard"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -7,7 +7,6 @@ import (
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/gui/pkg/contextmenu"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -6,7 +6,6 @@ import (
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/gui/pkg/environment"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

View file

@ -20,11 +20,13 @@ type Manager struct {
}
// NewManager creates a menu Manager.
// menu.NewManager(menu.NewWailsPlatform(app)).SetApplicationMenu([]menu.MenuItem{{Label: "File"}})
func NewManager(platform Platform) *Manager {
return &Manager{platform: platform}
}
// Build constructs a PlatformMenu from a tree of MenuItems.
// menu.NewManager(menu.NewWailsPlatform(app)).Build([]menu.MenuItem{{Label: "File"}})
func (m *Manager) Build(items []MenuItem) PlatformMenu {
menu := m.platform.NewMenu()
m.buildItems(menu, items)
@ -60,6 +62,7 @@ func (m *Manager) buildItems(menu PlatformMenu, items []MenuItem) {
}
// SetApplicationMenu builds and sets the application menu.
// menu.NewManager(menu.NewWailsPlatform(app)).SetApplicationMenu([]menu.MenuItem{{Label: "File"}})
func (m *Manager) SetApplicationMenu(items []MenuItem) {
menu := m.Build(items)
m.platform.SetApplicationMenu(menu)

View file

@ -21,6 +21,7 @@ type Manager struct {
}
// NewManager creates a systray Manager.
// systray.NewManager(systray.NewWailsPlatform(app)).Setup("Core", "Core")
func NewManager(platform Platform) *Manager {
return &Manager{
platform: platform,
@ -29,6 +30,7 @@ func NewManager(platform Platform) *Manager {
}
// Setup creates the system tray with default icon and tooltip.
// systray.NewManager(systray.NewWailsPlatform(app)).Setup("Core", "Core")
func (m *Manager) Setup(tooltip, label string) error {
m.tray = m.platform.NewTray()
if m.tray == nil {

View file

@ -43,6 +43,28 @@ type Options struct {
ConsoleLimit int // Max console messages per window (default: 1000)
}
func defaultOptions() Options {
return Options{
DebugURL: "http://localhost:9222",
Timeout: 30 * time.Second,
ConsoleLimit: 1000,
}
}
func normalizeOptions(options Options) Options {
defaults := defaultOptions()
if options.DebugURL == "" {
options.DebugURL = defaults.DebugURL
}
if options.Timeout == 0 {
options.Timeout = defaults.Timeout
}
if options.ConsoleLimit == 0 {
options.ConsoleLimit = defaults.ConsoleLimit
}
return options
}
type Service struct {
*core.ServiceRuntime[Options]
options Options
@ -52,18 +74,10 @@ type Service struct {
watcherSetup func(conn connector, windowName string) // called after connection creation
}
// Register binds the webview service to a Core instance.
// core.WithService(webview.Register())
// core.WithService(webview.Register(func(o *Options) { o.DebugURL = "http://localhost:9223" }))
func Register(optionFns ...func(*Options)) func(*core.Core) (any, error) {
o := Options{
DebugURL: "http://localhost:9222",
Timeout: 30 * time.Second,
ConsoleLimit: 1000,
}
for _, fn := range optionFns {
fn(&o)
}
// RegisterWithOptions binds the webview service to a Core instance using a declarative Options literal.
// core.WithService(webview.RegisterWithOptions(webview.Options{DebugURL: "http://localhost:9223", Timeout: 30 * time.Second, ConsoleLimit: 1000}))
func RegisterWithOptions(options Options) func(*core.Core) (any, error) {
o := normalizeOptions(options)
return func(c *core.Core) (any, error) {
svc := &Service{
ServiceRuntime: core.NewServiceRuntime[Options](c, o),
@ -76,6 +90,17 @@ func Register(optionFns ...func(*Options)) func(*core.Core) (any, error) {
}
}
// Deprecated: use RegisterWithOptions(webview.Options{DebugURL: "http://localhost:9223", Timeout: 30 * time.Second, ConsoleLimit: 1000}).
func Register(optionFns ...func(*Options)) func(*core.Core) (any, error) {
options := defaultOptions()
for _, fn := range optionFns {
if fn != nil {
fn(&options)
}
}
return RegisterWithOptions(options)
}
// defaultNewConn creates real go-webview connections.
func defaultNewConn(options Options) func(string, string) (connector, error) {
return func(debugURL, windowName string) (connector, error) {

View file

@ -37,21 +37,41 @@ type mockConnector struct {
consoleClearCalled bool
}
func (m *mockConnector) Navigate(url string) error { m.lastNavURL = url; return nil }
func (m *mockConnector) Click(sel string) error { m.lastClickSel = sel; return nil }
func (m *mockConnector) Type(sel, text string) error { m.lastTypeSel = sel; m.lastTypeText = text; return nil }
func (m *mockConnector) Hover(sel string) error { m.lastHoverSel = sel; return nil }
func (m *mockConnector) Select(sel, val string) error { m.lastSelectSel = sel; m.lastSelectVal = val; return nil }
func (m *mockConnector) Check(sel string, c bool) error { m.lastCheckSel = sel; m.lastCheckVal = c; return nil }
func (m *mockConnector) Evaluate(s string) (any, error) { return m.evalResult, nil }
func (m *mockConnector) Screenshot() ([]byte, error) { return m.screenshot, nil }
func (m *mockConnector) GetURL() (string, error) { return m.url, nil }
func (m *mockConnector) GetTitle() (string, error) { return m.title, nil }
func (m *mockConnector) Navigate(url string) error { m.lastNavURL = url; return nil }
func (m *mockConnector) Click(sel string) error { m.lastClickSel = sel; return nil }
func (m *mockConnector) Type(sel, text string) error {
m.lastTypeSel = sel
m.lastTypeText = text
return nil
}
func (m *mockConnector) Hover(sel string) error { m.lastHoverSel = sel; return nil }
func (m *mockConnector) Select(sel, val string) error {
m.lastSelectSel = sel
m.lastSelectVal = val
return nil
}
func (m *mockConnector) Check(sel string, c bool) error {
m.lastCheckSel = sel
m.lastCheckVal = c
return nil
}
func (m *mockConnector) Evaluate(s string) (any, error) { return m.evalResult, nil }
func (m *mockConnector) Screenshot() ([]byte, error) { return m.screenshot, nil }
func (m *mockConnector) GetURL() (string, error) { return m.url, nil }
func (m *mockConnector) GetTitle() (string, error) { return m.title, nil }
func (m *mockConnector) GetHTML(sel string) (string, error) { return m.html, nil }
func (m *mockConnector) ClearConsole() { m.consoleClearCalled = true }
func (m *mockConnector) Close() error { m.closed = true; return nil }
func (m *mockConnector) SetViewport(w, h int) error { m.lastViewportW = w; m.lastViewportH = h; return nil }
func (m *mockConnector) UploadFile(sel string, p []string) error { m.lastUploadSel = sel; m.lastUploadPaths = p; return nil }
func (m *mockConnector) ClearConsole() { m.consoleClearCalled = true }
func (m *mockConnector) Close() error { m.closed = true; return nil }
func (m *mockConnector) SetViewport(w, h int) error {
m.lastViewportW = w
m.lastViewportH = h
return nil
}
func (m *mockConnector) UploadFile(sel string, p []string) error {
m.lastUploadSel = sel
m.lastUploadPaths = p
return nil
}
func (m *mockConnector) QuerySelector(sel string) (*ElementInfo, error) {
if len(m.elements) > 0 {
@ -68,7 +88,7 @@ func (m *mockConnector) GetConsole() []ConsoleMessage { return m.console }
func newTestService(t *testing.T, mock *mockConnector) (*Service, *core.Core) {
t.Helper()
factory := Register()
factory := RegisterWithOptions(Options{})
c, err := core.New(core.WithService(factory), core.WithServiceLock())
require.NoError(t, err)
require.NoError(t, c.ServiceStartup(context.Background(), nil))
@ -78,7 +98,7 @@ func newTestService(t *testing.T, mock *mockConnector) (*Service, *core.Core) {
return svc, c
}
func TestRegister_Good(t *testing.T) {
func TestRegisterWithOptions_Good(t *testing.T) {
svc, _ := newTestService(t, &mockConnector{})
assert.NotNil(t, svc)
}

View file

@ -17,10 +17,9 @@ type QueryWindowByName struct{ Name string }
type QueryConfig struct{}
type TaskOpenWindow struct {
Window *Window
Options []WindowOption
}
// TaskOpenWindow opens a concrete Window descriptor.
// window.TaskOpenWindow{Window: &window.Window{Name: "settings", URL: "/", Width: 800, Height: 600}}
type TaskOpenWindow struct{ Window *Window }
type TaskCloseWindow struct{ Name string }

View file

@ -1,10 +1,11 @@
// pkg/window/options.go
package window
// WindowOption is a functional option applied to a Window descriptor.
// WindowOption is the compatibility layer for option-chain callers.
// Prefer a Window literal with Manager.CreateWindow.
type WindowOption func(*Window) error
// ApplyOptions creates a Window and applies all options in order.
// Deprecated: use Manager.CreateWindow(Window{Name: "settings", URL: "/", Width: 800, Height: 600}).
func ApplyOptions(options ...WindowOption) (*Window, error) {
w := &Window{}
for _, option := range options {
@ -18,6 +19,7 @@ func ApplyOptions(options ...WindowOption) (*Window, error) {
return w, nil
}
// Compatibility helpers for callers still using option chains.
func WithName(name string) WindowOption {
return func(w *Window) error { w.Name = name; return nil }
}

View file

@ -187,15 +187,10 @@ func (s *Service) primaryScreenArea() (int, int, int, int) {
}
func (s *Service) taskOpenWindow(t TaskOpenWindow) (any, bool, error) {
var (
pw PlatformWindow
err error
)
if t.Window != nil {
pw, err = s.manager.Create(t.Window)
} else {
pw, err = s.manager.Open(t.Options...)
if t.Window == nil {
return nil, true, coreerr.E("window.taskOpenWindow", "window descriptor is required", nil)
}
pw, err := s.manager.CreateWindow(*t.Window)
if err != nil {
return nil, true, err
}

View file

@ -49,10 +49,8 @@ func TestTaskTileWindows_Good_UsesPrimaryScreenSize(t *testing.T) {
},
})
_, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}})
require.NoError(t, err)
_, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}})
require.NoError(t, err)
_ = requireOpenWindow(t, c, Window{Name: "left", Width: 400, Height: 400})
_ = requireOpenWindow(t, c, Window{Name: "right", Width: 400, Height: 400})
_, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}})
require.NoError(t, err)
@ -82,8 +80,7 @@ func TestTaskSnapWindow_Good_UsesPrimaryScreenSize(t *testing.T) {
},
})
_, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("snap"), WithSize(400, 300)}})
require.NoError(t, err)
_ = requireOpenWindow(t, c, Window{Name: "snap", Width: 400, Height: 300})
_, handled, err := c.PERFORM(TaskSnapWindow{Name: "snap", Position: "left"})
require.NoError(t, err)
@ -107,10 +104,8 @@ func TestTaskTileWindows_Good_UsesPrimaryWorkAreaOrigin(t *testing.T) {
},
})
_, _, err := c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("left"), WithSize(400, 400)}})
require.NoError(t, err)
_, _, err = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("right"), WithSize(400, 400)}})
require.NoError(t, err)
_ = requireOpenWindow(t, c, Window{Name: "left", Width: 400, Height: 400})
_ = requireOpenWindow(t, c, Window{Name: "right", Width: 400, Height: 400})
_, handled, err := c.PERFORM(TaskTileWindows{Mode: "left-right", Windows: []string{"left", "right"}})
require.NoError(t, err)

View file

@ -22,6 +22,14 @@ func newTestWindowService(t *testing.T) (*Service, *core.Core) {
return svc, c
}
func requireOpenWindow(t *testing.T, c *core.Core, window Window) WindowInfo {
t.Helper()
result, handled, err := c.PERFORM(TaskOpenWindow{Window: &window})
require.NoError(t, err)
require.True(t, handled)
return result.(WindowInfo)
}
func TestRegister_Good(t *testing.T) {
svc, _ := newTestWindowService(t)
assert.NotNil(t, svc)
@ -39,15 +47,12 @@ func TestTaskOpenWindow_Good(t *testing.T) {
assert.Equal(t, "test", info.Name)
}
func TestTaskOpenWindow_OptionsFallback_Good(t *testing.T) {
func TestTaskOpenWindow_Bad_MissingWindow(t *testing.T) {
_, c := newTestWindowService(t)
result, handled, err := c.PERFORM(TaskOpenWindow{
Options: []WindowOption{WithName("test-fallback"), WithURL("/")},
})
require.NoError(t, err)
result, handled, err := c.PERFORM(TaskOpenWindow{})
assert.True(t, handled)
info := result.(WindowInfo)
assert.Equal(t, "test-fallback", info.Name)
assert.Error(t, err)
assert.Nil(t, result)
}
func TestTaskOpenWindow_Bad(t *testing.T) {
@ -60,8 +65,8 @@ func TestTaskOpenWindow_Bad(t *testing.T) {
func TestQueryWindowList_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("a")}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("b")}})
_ = requireOpenWindow(t, c, Window{Name: "a"})
_ = requireOpenWindow(t, c, Window{Name: "b"})
result, handled, err := c.QUERY(QueryWindowList{})
require.NoError(t, err)
@ -72,7 +77,7 @@ func TestQueryWindowList_Good(t *testing.T) {
func TestQueryWindowByName_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
result, handled, err := c.QUERY(QueryWindowByName{Name: "test"})
require.NoError(t, err)
@ -91,7 +96,7 @@ func TestQueryWindowByName_Bad(t *testing.T) {
func TestTaskCloseWindow_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskCloseWindow{Name: "test"})
require.NoError(t, err)
@ -111,7 +116,7 @@ func TestTaskCloseWindow_Bad(t *testing.T) {
func TestTaskSetPosition_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskSetPosition{Name: "test", X: 100, Y: 200})
require.NoError(t, err)
@ -125,7 +130,7 @@ func TestTaskSetPosition_Good(t *testing.T) {
func TestTaskSetSize_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskSetSize{Name: "test", Width: 800, Height: 600})
require.NoError(t, err)
@ -139,7 +144,7 @@ func TestTaskSetSize_Good(t *testing.T) {
func TestTaskMaximise_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskMaximise{Name: "test"})
require.NoError(t, err)
@ -154,10 +159,7 @@ func TestFileDrop_Good(t *testing.T) {
_, c := newTestWindowService(t)
// Open a window
result, _, _ := c.PERFORM(TaskOpenWindow{
Options: []WindowOption{WithName("drop-test")},
})
info := result.(WindowInfo)
info := requireOpenWindow(t, c, Window{Name: "drop-test"})
assert.Equal(t, "drop-test", info.Name)
// Capture broadcast actions
@ -190,7 +192,7 @@ func TestFileDrop_Good(t *testing.T) {
func TestTaskMinimise_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskMinimise{Name: "test"})
require.NoError(t, err)
@ -213,7 +215,7 @@ func TestTaskMinimise_Bad(t *testing.T) {
func TestTaskFocus_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskFocus{Name: "test"})
require.NoError(t, err)
@ -236,7 +238,7 @@ func TestTaskFocus_Bad(t *testing.T) {
func TestTaskRestore_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
// First maximise, then restore
_, _, _ = c.PERFORM(TaskMaximise{Name: "test"})
@ -267,7 +269,7 @@ func TestTaskRestore_Bad(t *testing.T) {
func TestTaskSetTitle_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskSetTitle{Name: "test", Title: "New Title"})
require.NoError(t, err)
@ -289,7 +291,7 @@ func TestTaskSetTitle_Bad(t *testing.T) {
func TestTaskSetAlwaysOnTop_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskSetAlwaysOnTop{Name: "test", AlwaysOnTop: true})
require.NoError(t, err)
@ -312,7 +314,7 @@ func TestTaskSetAlwaysOnTop_Bad(t *testing.T) {
func TestTaskSetBackgroundColour_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskSetBackgroundColour{
Name: "test", Red: 10, Green: 20, Blue: 30, Alpha: 40,
@ -337,7 +339,7 @@ func TestTaskSetBackgroundColour_Bad(t *testing.T) {
func TestTaskSetVisibility_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
_, handled, err := c.PERFORM(TaskSetVisibility{Name: "test", Visible: true})
require.NoError(t, err)
@ -366,7 +368,7 @@ func TestTaskSetVisibility_Bad(t *testing.T) {
func TestTaskFullscreen_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("test")}})
_ = requireOpenWindow(t, c, Window{Name: "test"})
// Enter fullscreen
_, handled, err := c.PERFORM(TaskFullscreen{Name: "test", Fullscreen: true})
@ -396,8 +398,8 @@ func TestTaskFullscreen_Bad(t *testing.T) {
func TestTaskSaveLayout_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(960, 1080), WithPosition(0, 0)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(960, 1080), WithPosition(960, 0)}})
_ = requireOpenWindow(t, c, Window{Name: "editor", Width: 960, Height: 1080, X: 0, Y: 0})
_ = requireOpenWindow(t, c, Window{Name: "terminal", Width: 960, Height: 1080, X: 960, Y: 0})
_, handled, err := c.PERFORM(TaskSaveLayout{Name: "coding"})
require.NoError(t, err)
@ -433,8 +435,8 @@ func TestTaskSaveLayout_Bad(t *testing.T) {
func TestTaskRestoreLayout_Good(t *testing.T) {
svc, c := newTestWindowService(t)
// Open windows
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(800, 600), WithPosition(0, 0)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(800, 600), WithPosition(0, 0)}})
_ = requireOpenWindow(t, c, Window{Name: "editor", Width: 800, Height: 600, X: 0, Y: 0})
_ = requireOpenWindow(t, c, Window{Name: "terminal", Width: 800, Height: 600, X: 0, Y: 0})
// Save a layout with specific positions
_, _, _ = c.PERFORM(TaskSaveLayout{Name: "coding"})
@ -483,8 +485,8 @@ func TestTaskRestoreLayout_Bad(t *testing.T) {
func TestTaskStackWindows_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("s1"), WithSize(800, 600)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("s2"), WithSize(800, 600)}})
_ = requireOpenWindow(t, c, Window{Name: "s1", Width: 800, Height: 600})
_ = requireOpenWindow(t, c, Window{Name: "s2", Width: 800, Height: 600})
_, handled, err := c.PERFORM(TaskStackWindows{Windows: []string{"s1", "s2"}, OffsetX: 25, OffsetY: 35})
require.NoError(t, err)
@ -501,8 +503,8 @@ func TestTaskStackWindows_Good(t *testing.T) {
func TestTaskApplyWorkflow_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("editor"), WithSize(800, 600)}})
_, _, _ = c.PERFORM(TaskOpenWindow{Options: []WindowOption{WithName("terminal"), WithSize(800, 600)}})
_ = requireOpenWindow(t, c, Window{Name: "editor", Width: 800, Height: 600})
_ = requireOpenWindow(t, c, Window{Name: "terminal", Width: 800, Height: 600})
_, handled, err := c.PERFORM(TaskApplyWorkflow{Workflow: "side-by-side"})
require.NoError(t, err)

View file

@ -49,6 +49,7 @@ type Manager struct {
}
// NewManager creates a window Manager with the given platform backend.
// window.NewManager(window.NewWailsPlatform(app))
func NewManager(platform Platform) *Manager {
return &Manager{
platform: platform,
@ -59,7 +60,7 @@ func NewManager(platform Platform) *Manager {
}
// NewManagerWithDir creates a window Manager with a custom config directory for state/layout persistence.
// Useful for testing or when the default config directory is not appropriate.
// window.NewManagerWithDir(window.NewMockPlatform(), t.TempDir())
func NewManagerWithDir(platform Platform, configDir string) *Manager {
return &Manager{
platform: platform,
@ -81,51 +82,70 @@ func (m *Manager) SetDefaultHeight(height int) {
}
}
// Open creates a window using functional options, applies saved state, and tracks it.
// Deprecated: use CreateWindow(Window{Name: "settings", URL: "/settings", Width: 800, Height: 600}).
func (m *Manager) Open(options ...WindowOption) (PlatformWindow, error) {
w, err := ApplyOptions(options...)
if err != nil {
return nil, coreerr.E("window.Manager.Open", "failed to apply options", err)
}
return m.Create(w)
return m.CreateWindow(*w)
}
// Create creates a window from a Window descriptor.
// CreateWindow creates a window from a Window descriptor.
// window.NewManager(window.NewWailsPlatform(app)).CreateWindow(window.Window{Name: "settings", URL: "/settings", Width: 800, Height: 600})
func (m *Manager) CreateWindow(spec Window) (PlatformWindow, error) {
_, pw, err := m.createWindow(spec)
return pw, err
}
// Deprecated: use CreateWindow(Window{Name: "settings", URL: "/settings", Width: 800, Height: 600}).
func (m *Manager) Create(w *Window) (PlatformWindow, error) {
if w.Name == "" {
w.Name = "main"
if w == nil {
return nil, coreerr.E("window.Manager.Create", "window descriptor is required", nil)
}
if w.Title == "" {
w.Title = "Core"
spec, pw, err := m.createWindow(*w)
if err != nil {
return nil, err
}
if w.Width == 0 {
*w = spec
return pw, nil
}
func (m *Manager) createWindow(spec Window) (Window, PlatformWindow, error) {
if spec.Name == "" {
spec.Name = "main"
}
if spec.Title == "" {
spec.Title = "Core"
}
if spec.Width == 0 {
if m.defaultWidth > 0 {
w.Width = m.defaultWidth
spec.Width = m.defaultWidth
} else {
w.Width = 1280
spec.Width = 1280
}
}
if w.Height == 0 {
if spec.Height == 0 {
if m.defaultHeight > 0 {
w.Height = m.defaultHeight
spec.Height = m.defaultHeight
} else {
w.Height = 800
spec.Height = 800
}
}
if w.URL == "" {
w.URL = "/"
if spec.URL == "" {
spec.URL = "/"
}
// Apply saved state if available
m.state.ApplyState(w)
// Apply saved state if available.
m.state.ApplyState(&spec)
pw := m.platform.CreateWindow(w.ToPlatformOptions())
pw := m.platform.CreateWindow(spec.ToPlatformOptions())
m.mu.Lock()
m.windows[w.Name] = pw
m.windows[spec.Name] = pw
m.mu.Unlock()
return pw, nil
return spec, pw, nil
}
// Get returns a tracked window by name.

View file

@ -91,39 +91,48 @@ func newTestManager() (*Manager, *mockPlatform) {
return m, p
}
func TestManager_Open_Good(t *testing.T) {
m, p := newTestManager()
pw, err := m.Open(WithName("test"), WithTitle("Test"), WithURL("/test"), WithSize(800, 600))
func requireCreateWindow(t *testing.T, m *Manager, w Window) PlatformWindow {
t.Helper()
pw, err := m.CreateWindow(w)
require.NoError(t, err)
return pw
}
func TestManager_CreateWindow_Good(t *testing.T) {
m, p := newTestManager()
pw := requireCreateWindow(t, m, Window{
Name: "test",
Title: "Test",
URL: "/test",
Width: 800,
Height: 600,
})
assert.NotNil(t, pw)
assert.Equal(t, "test", pw.Name())
assert.Len(t, p.windows, 1)
}
func TestManager_Open_Defaults_Good(t *testing.T) {
func TestManager_CreateWindow_Defaults_Good(t *testing.T) {
m, _ := newTestManager()
pw, err := m.Open()
require.NoError(t, err)
pw := requireCreateWindow(t, m, Window{})
assert.Equal(t, "main", pw.Name())
w, h := pw.Size()
assert.Equal(t, 1280, w)
assert.Equal(t, 800, h)
}
func TestManager_Open_CustomDefaults_Good(t *testing.T) {
func TestManager_CreateWindow_CustomDefaults_Good(t *testing.T) {
m, _ := newTestManager()
m.SetDefaultWidth(1440)
m.SetDefaultHeight(900)
pw, err := m.Open()
require.NoError(t, err)
pw := requireCreateWindow(t, m, Window{})
w, h := pw.Size()
assert.Equal(t, 1440, w)
assert.Equal(t, 900, h)
}
func TestManager_Open_Bad(t *testing.T) {
func TestManager_Open_Compatibility_Bad(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(func(w *Window) error { return assert.AnError })
assert.Error(t, err)
@ -131,7 +140,7 @@ func TestManager_Open_Bad(t *testing.T) {
func TestManager_Get_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("findme"))
_ = requireCreateWindow(t, m, Window{Name: "findme"})
pw, ok := m.Get("findme")
assert.True(t, ok)
assert.Equal(t, "findme", pw.Name())
@ -145,8 +154,8 @@ func TestManager_Get_Bad(t *testing.T) {
func TestManager_List_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("a"))
_, _ = m.Open(WithName("b"))
_ = requireCreateWindow(t, m, Window{Name: "a"})
_ = requireCreateWindow(t, m, Window{Name: "b"})
names := m.List()
assert.Len(t, names, 2)
assert.Contains(t, names, "a")
@ -155,7 +164,7 @@ func TestManager_List_Good(t *testing.T) {
func TestManager_Remove_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("temp"))
_ = requireCreateWindow(t, m, Window{Name: "temp"})
m.Remove("temp")
_, ok := m.Get("temp")
assert.False(t, ok)
@ -170,8 +179,8 @@ func TestTileMode_String_Good(t *testing.T) {
func TestManager_TileWindows_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("a"), WithSize(800, 600))
_, _ = m.Open(WithName("b"), WithSize(800, 600))
_ = requireCreateWindow(t, m, Window{Name: "a", Width: 800, Height: 600})
_ = requireCreateWindow(t, m, Window{Name: "b", Width: 800, Height: 600})
err := m.TileWindows(TileModeLeftRight, []string{"a", "b"}, 1920, 1080)
require.NoError(t, err)
a, _ := m.Get("a")
@ -190,7 +199,7 @@ func TestManager_TileWindows_Bad(t *testing.T) {
func TestManager_SnapWindow_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("snap"), WithSize(800, 600))
_ = requireCreateWindow(t, m, Window{Name: "snap", Width: 800, Height: 600})
err := m.SnapWindow("snap", SnapLeft, 1920, 1080)
require.NoError(t, err)
w, _ := m.Get("snap")
@ -202,8 +211,8 @@ func TestManager_SnapWindow_Good(t *testing.T) {
func TestManager_StackWindows_Good(t *testing.T) {
m, _ := newTestManager()
_, _ = m.Open(WithName("s1"), WithSize(800, 600))
_, _ = m.Open(WithName("s2"), WithSize(800, 600))
_ = requireCreateWindow(t, m, Window{Name: "s1", Width: 800, Height: 600})
_ = requireCreateWindow(t, m, Window{Name: "s2", Width: 800, Height: 600})
err := m.StackWindows([]string{"s1", "s2"}, 30, 30)
require.NoError(t, err)
s2, _ := m.Get("s2")
@ -244,10 +253,9 @@ func TestTileWindows_AllModes_Good(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(WithName("win"), WithSize(800, 600))
require.NoError(t, err)
_ = requireCreateWindow(t, m, Window{Name: "win", Width: 800, Height: 600})
err = m.TileWindows(tc.mode, []string{"win"}, screenW, screenH)
err := m.TileWindows(tc.mode, []string{"win"}, screenW, screenH)
require.NoError(t, err)
pw, ok := m.Get("win")
@ -290,10 +298,9 @@ func TestSnapWindow_AllPositions_Good(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(WithName("snap"), WithSize(tc.initW, tc.initH))
require.NoError(t, err)
_ = requireCreateWindow(t, m, Window{Name: "snap", Width: tc.initW, Height: tc.initH})
err = m.SnapWindow("snap", tc.pos, screenW, screenH)
err := m.SnapWindow("snap", tc.pos, screenW, screenH)
require.NoError(t, err)
pw, ok := m.Get("snap")
@ -313,8 +320,7 @@ func TestStackWindows_ThreeWindows_Good(t *testing.T) {
m, _ := newTestManager()
names := []string{"s1", "s2", "s3"}
for _, name := range names {
_, err := m.Open(WithName(name), WithSize(800, 600))
require.NoError(t, err)
_ = requireCreateWindow(t, m, Window{Name: name, Width: 800, Height: 600})
}
err := m.StackWindows(names, 30, 30)
@ -369,12 +375,10 @@ func TestApplyWorkflow_AllLayouts_Good(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m, _ := newTestManager()
_, err := m.Open(WithName("editor"), WithSize(800, 600))
require.NoError(t, err)
_, err = m.Open(WithName("terminal"), WithSize(800, 600))
require.NoError(t, err)
_ = requireCreateWindow(t, m, Window{Name: "editor", Width: 800, Height: 600})
_ = requireCreateWindow(t, m, Window{Name: "terminal", Width: 800, Height: 600})
err = m.ApplyWorkflow(tc.workflow, []string{"editor", "terminal"}, screenW, screenH)
err := m.ApplyWorkflow(tc.workflow, []string{"editor", "terminal"}, screenW, screenH)
require.NoError(t, err)
pw0, ok := m.Get("editor")