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>
168 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|