1package model
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7
8 "charm.land/bubbles/v2/textarea"
9 "github.com/charmbracelet/crush/internal/ui/chat"
10 "github.com/charmbracelet/crush/internal/ui/common"
11)
12
13// testMessageItem is a minimal chat item used to populate the chat list
14// without pulling in full message rendering machinery.
15type testMessageItem struct {
16 id string
17 text string
18}
19
20func (m testMessageItem) ID() string { return m.id }
21func (m testMessageItem) Render(int) string { return m.text }
22func (m testMessageItem) RawRender(int) string { return m.text }
23
24var _ chat.MessageItem = testMessageItem{}
25
26// newTestUI builds a focused uiChat model with dynamic textarea sizing enabled.
27// It intentionally keeps dependencies minimal so layout behavior can be tested
28// in isolation.
29func newTestUI() *UI {
30 com := common.DefaultCommon(nil)
31
32 ta := textarea.New()
33 ta.SetStyles(com.Styles.Editor.Textarea)
34 ta.ShowLineNumbers = false
35 ta.CharLimit = -1
36 ta.SetVirtualCursor(false)
37 ta.DynamicHeight = true
38 ta.MinHeight = TextareaMinHeight
39 ta.MaxHeight = TextareaMaxHeight
40 ta.Focus()
41
42 u := &UI{
43 com: com,
44 status: NewStatus(com, nil),
45 chat: NewChat(com),
46 textarea: ta,
47 state: uiChat,
48 focus: uiFocusEditor,
49 width: 140,
50 height: 45,
51 }
52
53 return u
54}
55
56func TestUpdateLayoutAndSize_EditorGrowthShrinksChat(t *testing.T) {
57 t.Parallel()
58
59 // Baseline layout at min textarea height.
60 u := newTestUI()
61 u.updateLayoutAndSize()
62
63 initialEditorHeight := u.layout.editor.Dy()
64 initialChatHeight := u.layout.main.Dy()
65
66 // Increase textarea content enough to trigger growth, then run the
67 // same resize hook used in the real update path.
68 prevHeight := u.textarea.Height()
69 u.textarea.SetValue(strings.Repeat("line\n", 8))
70 u.textarea.MoveToEnd()
71 _ = u.handleTextareaHeightChange(prevHeight)
72
73 if got := u.layout.editor.Dy(); got <= initialEditorHeight {
74 t.Fatalf("expected editor to grow: got %d, want > %d", got, initialEditorHeight)
75 }
76
77 if got := u.layout.main.Dy(); got >= initialChatHeight {
78 t.Fatalf("expected chat to shrink: got %d, want < %d", got, initialChatHeight)
79 }
80}
81
82func TestHandleTextareaHeightChange_FollowModeStaysAtBottom(t *testing.T) {
83 t.Parallel()
84
85 // Use enough messages to make the chat scrollable so AtBottom/Follow
86 // assertions are meaningful.
87 u := newTestUI()
88
89 msgs := make([]chat.MessageItem, 0, 60)
90 for i := range 60 {
91 msgs = append(msgs, testMessageItem{
92 id: "m-" + strconv.Itoa(i),
93 text: "message " + strconv.Itoa(i),
94 })
95 }
96 u.chat.SetMessages(msgs...)
97 u.updateLayoutAndSize()
98
99 // Enter follow mode and verify we're anchored at the bottom first.
100 u.chat.ScrollToBottom()
101 if !u.chat.AtBottom() {
102 t.Fatal("expected chat to start at bottom")
103 }
104
105 // Grow the editor; follow mode should keep the chat pinned to the end
106 // even as the chat viewport shrinks.
107 prevHeight := u.textarea.Height()
108 u.textarea.SetValue(strings.Repeat("line\n", 10))
109 u.textarea.MoveToEnd()
110 _ = u.handleTextareaHeightChange(prevHeight)
111
112 if !u.chat.Follow() {
113 t.Fatal("expected follow mode to remain enabled")
114 }
115 if !u.chat.AtBottom() {
116 t.Fatal("expected chat to remain at bottom after editor resize in follow mode")
117 }
118}