7 "github.com/charmbracelet/glamour/v2/ansi"
8 uv "github.com/charmbracelet/ultraviolet"
11func TestRenderHelper(t *testing.T) {
13 NewStringItem("Item 1"),
14 NewStringItem("Item 2"),
15 NewStringItem("Item 3"),
25 t.Error("expected non-empty output from Render()")
28 // Check that output contains the items
29 if !strings.Contains(output, "Item 1") {
30 t.Error("expected output to contain 'Item 1'")
32 if !strings.Contains(output, "Item 2") {
33 t.Error("expected output to contain 'Item 2'")
35 if !strings.Contains(output, "Item 3") {
36 t.Error("expected output to contain 'Item 3'")
40func TestRenderWithScrolling(t *testing.T) {
42 NewStringItem("Item 1"),
43 NewStringItem("Item 2"),
44 NewStringItem("Item 3"),
45 NewStringItem("Item 4"),
46 NewStringItem("Item 5"),
50 l.SetSize(80, 2) // Small viewport
52 // Initial render should show first 2 items
54 if !strings.Contains(output, "Item 1") {
55 t.Error("expected output to contain 'Item 1'")
57 if !strings.Contains(output, "Item 2") {
58 t.Error("expected output to contain 'Item 2'")
60 if strings.Contains(output, "Item 3") {
61 t.Error("expected output to NOT contain 'Item 3' in initial view")
64 // Scroll down and render
68 // Now should show items 3 and 4
69 if strings.Contains(output, "Item 1") {
70 t.Error("expected output to NOT contain 'Item 1' after scrolling")
72 if !strings.Contains(output, "Item 3") {
73 t.Error("expected output to contain 'Item 3' after scrolling")
75 if !strings.Contains(output, "Item 4") {
76 t.Error("expected output to contain 'Item 4' after scrolling")
80func TestRenderEmptyList(t *testing.T) {
86 t.Errorf("expected empty output for empty list, got: %q", output)
90func TestRenderVsDrawConsistency(t *testing.T) {
92 NewStringItem("Item 1"),
93 NewStringItem("Item 2"),
99 // Render using Render() method
100 renderOutput := l.Render()
102 // Render using Draw() method
103 screen := uv.NewScreenBuffer(80, 10)
104 area := uv.Rect(0, 0, 80, 10)
105 l.Draw(&screen, area)
106 drawOutput := screen.Render()
108 // Trim any trailing whitespace for comparison
109 renderOutput = strings.TrimRight(renderOutput, "\n")
110 drawOutput = strings.TrimRight(drawOutput, "\n")
112 // Both methods should produce the same output
113 if renderOutput != drawOutput {
114 t.Errorf("Render() and Draw() produced different outputs:\nRender():\n%q\n\nDraw():\n%q",
115 renderOutput, drawOutput)
119func BenchmarkRender(b *testing.B) {
120 items := make([]Item, 100)
121 for i := range items {
122 items[i] = NewStringItem("Item content here")
127 l.Render() // Prime the buffer
130 for i := 0; i < b.N; i++ {
135func BenchmarkRenderWithScrolling(b *testing.B) {
136 items := make([]Item, 1000)
137 for i := range items {
138 items[i] = NewStringItem("Item content here")
143 l.Render() // Prime the buffer
146 for i := 0; i < b.N; i++ {
152func TestStringItemCache(t *testing.T) {
153 item := NewStringItem("Test content")
155 // First draw at width 80 should populate cache
156 screen1 := uv.NewScreenBuffer(80, 5)
157 area1 := uv.Rect(0, 0, 80, 5)
158 item.Draw(&screen1, area1)
160 if len(item.cache) != 1 {
161 t.Errorf("expected cache to have 1 entry after first draw, got %d", len(item.cache))
163 if _, ok := item.cache[80]; !ok {
164 t.Error("expected cache to have entry for width 80")
167 // Second draw at same width should reuse cache
168 screen2 := uv.NewScreenBuffer(80, 5)
169 area2 := uv.Rect(0, 0, 80, 5)
170 item.Draw(&screen2, area2)
172 if len(item.cache) != 1 {
173 t.Errorf("expected cache to still have 1 entry after second draw, got %d", len(item.cache))
176 // Draw at different width should add to cache
177 screen3 := uv.NewScreenBuffer(40, 5)
178 area3 := uv.Rect(0, 0, 40, 5)
179 item.Draw(&screen3, area3)
181 if len(item.cache) != 2 {
182 t.Errorf("expected cache to have 2 entries after draw at different width, got %d", len(item.cache))
184 if _, ok := item.cache[40]; !ok {
185 t.Error("expected cache to have entry for width 40")
189func TestWrappingItemHeight(t *testing.T) {
190 // Short text that fits in one line
191 item1 := NewWrappingStringItem("Short")
192 if h := item1.Height(80); h != 1 {
193 t.Errorf("expected height 1 for short text, got %d", h)
196 // Long text that wraps
197 longText := "This is a very long line that will definitely wrap when constrained to a narrow width"
198 item2 := NewWrappingStringItem(longText)
200 // At width 80, should be fewer lines than width 20
201 height80 := item2.Height(80)
202 height20 := item2.Height(20)
204 if height20 <= height80 {
205 t.Errorf("expected more lines at narrow width (20: %d lines) than wide width (80: %d lines)",
209 // Non-wrapping version should always be 1 line
210 item3 := NewStringItem(longText)
211 if h := item3.Height(20); h != 1 {
212 t.Errorf("expected height 1 for non-wrapping item, got %d", h)
216func TestMarkdownItemBasic(t *testing.T) {
217 markdown := "# Hello\n\nThis is a **test**."
218 item := NewMarkdownItem(markdown)
220 // Test that height is calculated
221 height := item.Height(80)
223 t.Errorf("expected height >= 1, got %d", height)
227 screen := uv.NewScreenBuffer(80, 10)
228 area := uv.Rect(0, 0, 80, 10)
229 item.Draw(&screen, area)
231 // Should not panic and should render something
232 rendered := screen.Render()
233 if len(rendered) == 0 {
234 t.Error("expected non-empty rendered output")
238func TestMarkdownItemCache(t *testing.T) {
239 markdown := "# Test\n\nSome content."
240 item := NewMarkdownItem(markdown)
242 // First render at width 80 should populate cache
243 height1 := item.Height(80)
244 if len(item.cache) != 1 {
245 t.Errorf("expected cache to have 1 entry after first render, got %d", len(item.cache))
248 // Second render at same width should reuse cache
249 height2 := item.Height(80)
250 if height1 != height2 {
251 t.Errorf("expected consistent height, got %d then %d", height1, height2)
253 if len(item.cache) != 1 {
254 t.Errorf("expected cache to still have 1 entry, got %d", len(item.cache))
257 // Render at different width should add to cache
259 if len(item.cache) != 2 {
260 t.Errorf("expected cache to have 2 entries after different width, got %d", len(item.cache))
264func TestMarkdownItemMaxCacheWidth(t *testing.T) {
265 markdown := "# Test\n\nSome content."
266 item := NewMarkdownItem(markdown).WithMaxWidth(50)
268 // Render at width 40 (below limit) - should cache at width 40
270 if len(item.cache) != 1 {
271 t.Errorf("expected cache to have 1 entry for width 40, got %d", len(item.cache))
274 // Render at width 80 (above limit) - should cap to 50 and cache
276 // Cache should have width 50 entry (capped from 80)
277 if len(item.cache) != 2 {
278 t.Errorf("expected cache to have 2 entries (40 and 50), got %d", len(item.cache))
280 if _, ok := item.cache[50]; !ok {
281 t.Error("expected cache to have entry for width 50 (capped from 80)")
284 // Render at width 100 (also above limit) - should reuse cached width 50
286 if len(item.cache) != 2 {
287 t.Errorf("expected cache to still have 2 entries (reusing 50), got %d", len(item.cache))
291func TestMarkdownItemWithStyleConfig(t *testing.T) {
292 markdown := "# Styled\n\nContent with **bold** text."
294 // Create a custom style config
295 styleConfig := ansi.StyleConfig{
296 Document: ansi.StyleBlock{
301 item := NewMarkdownItem(markdown).WithStyleConfig(styleConfig)
303 // Render should use the custom style
304 height := item.Height(80)
306 t.Errorf("expected height >= 1, got %d", height)
309 // Draw should work without panic
310 screen := uv.NewScreenBuffer(80, 10)
311 area := uv.Rect(0, 0, 80, 10)
312 item.Draw(&screen, area)
314 rendered := screen.Render()
315 if len(rendered) == 0 {
316 t.Error("expected non-empty rendered output with custom style")
320func TestMarkdownItemInList(t *testing.T) {
322 NewMarkdownItem("# First\n\nMarkdown item."),
323 NewMarkdownItem("# Second\n\nAnother item."),
324 NewStringItem("Regular string item"),
330 // Should render without error
332 if len(output) == 0 {
333 t.Error("expected non-empty output from list with markdown items")
336 // Should contain content from markdown items
337 if !strings.Contains(output, "First") {
338 t.Error("expected output to contain 'First'")
340 if !strings.Contains(output, "Second") {
341 t.Error("expected output to contain 'Second'")
343 if !strings.Contains(output, "Regular string item") {
344 t.Error("expected output to contain 'Regular string item'")
348func TestMarkdownItemHeightWithWidth(t *testing.T) {
349 // Test that widths are capped to maxWidth
350 markdown := "This is a paragraph with some text."
352 item := NewMarkdownItem(markdown).WithMaxWidth(50)
354 // At width 30 (below limit), should cache and render at width 30
355 height30 := item.Height(30)
357 t.Errorf("expected height >= 1, got %d", height30)
360 // At width 100 (above maxWidth), should cap to 50 and cache
361 height100 := item.Height(100)
363 t.Errorf("expected height >= 1, got %d", height100)
366 // Both should be cached (width 30 and capped width 50)
367 if len(item.cache) != 2 {
368 t.Errorf("expected cache to have 2 entries (30 and 50), got %d", len(item.cache))
370 if _, ok := item.cache[30]; !ok {
371 t.Error("expected cache to have entry for width 30")
373 if _, ok := item.cache[50]; !ok {
374 t.Error("expected cache to have entry for width 50 (capped from 100)")
378func BenchmarkMarkdownItemRender(b *testing.B) {
379 markdown := "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- Item 1\n- Item 2\n- Item 3"
380 item := NewMarkdownItem(markdown)
383 screen := uv.NewScreenBuffer(80, 10)
384 area := uv.Rect(0, 0, 80, 10)
385 item.Draw(&screen, area)
388 for i := 0; i < b.N; i++ {
389 screen := uv.NewScreenBuffer(80, 10)
390 area := uv.Rect(0, 0, 80, 10)
391 item.Draw(&screen, area)
395func BenchmarkMarkdownItemUncached(b *testing.B) {
396 markdown := "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- Item 1\n- Item 2\n- Item 3"
399 for i := 0; i < b.N; i++ {
400 item := NewMarkdownItem(markdown)
401 screen := uv.NewScreenBuffer(80, 10)
402 area := uv.Rect(0, 0, 80, 10)
403 item.Draw(&screen, area)
407func TestSpacerItem(t *testing.T) {
408 spacer := NewSpacerItem(3)
411 if h := spacer.Height(80); h != 3 {
412 t.Errorf("expected height 3, got %d", h)
415 // Height should be constant regardless of width
416 if h := spacer.Height(20); h != 3 {
417 t.Errorf("expected height 3 for width 20, got %d", h)
420 // Draw should not produce any visible content
421 screen := uv.NewScreenBuffer(20, 3)
422 area := uv.Rect(0, 0, 20, 3)
423 spacer.Draw(&screen, area)
425 output := screen.Render()
426 // Should be empty (just spaces)
427 for _, line := range strings.Split(output, "\n") {
428 trimmed := strings.TrimSpace(line)
430 t.Errorf("expected empty spacer output, got: %q", line)
435func TestSpacerItemInList(t *testing.T) {
436 // Create a list with items separated by spacers
438 NewStringItem("Item 1"),
440 NewStringItem("Item 2"),
442 NewStringItem("Item 3"),
450 // Should contain all three items
451 if !strings.Contains(output, "Item 1") {
452 t.Error("expected output to contain 'Item 1'")
454 if !strings.Contains(output, "Item 2") {
455 t.Error("expected output to contain 'Item 2'")
457 if !strings.Contains(output, "Item 3") {
458 t.Error("expected output to contain 'Item 3'")
461 // Total height should be: 1 (item1) + 1 (spacer1) + 1 (item2) + 2 (spacer2) + 1 (item3) = 6
463 if l.TotalHeight() != expectedHeight {
464 t.Errorf("expected total height %d, got %d", expectedHeight, l.TotalHeight())
468func TestSpacerItemNavigation(t *testing.T) {
469 // Spacers should not be selectable (they're not focusable)
471 NewStringItem("Item 1"),
473 NewStringItem("Item 2"),
481 if l.SelectedIndex() != 0 {
482 t.Errorf("expected selected index 0, got %d", l.SelectedIndex())
485 // Can select the spacer (it's a valid item, just not focusable)
487 if l.SelectedIndex() != 1 {
488 t.Errorf("expected selected index 1, got %d", l.SelectedIndex())
491 // Can select item after spacer
493 if l.SelectedIndex() != 2 {
494 t.Errorf("expected selected index 2, got %d", l.SelectedIndex())
498// Helper function to create a pointer to uint
499func uintPtr(v uint) *uint {
503func TestListDoesNotEatLastLine(t *testing.T) {
504 // Create items that exactly fill the viewport
506 NewStringItem("Line 1"),
507 NewStringItem("Line 2"),
508 NewStringItem("Line 3"),
509 NewStringItem("Line 4"),
510 NewStringItem("Line 5"),
513 // Create list with height exactly matching content (5 lines, no gaps)
520 // Count actual lines in output
521 lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
523 for _, line := range lines {
524 if strings.TrimSpace(line) != "" {
529 // All 5 items should be visible
530 if !strings.Contains(output, "Line 1") {
531 t.Error("expected output to contain 'Line 1'")
533 if !strings.Contains(output, "Line 2") {
534 t.Error("expected output to contain 'Line 2'")
536 if !strings.Contains(output, "Line 3") {
537 t.Error("expected output to contain 'Line 3'")
539 if !strings.Contains(output, "Line 4") {
540 t.Error("expected output to contain 'Line 4'")
542 if !strings.Contains(output, "Line 5") {
543 t.Error("expected output to contain 'Line 5'")
546 if actualLineCount != 5 {
547 t.Errorf("expected 5 lines with content, got %d", actualLineCount)
551func TestListWithScrollDoesNotEatLastLine(t *testing.T) {
552 // Create more items than viewport height
554 NewStringItem("Item 1"),
555 NewStringItem("Item 2"),
556 NewStringItem("Item 3"),
557 NewStringItem("Item 4"),
558 NewStringItem("Item 5"),
559 NewStringItem("Item 6"),
560 NewStringItem("Item 7"),
563 // Viewport shows 3 items at a time
567 // Need to render first to build the buffer and calculate total height
570 // Now scroll to bottom
575 t.Logf("Output:\n%s", output)
576 t.Logf("Offset: %d, Total height: %d", l.offset, l.TotalHeight())
578 // Should show last 3 items: 5, 6, 7
579 if !strings.Contains(output, "Item 5") {
580 t.Error("expected output to contain 'Item 5'")
582 if !strings.Contains(output, "Item 6") {
583 t.Error("expected output to contain 'Item 6'")
585 if !strings.Contains(output, "Item 7") {
586 t.Error("expected output to contain 'Item 7'")
589 // Should not show earlier items
590 if strings.Contains(output, "Item 1") {
591 t.Error("expected output to NOT contain 'Item 1' when scrolled to bottom")
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list