226 lines
6.7 KiB
Go
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)
|
|
}
|