ai-agent/internal/tui/scroll_anchor_test.go
admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

226 lines
6.7 KiB
Go

package tui
import (
"testing"
tea "charm.land/bubbletea/v2"
"ai-agent/internal/agent"
"ai-agent/internal/command"
)
func TestScrollAnchor_Initialization(t *testing.T) {
m := newTestModel(t)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
m = updated.(*Model)
if !m.ready {
t.Fatal("viewport should be ready after WindowSizeMsg")
}
if !m.anchorActive {
t.Error("anchorActive should be true after initialization")
}
if m.scrollAnchor != 0 {
t.Errorf("scrollAnchor should be 0, got %d", m.scrollAnchor)
}
if m.lastContentHeight != 0 {
t.Errorf("lastContentHeight should be 0, got %d", m.lastContentHeight)
}
}
func TestScrollAnchor_MouseWheelUp(t *testing.T) {
m := newTestModel(t)
m.anchorActive = true
m.userScrolledUp = false
var longContent string
for i := 0; i < 100; i++ {
longContent += "line " + string(rune(i)) + "\n"
}
m.viewport.SetContent(longContent)
m.viewport.GotoBottom()
if !m.viewport.AtBottom() {
t.Fatal("viewport should be at bottom before scroll")
}
updated, _ := m.Update(tea.MouseWheelMsg{X: 0, Y: 0, Button: tea.MouseWheelUp})
m = updated.(*Model)
if m.anchorActive {
t.Error("anchorActive should be false after scrolling up")
}
if !m.userScrolledUp {
t.Error("userScrolledUp should be true after scrolling up")
}
if m.scrollAnchor <= 0 {
t.Error("scrollAnchor should be positive after scrolling up")
}
}
func TestScrollAnchor_MouseWheelDown(t *testing.T) {
m := newTestModel(t)
m.anchorActive = false
m.userScrolledUp = true
m.scrollAnchor = 10
m.viewport.SetContent("short content")
updated, _ := m.Update(tea.MouseWheelMsg{X: 0, Y: 0, Button: tea.MouseWheelDown})
m = updated.(*Model)
if !m.anchorActive {
t.Error("anchorActive should be true when at bottom")
}
}
func TestScrollAnchor_StreamTextMsg(t *testing.T) {
m := newTestModel(t)
m.state = StateStreaming
m.entries = []ChatEntry{
{Kind: "assistant", Content: "Initial response"},
}
m.viewport.SetContent(m.renderEntries())
m.anchorActive = true
updated, _ := m.Update(StreamTextMsg{Text: "more"})
m = updated.(*Model)
if !m.viewport.AtBottom() {
t.Error("viewport should be at bottom when anchor is active")
}
m.anchorActive = false
m.viewport.GotoTop()
updated, _ = m.Update(StreamTextMsg{Text: "even more"})
m = updated.(*Model)
if m.viewport.AtBottom() {
t.Log("Note: viewport scrolled to bottom even with anchor inactive")
}
}
func TestScrollAnchor_AgentDoneMsg(t *testing.T) {
m := newTestModel(t)
m.state = StateStreaming
m.anchorActive = false
m.userScrolledUp = true
m.scrollAnchor = 10
updated, _ := m.Update(AgentDoneMsg{})
m = updated.(*Model)
if m.state != StateIdle {
t.Errorf("state should be StateIdle, got %d", m.state)
}
if !m.anchorActive {
t.Error("anchorActive should be reset to true after AgentDoneMsg")
}
if m.scrollAnchor != 0 {
t.Errorf("scrollAnchor should be reset to 0, got %d", m.scrollAnchor)
}
if m.userScrolledUp {
t.Error("userScrolledUp should be reset to false")
}
}
func TestScrollAnchor_ToolMessages(t *testing.T) {
m := newTestModel(t)
m.state = StateStreaming
m.anchorActive = true
updated, _ := m.Update(ToolCallStartMsg{
Name: "read_file",
Args: map[string]any{"path": "test.go"},
StartTime: testTime,
})
m = updated.(*Model)
if !m.anchorActive {
t.Error("anchorActive should remain true after ToolCallStartMsg")
}
updated, _ = m.Update(ToolCallResultMsg{
Name: "read_file",
Result: "file content",
IsError: false,
Duration: testDuration,
})
m = updated.(*Model)
if !m.anchorActive {
t.Error("anchorActive should remain true after ToolCallResultMsg")
}
}
func TestScrollAnchor_SystemMessages(t *testing.T) {
m := newTestModel(t)
m.anchorActive = true
updated, _ := m.Update(SystemMessageMsg{Msg: "system message"})
m = updated.(*Model)
if !m.anchorActive {
t.Error("anchorActive should remain true after SystemMessageMsg")
}
updated, _ = m.Update(ErrorMsg{Msg: "error message"})
m = updated.(*Model)
if !m.anchorActive {
t.Error("anchorActive should remain true after ErrorMsg")
}
}
func TestScrollAnchor_WindowResize(t *testing.T) {
m := newTestModel(t)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
m = updated.(*Model)
if !m.anchorActive {
t.Fatal("anchorActive should be true after initial sizing")
}
}
func TestCheckAutoScroll_ReenablesAnchorAtBottom(t *testing.T) {
m := newTestModel(t)
m.anchorActive = false
m.userScrolledUp = true
m.scrollAnchor = 10
m.viewport.SetContent("short content")
m.viewport.GotoBottom()
m.checkAutoScroll()
if !m.anchorActive {
t.Error("checkAutoScroll should set anchorActive to true when at bottom")
}
if m.userScrolledUp {
t.Error("checkAutoScroll should set userScrolledUp to false when at bottom")
}
if m.scrollAnchor != 0 {
t.Error("checkAutoScroll should reset scrollAnchor to 0 when at bottom")
}
}
func TestScrollAnchor_ViewportAtBottom(t *testing.T) {
m := newTestModel(t)
m.viewport.SetContent("line1\nline2\nline3")
if !m.viewport.AtBottom() {
t.Error("viewport should be at bottom with short content")
}
var longContent string
for i := 0; i < 100; i++ {
longContent += "line " + string(rune(i)) + "\n"
}
m.viewport.SetContent(longContent)
m.viewport.GotoBottom()
if !m.viewport.AtBottom() {
t.Error("viewport should be at bottom after GotoBottom()")
}
m.viewport.GotoTop()
if m.viewport.AtBottom() {
t.Error("viewport should not be at bottom after scrolling to top")
}
}
func BenchmarkScrollAnchor_Performance(b *testing.B) {
m := newTestModelB(b)
m.anchorActive = true
var longContent string
for i := 0; i < 100; i++ {
longContent += "line " + string(rune(i)) + "\n"
}
m.viewport.SetContent(longContent)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = m.viewport.AtBottom()
}
}
func newTestModelB(b *testing.B) *Model {
reg := command.NewRegistry()
command.RegisterBuiltins(reg)
completer := NewCompleter(reg, []string{"model-a", "model-b"}, []string{"skill-a"}, []string{"agent-x"}, nil)
ag := agent.New(nil, nil, 0)
m := New(ag, reg, nil, completer, nil, nil, nil)
m.initializing = false
updated, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
return updated.(*Model)
}