Files
MujicaUI/chat/slash_command_menu_test.go
T
ZacharyZhang-NYandClaude Opus 5.5 bcce34cda1 Upgrade MyGo to v0.5.5
MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:

- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
  elements expire and Context methods are inert: read IsDisabled into a
  local during the build, keep a ui.Handle for Focus, use c.Services()
  for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
  content builds, so same-frame autofocus lost focus restore on close.
  It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
  c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
  split button tooltip no longer reorders its open menu; disabled
  menubar rows pass presses to the panel; Form defers blur-started
  async checks so the growing message no longer moves Submit away
  from the release; Combobox drops its disabled copy now that MyGo
  blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
  time.Sleep/time.Now and read element state inside the view. The web
  mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.

Snapshot goldens are darwin-only and were not re-verified here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 01:37:54 -04:00

187 lines
5.1 KiB
Go

package chat
import (
"fmt"
"testing"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
)
var slashCommandsSample = []SlashCommand{
{Name: "summarize", Description: "Summarize the thread"},
{Name: "search", Description: "Search the web"},
{Name: "translate", Description: "Translate to French"},
}
// slashTester records command choices and prompt submissions.
type slashTester struct {
text string
chosen []string
sends int
off bool
}
func (s *slashTester) view(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Column(c).Fill().Justify(ui.End).Padding(16).Children(func() {
p := PromptComposer(c, &s.text, PromptComposerOptions{Label: "Prompt", Disabled: s.off})
if p.Submitted() {
s.sends++
}
if cmd, ok := SlashCommandMenu(c, p, &s.text, slashCommandsSample).Chosen(); ok {
s.chosen = append(s.chosen, cmd.Name)
}
})
}
func TestSlashCommandMenuFilterAndKeys(t *testing.T) {
s := &slashTester{}
tt := ui.NewTester(s.view, 500, 400)
_ = tt.Click("Prompt")
tt.Type("/s")
sr, _ := tt.Find("Search the web")
tr, ok := tt.Find("Translate to French")
if !ok || !tt.HasText("/summarize") || tr.Y <= sr.Y {
t.Fatalf("filter /s does not list prefix matches first: %q", tt.Texts())
}
tt.Type("e")
if tt.HasText("/summarize") || !tt.HasText("/search") {
t.Fatalf("filter /se: %q", tt.Texts())
}
tt.Key(0, ui.KeyBackspace)
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnter)
if s.text != "/search " || len(s.chosen) != 1 || s.chosen[0] != "search" || s.sends != 0 {
t.Fatalf("Enter: text %q chosen %v sends %d", s.text, s.chosen, s.sends)
}
if tt.HasText("/summarize") {
t.Fatal("menu stays open after choosing")
}
}
func TestSlashCommandMenuTabCompletesAndEscapeCloses(t *testing.T) {
s := &slashTester{}
tt := ui.NewTester(s.view, 500, 400)
_ = tt.Click("Prompt")
tt.Type("/tr")
tt.Key(0, ui.KeyTab)
if s.text != "/translate " || len(s.chosen) != 0 {
t.Fatalf("Tab: text %q chosen %v", s.text, s.chosen)
}
s.text = ""
tt.Frame()
tt.Type("/")
if !tt.HasText("/summarize") {
t.Fatal("menu did not open on /")
}
tt.Key(0, ui.KeyEscape)
if tt.HasText("/summarize") {
t.Fatal("Escape did not close the menu")
}
tt.Type("t")
if !tt.HasText("/translate") {
t.Fatal("menu did not reopen as the filter changed")
}
}
func TestSlashCommandMenuClickAndEmpty(t *testing.T) {
s := &slashTester{}
tt := ui.NewTester(s.view, 500, 400)
_ = tt.Click("Prompt")
tt.Type("/zz")
if !tt.HasText("No matching commands") {
t.Fatalf("empty state: %q", tt.Texts())
}
tt.Key(0, ui.KeyBackspace)
tt.Key(0, ui.KeyBackspace)
if err := tt.Click("/translate"); err != nil {
t.Fatal(err)
}
if len(s.chosen) != 1 || s.chosen[0] != "translate" || !tt.Focused("Prompt") {
t.Fatalf("click chose %v, composer focused %v", s.chosen, tt.Focused("Prompt"))
}
}
func TestSlashCommandMenuDisabledComposer(t *testing.T) {
s := &slashTester{text: "/s", off: true}
tt := ui.NewTester(s.view, 500, 400)
_ = tt.Click("Prompt")
tt.Key(0, ui.KeyEnter)
if tt.HasText("/search") || len(s.chosen) != 0 {
t.Fatal("disabled composer shows or uses the menu")
}
}
func TestSlashCommandMenuSnapshot(t *testing.T) {
snapshotAfter(t, "slash_command_menu", 520, 360, func(c *ui.Context) {
(*ui.Local(c.Root(), "s", func() *slashTester { return &slashTester{} })).view(c)
}, func(tt *ui.Tester) {
_ = tt.Click("Prompt")
tt.Type("/")
tt.Key(0, ui.KeyDown)
})
}
func TestSlashCommandMenuNarrowAndLargeData(t *testing.T) {
cmds := make([]SlashCommand, 100000)
for i := range cmds {
cmds[i] = SlashCommand{Name: fmt.Sprintf("cmd%d", i)}
}
text := "/"
shown, b := false, ui.Rect{}
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
p := PromptComposer(c, &text, PromptComposerOptions{Label: "Prompt"})
panel := SlashCommandMenu(c, p, &text, cmds).Element
shown, b = panel.Valid(), panel.Bounds()
}, 180, 120)
_ = tt.Click("Prompt")
tt.Frame()
if !shown {
t.Fatal("menu missing")
}
if b.X < 0 || b.Y < 0 || b.X+b.W > 180 || b.Y+b.H > 120 {
t.Errorf("popup outside window: %v", b)
}
if n := len(tt.Texts()); n > 150 {
t.Errorf("built %d labels for 100000 commands, want <= 150", n)
}
}
func TestSlashCommandMenuReopensAfterTriggerRemoved(t *testing.T) {
s := &slashTester{}
tt := ui.NewTester(s.view, 500, 400)
_ = tt.Click("Prompt")
tt.Type("/")
tt.Key(0, ui.KeyEscape)
tt.Key(0, ui.KeyBackspace)
tt.Type("/")
if !tt.HasText("/summarize") {
t.Fatal("retyped slash stayed dismissed")
}
}
func TestSlashCommandMenuQueryRevealsHighlight(t *testing.T) {
commands := make([]SlashCommand, 100)
for i := range commands {
commands[i] = SlashCommand{Name: fmt.Sprintf("cmd%03d", i)}
}
text := "/"
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Column(c).Fill().Justify(ui.End).Children(func() {
p := PromptComposer(c, &text, PromptComposerOptions{Label: "Prompt"})
SlashCommandMenu(c, p, &text, commands)
})
}, 500, 400)
_ = tt.Click("Prompt")
tt.Scroll(100, 150, 0, 1500)
tt.Type("c")
b, ok := tt.Find("/cmd000")
if !ok || b.H == 0 || b.Y < 0 || b.Y+b.H > 400 {
t.Fatalf("highlight outside view: %v found %v", b, ok)
}
}