layout_test.go

  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}