diff --git a/docs/framework/display.md b/docs/framework/display.md index 1d586cb..68b824e 100644 --- a/docs/framework/display.md +++ b/docs/framework/display.md @@ -5,7 +5,7 @@ Complete API reference for the Display service (`pkg/display`). ## Service Creation ```go -func NewService() (*Service, error) +func NewService() *Service ``` ## Window Management diff --git a/pkg/display/display.go b/pkg/display/display.go index 1c26c69..775da23 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -42,25 +42,23 @@ type Service struct { events *WebSocketEventManager } -// NewService constructs the display service. -func NewService() (*Service, error) { +// Display services start with an empty config cache. +// svc := display.NewService() +func NewService() *Service { return &Service{ configData: map[string]map[string]any{ "window": {}, "systray": {}, "menu": {}, }, - }, nil + } } -// Register creates a factory closure that captures the Wails app. -// Pass nil for testing without a Wails runtime. +// Build a Core factory without an option chain. +// factory := display.Register(nil) func Register(wailsApp *application.App) func(*core.Core) (any, error) { return func(c *core.Core) (any, error) { - s, err := NewService() - if err != nil { - return nil, err - } + s := NewService() s.ServiceRuntime = core.NewServiceRuntime[Options](c, Options{}) s.wailsApp = wailsApp return s, nil diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 871c6e4..0622748 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -48,16 +48,13 @@ func newTestConclave(t *testing.T) *core.Core { func TestNewService(t *testing.T) { t.Run("creates service successfully", func(t *testing.T) { - service, err := NewService() - assert.NoError(t, err) + service := NewService() assert.NotNil(t, service, "NewService() should return a non-nil service instance") }) t.Run("returns independent instances", func(t *testing.T) { - service1, err1 := NewService() - service2, err2 := NewService() - assert.NoError(t, err1) - assert.NoError(t, err2) + service1 := NewService() + service2 := NewService() assert.NotSame(t, service1, service2, "NewService() should return different instances") }) } @@ -500,7 +497,7 @@ menu: show_dev_tools: false `), 0o644)) - s, _ := NewService() + s := NewService() s.loadConfigFrom(cfgPath) // Verify configData was populated from file @@ -510,7 +507,7 @@ menu: } func TestLoadConfig_Bad_MissingFile(t *testing.T) { - s, _ := NewService() + s := NewService() s.loadConfigFrom(filepath.Join(t.TempDir(), "nonexistent.yaml")) // Should not panic, configData stays at empty defaults @@ -523,7 +520,7 @@ func TestHandleConfigTask_Persists_Good(t *testing.T) { dir := t.TempDir() cfgPath := filepath.Join(dir, "config.yaml") - s, _ := NewService() + s := NewService() s.loadConfigFrom(cfgPath) // Creates empty config (file doesn't exist yet) // Simulate a TaskSaveConfig through the handler diff --git a/pkg/display/docs/backend.md b/pkg/display/docs/backend.md index 372ec40..ccb08b3 100644 --- a/pkg/display/docs/backend.md +++ b/pkg/display/docs/backend.md @@ -8,7 +8,7 @@ The backend is written in Go and uses the `forge.lthn.ai/core/gui/pkg/display` p The `Service` struct is the main entry point for the display logic. - **Initialization:** - - `NewService() (*Service, error)`: Creates a new instance of the service. + - `NewService() *Service`: Creates a new instance of the service. - `Register(wailsApp *application.App) func(*core.Core) (any, error)`: Captures the Wails app and registers the service with Core. - `OnStartup(ctx context.Context) error`: Loads config and registers IPC handlers. diff --git a/pkg/webview/service.go b/pkg/webview/service.go index 7be174d..485a0f1 100644 --- a/pkg/webview/service.go +++ b/pkg/webview/service.go @@ -54,28 +54,35 @@ type Service struct { watcherSetup func(conn connector, windowName string) // called after connection creation } -// Register creates a factory closure with the given options. -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) - } +// Build a Core factory from one declarative Options value. +// factory := webview.Register(webview.Options{DebugURL: "http://localhost:9333"}) +func Register(options Options) func(*core.Core) (any, error) { + options = defaultOptions(options) return func(c *core.Core) (any, error) { svc := &Service{ - ServiceRuntime: core.NewServiceRuntime[Options](c, o), - options: o, + ServiceRuntime: core.NewServiceRuntime[Options](c, options), + options: options, connections: make(map[string]connector), - newConn: defaultNewConn(o), + newConn: defaultNewConn(options), } svc.watcherSetup = svc.defaultWatcherSetup return svc, nil } } +func defaultOptions(options Options) Options { + if options.DebugURL == "" { + options.DebugURL = "http://localhost:9222" + } + if options.Timeout <= 0 { + options.Timeout = 30 * time.Second + } + if options.ConsoleLimit <= 0 { + options.ConsoleLimit = 1000 + } + return options +} + // defaultNewConn creates real go-webview connections. func defaultNewConn(options Options) func(string, string) (connector, error) { return func(debugURL, windowName string) (connector, error) { diff --git a/pkg/webview/service_test.go b/pkg/webview/service_test.go index 45a8f20..ec1ef70 100644 --- a/pkg/webview/service_test.go +++ b/pkg/webview/service_test.go @@ -4,6 +4,7 @@ package webview import ( "context" "testing" + "time" "forge.lthn.ai/core/go/pkg/core" "forge.lthn.ai/core/gui/pkg/window" @@ -37,21 +38,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 +89,7 @@ func (m *mockConnector) GetConsole() []ConsoleMessage { return m.console } func newTestService(t *testing.T, mock *mockConnector) (*Service, *core.Core) { t.Helper() - factory := Register() + factory := Register(Options{}) c, err := core.New(core.WithService(factory), core.WithServiceLock()) require.NoError(t, err) require.NoError(t, c.ServiceStartup(context.Background(), nil)) @@ -83,6 +104,22 @@ func TestRegister_Good(t *testing.T) { assert.NotNil(t, svc) } +func TestRegister_Good_UsesOptions(t *testing.T) { + options := Options{ + DebugURL: "http://localhost:9333", + Timeout: 45 * time.Second, + ConsoleLimit: 12, + } + + factory := Register(options) + 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, "webview") + assert.Equal(t, options, svc.options) +} + func TestQueryURL_Good(t *testing.T) { _, c := newTestService(t, &mockConnector{url: "https://example.com"}) result, handled, err := c.QUERY(QueryURL{Window: "main"})