layout_test.go 3.1 KB

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