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 }
 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}