Files
MujicaUI/chat/citation_badge_test.go
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

168 lines
4.7 KiB
Go

package chat
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
)
var sourcesSample = []Source{
{Title: "The Palace of Versailles", URL: "https://example.com/versailles", Site: "example.com",
Snippet: "Seat of the court from 1682.", Published: time.Date(2025, 5, 4, 0, 0, 0, 0, time.UTC)},
{Title: "Hall of Mirrors", URL: "https://example.org/hall", Snippet: "Seventeen arches of mirrors face the garden."},
{Title: "Court etiquette", URL: "https://example.net/etiquette", Site: "example.net"},
}
// citationApp shares state between inline citations and sources.
type citationApp struct {
st CitationState
opened []int
}
func (a *citationApp) view(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Row(c).Fill().Padding(16).Gap(16).AlignItems(ui.Start).Children(func() {
ui.Row(c).Gap(4).Children(func() {
ui.Text(c, "The court moved in 1682")
CitationBadge(c, 1, sourcesSample[0], &a.st)
CitationBadge(c, 3, sourcesSample[2], &a.st)
})
ui.Column(c).Width(300).Height(400).Children(func() {
if n, ok := SourcesPanel(c, sourcesSample, &a.st).Opened(); ok {
a.opened = append(a.opened, n)
}
})
})
}
func TestCitationBadgeActivatesAndPreviews(t *testing.T) {
a := &citationApp{}
tt := ui.NewTester(a.view, 900, 500)
r, ok := tt.Find("Source 3: Court etiquette")
if !ok {
t.Fatal("badge not named")
}
tt.Move(r.X+r.W/2, r.Y+r.H/2)
tt.Frame()
if a.st.hover != 3 {
t.Fatalf("hover %d, want 3", a.st.hover)
}
tt.Advance(600 * time.Millisecond)
if n := len(tt.Texts()); n == 0 || !tt.HasText("example.net") {
t.Fatalf("no preview card: %q", tt.Texts())
}
tt.ClickAt(r.X+r.W/2, r.Y+r.H/2)
if a.st.Active != 3 {
t.Fatalf("active %d", a.st.Active)
}
tt.Move(5, 480)
tt.Frame()
if a.st.hover != 0 {
t.Fatalf("hover %d after leaving", a.st.hover)
}
}
func TestCitationBadgeKeyboard(t *testing.T) {
a := &citationApp{}
tt := ui.NewTester(a.view, 900, 500)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnter)
if a.st.Active != 1 {
t.Fatalf("Enter on the first badge: active %d", a.st.Active)
}
}
func TestCitationBadgeBadNumber(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("no panic for number 0")
}
}()
var st CitationState
ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
CitationBadge(c, 0, sourcesSample[0], &st)
}, 100, 100)
}
func TestCitationBadgeSnapshot(t *testing.T) {
snapshot(t, "citation_badge", 360, 60, func(c *ui.Context) {
st := ui.Local(c.Root(), "st", func() CitationState { return CitationState{Active: 2} })
ui.Row(c).Padding(16).Gap(4).Children(func() {
ui.Text(c, "Seventeen arches")
CitationBadge(c, 1, sourcesSample[0], st)
CitationBadge(c, 2, sourcesSample[1], st)
})
})
}
func TestCitationBadgeHitAreaAndDisabledPreview(t *testing.T) {
var st CitationState
off := false
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Row(c).Padding(16).Disabled(off).Children(func() {
CitationBadge(c, 1, sourcesSample[0], &st)
})
}, 240, 160)
b, _ := tt.Find("Source 1: The Palace of Versailles")
if b.W < 28 || b.H < 28 {
t.Errorf("hit area %v, want at least 28x28", b)
}
tt.Key(0, ui.KeyTab)
tt.Advance(550 * time.Millisecond)
off = true
tt.Frame()
tt.Key(0, ui.KeyEnter)
if tt.HasText("Seat of the court from 1682.") || st.Active != 0 {
t.Errorf("disabled preview or activation: active %d texts %q", st.Active, tt.Texts())
}
}
func TestCitationBadgePreviewFitsNarrowWindow(t *testing.T) {
var st CitationState
var preview ui.Rect
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
src := sourcesSample[0]
ui.Row(c).Padding(8).Children(func() { CitationBadge(c, 1, src, &st) })
}, 180, 120)
tt.Key(0, ui.KeyTab)
tt.Advance(550 * time.Millisecond)
preview, ok := tt.Find("Source preview")
if !ok || preview.X < 0 || preview.X+preview.W > 180 || preview.Y+preview.H > 120 {
t.Fatalf("preview content outside window: %v found %v", preview, ok)
}
}
func TestCitationBadgeRemovedOwnerClearsHover(t *testing.T) {
show := true
st := CitationState{}
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Column(c).Children(func() {
ui.Row(c.Key("badge")).Height(36).Children(func() {
if show {
CitationBadge(c, 1, sourcesSample[0], &st)
}
})
ui.Row(c.Key("card")).Width(300).Children(func() { SourceCard(c, 1, sourcesSample[0], &st) })
})
}, 400, 300)
b, _ := tt.Find("Source 1: The Palace of Versailles")
tt.Move(b.X+b.W/2, b.Y+b.H/2)
tt.Frame()
if st.hover != 1 {
t.Fatal("hover did not activate")
}
show = false
tt.Move(390, 290)
tt.Frame()
if st.hover != 0 {
t.Fatalf("removed owner retained hover %d", st.hover)
}
}