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