gui/pkg/contextmenu/service_test.go
Snider 94c17b88c2 feat(contextmenu): add contextmenu core.Service with Platform interface and IPC
Completes full Wails v3 Manager API coverage through the IPC bus.
Service maintains in-memory registry, delegates to Platform for native
context menu operations, broadcasts ActionItemClicked on menu item clicks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:51:33 +00:00

299 lines
7.1 KiB
Go

// pkg/contextmenu/service_test.go
package contextmenu
import (
"context"
"sync"
"testing"
"forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockPlatform records Add/Remove calls and allows simulating clicks.
type mockPlatform struct {
mu sync.Mutex
menus map[string]ContextMenuDef
clickHandlers map[string]func(menuName, actionID, data string)
removed []string
addErr error
removeErr error
}
func newMockPlatform() *mockPlatform {
return &mockPlatform{
menus: make(map[string]ContextMenuDef),
clickHandlers: make(map[string]func(menuName, actionID, data string)),
}
}
func (m *mockPlatform) Add(name string, menu ContextMenuDef, onItemClick func(string, string, string)) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.addErr != nil {
return m.addErr
}
m.menus[name] = menu
m.clickHandlers[name] = onItemClick
return nil
}
func (m *mockPlatform) Remove(name string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.removeErr != nil {
return m.removeErr
}
delete(m.menus, name)
delete(m.clickHandlers, name)
m.removed = append(m.removed, name)
return nil
}
func (m *mockPlatform) Get(name string) (*ContextMenuDef, bool) {
m.mu.Lock()
defer m.mu.Unlock()
menu, ok := m.menus[name]
if !ok {
return nil, false
}
return &menu, true
}
func (m *mockPlatform) GetAll() map[string]ContextMenuDef {
m.mu.Lock()
defer m.mu.Unlock()
out := make(map[string]ContextMenuDef, len(m.menus))
for k, v := range m.menus {
out[k] = v
}
return out
}
// simulateClick simulates a context menu item click by calling the registered handler.
func (m *mockPlatform) simulateClick(menuName, actionID, data string) {
m.mu.Lock()
h, ok := m.clickHandlers[menuName]
m.mu.Unlock()
if ok {
h(menuName, actionID, data)
}
}
func newTestContextMenuService(t *testing.T, mp *mockPlatform) (*Service, *core.Core) {
t.Helper()
c, err := core.New(
core.WithService(Register(mp)),
core.WithServiceLock(),
)
require.NoError(t, err)
require.NoError(t, c.ServiceStartup(context.Background(), nil))
svc := core.MustServiceFor[*Service](c, "contextmenu")
return svc, c
}
func TestRegister_Good(t *testing.T) {
mp := newMockPlatform()
svc, _ := newTestContextMenuService(t, mp)
assert.NotNil(t, svc)
assert.NotNil(t, svc.platform)
}
func TestTaskAdd_Good(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
_, handled, err := c.PERFORM(TaskAdd{
Name: "file-menu",
Menu: ContextMenuDef{
Name: "file-menu",
Items: []MenuItemDef{
{Label: "Open", ActionID: "open"},
{Label: "Delete", ActionID: "delete"},
},
},
})
require.NoError(t, err)
assert.True(t, handled)
// Verify menu registered on platform
_, ok := mp.Get("file-menu")
assert.True(t, ok)
}
func TestTaskAdd_Good_ReplaceExisting(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
// Add initial menu
_, _, _ = c.PERFORM(TaskAdd{
Name: "ctx",
Menu: ContextMenuDef{Name: "ctx", Items: []MenuItemDef{{Label: "A", ActionID: "a"}}},
})
// Replace with new menu
_, handled, err := c.PERFORM(TaskAdd{
Name: "ctx",
Menu: ContextMenuDef{Name: "ctx", Items: []MenuItemDef{{Label: "B", ActionID: "b"}}},
})
require.NoError(t, err)
assert.True(t, handled)
// Verify registry has new menu
result, _, _ := c.QUERY(QueryGet{Name: "ctx"})
def := result.(*ContextMenuDef)
require.Len(t, def.Items, 1)
assert.Equal(t, "B", def.Items[0].Label)
}
func TestTaskRemove_Good(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
// Add then remove
_, _, _ = c.PERFORM(TaskAdd{
Name: "test",
Menu: ContextMenuDef{Name: "test"},
})
_, handled, err := c.PERFORM(TaskRemove{Name: "test"})
require.NoError(t, err)
assert.True(t, handled)
// Verify removed from registry
result, _, _ := c.QUERY(QueryGet{Name: "test"})
assert.Nil(t, result)
}
func TestTaskRemove_Bad_NotFound(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
_, handled, err := c.PERFORM(TaskRemove{Name: "nonexistent"})
assert.True(t, handled)
assert.ErrorIs(t, err, ErrMenuNotFound)
}
func TestQueryGet_Good(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
_, _, _ = c.PERFORM(TaskAdd{
Name: "my-menu",
Menu: ContextMenuDef{
Name: "my-menu",
Items: []MenuItemDef{{Label: "Edit", ActionID: "edit"}},
},
})
result, handled, err := c.QUERY(QueryGet{Name: "my-menu"})
require.NoError(t, err)
assert.True(t, handled)
def := result.(*ContextMenuDef)
assert.Equal(t, "my-menu", def.Name)
assert.Len(t, def.Items, 1)
}
func TestQueryGet_Good_NotFound(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
result, handled, err := c.QUERY(QueryGet{Name: "missing"})
require.NoError(t, err)
assert.True(t, handled)
assert.Nil(t, result)
}
func TestQueryList_Good(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
_, _, _ = c.PERFORM(TaskAdd{Name: "a", Menu: ContextMenuDef{Name: "a"}})
_, _, _ = c.PERFORM(TaskAdd{Name: "b", Menu: ContextMenuDef{Name: "b"}})
result, handled, err := c.QUERY(QueryList{})
require.NoError(t, err)
assert.True(t, handled)
list := result.(map[string]ContextMenuDef)
assert.Len(t, list, 2)
}
func TestQueryList_Good_Empty(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
result, handled, err := c.QUERY(QueryList{})
require.NoError(t, err)
assert.True(t, handled)
list := result.(map[string]ContextMenuDef)
assert.Len(t, list, 0)
}
func TestTaskAdd_Good_ClickBroadcast(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
// Capture broadcast actions
var clicked ActionItemClicked
var mu sync.Mutex
c.RegisterAction(func(_ *core.Core, msg core.Message) error {
if a, ok := msg.(ActionItemClicked); ok {
mu.Lock()
clicked = a
mu.Unlock()
}
return nil
})
_, _, _ = c.PERFORM(TaskAdd{
Name: "file-menu",
Menu: ContextMenuDef{
Name: "file-menu",
Items: []MenuItemDef{
{Label: "Open", ActionID: "open"},
},
},
})
// Simulate click via mock
mp.simulateClick("file-menu", "open", "file-123")
mu.Lock()
assert.Equal(t, "file-menu", clicked.MenuName)
assert.Equal(t, "open", clicked.ActionID)
assert.Equal(t, "file-123", clicked.Data)
mu.Unlock()
}
func TestTaskAdd_Good_SubmenuItems(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)
_, handled, err := c.PERFORM(TaskAdd{
Name: "nested",
Menu: ContextMenuDef{
Name: "nested",
Items: []MenuItemDef{
{Label: "File", Type: "submenu", Items: []MenuItemDef{
{Label: "New", ActionID: "new"},
{Label: "Open", ActionID: "open"},
}},
{Type: "separator"},
{Label: "Quit", ActionID: "quit"},
},
},
})
require.NoError(t, err)
assert.True(t, handled)
result, _, _ := c.QUERY(QueryGet{Name: "nested"})
def := result.(*ContextMenuDef)
assert.Len(t, def.Items, 3)
assert.Len(t, def.Items[0].Items, 2) // submenu children
}
func TestQueryList_Bad_NoService(t *testing.T) {
c, _ := core.New(core.WithServiceLock())
_, handled, _ := c.QUERY(QueryList{})
assert.False(t, handled)
}