hscroll_test.go (6870B)
1 package tui 2 3 import ( 4 "strings" 5 "testing" 6 7 tea "github.com/charmbracelet/bubbletea" 8 "github.com/charmbracelet/x/ansi" 9 ) 10 11 const wideReadme = "# janitor\n\nsome prose that is long enough to wrap around the pane width for sure, yes indeed it is long. " + 12 "some prose that is long enough to wrap around the pane width for sure, yes indeed it is long.\n\n" + 13 "```\n" + 14 "+--------+ +-------------------------+ +--------------------+ +----------+ +-------+ END\n" + 15 "| client | ---> | server with a long name | ---> | queue | ---> | db |\n" + 16 "```\n\nnarrow:\n\n```\nok\n```\n" 17 18 func TestWideBlocksDoNotWrap(t *testing.T) { 19 out := ansi.Strip(renderMarkdown(wideReadme, 40)) 20 if !strings.Contains(out, "┃ +--------+ +-------------------------+ +--------------------+ +----------+ +-------+ END") { 21 t.Fatalf("wide block wrapped:\n%s", out) 22 } 23 for _, l := range strings.Split(out, "\n") { 24 if strings.Contains(l, "prose") && ansi.StringWidth(l) > 40 { 25 t.Fatalf("prose did not wrap: %q", l) 26 } 27 } 28 if !strings.Contains(out, "┃ ok") { 29 t.Fatalf("narrow block lost:\n%s", out) 30 } 31 if strings.Contains(out, wideBlockToken) { 32 t.Fatalf("placeholder leaked:\n%s", out) 33 } 34 } 35 36 func TestIndentedListContinuationIsNotCode(t *testing.T) { 37 md := "- item\n\n continuation of the list item that is quite long and keeps going past forty columns\n" 38 src, blocks := extractWideBlocks(md, 40) 39 if len(blocks) != 0 || src != md { 40 t.Fatalf("list continuation extracted: %q %v", src, blocks) 41 } 42 } 43 44 func TestReadmeHorizontalScroll(t *testing.T) { 45 m := testModel(t, 100, 40) 46 id := m.projects[0].ID 47 m.projectReadmes[id] = wideReadme 48 m.activateTab(tabProjects) 49 m.handleKeyMsg(tea.KeyMsg{Type: tea.KeyEnter}) 50 51 view := ansi.Strip(m.View()) 52 if !strings.Contains(view, "›") { 53 t.Fatalf("no right cut marker:\n%s", view) 54 } 55 if strings.Contains(view, "END") { 56 t.Fatal("END visible before scrolling") 57 } 58 for i := 0; i < 10; i++ { 59 m.handleKeyMsg(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'l'}}) 60 } 61 view = ansi.Strip(m.View()) 62 if !strings.Contains(view, "END") || !strings.Contains(view, "‹") { 63 t.Fatalf("scrolling right did not reveal END:\n%s", view) 64 } 65 for _, l := range strings.Split(view, "\n") { 66 if ansi.StringWidth(l) > 100 { 67 t.Fatalf("line overflows: %d", ansi.StringWidth(l)) 68 } 69 } 70 for i := 0; i < 20; i++ { 71 m.handleKeyMsg(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'h'}}) 72 } 73 if m.detailXOffset != 0 { 74 t.Fatalf("x offset = %d", m.detailXOffset) 75 } 76 // left still leaves readme focus 77 m.handleKeyMsg(tea.KeyMsg{Type: tea.KeyLeft}) 78 if m.projectReadmeScroll { 79 t.Fatal("left no longer backs out of the readme") 80 } 81 } 82 83 // readmeModel returns a model on the projects tab with the wide readme 84 // rendered and the readme pane focused. 85 func readmeModel(t *testing.T) portfolioModel { 86 t.Helper() 87 m := testModel(t, 100, 40) 88 id := m.projects[0].ID 89 m.projectReadmes[id] = wideReadme 90 m.activateTab(tabProjects) 91 m = send(t, m, tea.KeyMsg{Type: tea.KeyEnter}) 92 if !m.projectReadmeScroll { 93 t.Fatal("enter did not focus the readme") 94 } 95 if m.detailMaxXOffset() <= 0 { 96 t.Fatalf("detailMaxXOffset = %d for a wide code block", m.detailMaxXOffset()) 97 } 98 return m 99 } 100 101 func send(t *testing.T, m portfolioModel, msg tea.Msg) portfolioModel { 102 t.Helper() 103 next, _ := m.Update(msg) 104 return next.(portfolioModel) 105 } 106 107 func TestReadmeArrowKeysScrollSideways(t *testing.T) { 108 m := readmeModel(t) 109 maxX := m.detailMaxXOffset() 110 111 m = send(t, m, tea.KeyMsg{Type: tea.KeyRight}) 112 if m.detailXOffset != detailHorizontalStep || m.activeTab != tabProjects { 113 t.Fatalf("right: x=%d tab=%d", m.detailXOffset, m.activeTab) 114 } 115 for i := 0; i < 50; i++ { 116 m = send(t, m, tea.KeyMsg{Type: tea.KeyRight}) 117 } 118 if m.detailXOffset != maxX || m.activeTab != tabProjects || !m.projectReadmeScroll { 119 t.Fatalf("right to the end: x=%d (max %d) tab=%d focus=%v", m.detailXOffset, maxX, m.activeTab, m.projectReadmeScroll) 120 } 121 if view := ansi.Strip(m.View()); !strings.Contains(view, "END") { 122 t.Fatalf("END not visible at max offset:\n%s", view) 123 } 124 125 m = send(t, m, tea.KeyMsg{Type: tea.KeyLeft}) 126 if m.detailXOffset != maxX-detailHorizontalStep || m.activeTab != tabProjects || !m.projectReadmeScroll { 127 t.Fatalf("left: x=%d tab=%d focus=%v", m.detailXOffset, m.activeTab, m.projectReadmeScroll) 128 } 129 for m.detailXOffset > 0 { 130 m = send(t, m, tea.KeyMsg{Type: tea.KeyLeft}) 131 if m.activeTab != tabProjects || !m.projectReadmeScroll { 132 t.Fatalf("left while panned changed tab/focus: x=%d tab=%d", m.detailXOffset, m.activeTab) 133 } 134 } 135 // left at the left edge backs out of the readme, still without a tab change 136 m = send(t, m, tea.KeyMsg{Type: tea.KeyLeft}) 137 if m.projectReadmeScroll || m.activeTab != tabProjects { 138 t.Fatalf("left at x=0: focus=%v tab=%d", m.projectReadmeScroll, m.activeTab) 139 } 140 // with the readme unfocused, arrows switch tabs again 141 m = send(t, m, tea.KeyMsg{Type: tea.KeyRight}) 142 if m.activeTab != tabAbout { 143 t.Fatalf("right outside the readme: tab=%d", m.activeTab) 144 } 145 } 146 147 func TestReadmeHLAndShiftArrowsScrollEndToEnd(t *testing.T) { 148 for _, keys := range []struct { 149 name string 150 right, left tea.KeyMsg 151 }{ 152 {"h/l", tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'l'}}, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'h'}}}, 153 {"shift+arrows", tea.KeyMsg{Type: tea.KeyShiftRight}, tea.KeyMsg{Type: tea.KeyShiftLeft}}, 154 } { 155 m := readmeModel(t) 156 maxX := m.detailMaxXOffset() 157 for i := 0; i < 50; i++ { 158 m = send(t, m, keys.right) 159 } 160 if m.detailXOffset != maxX || m.activeTab != tabProjects { 161 t.Fatalf("%s right: x=%d (max %d) tab=%d", keys.name, m.detailXOffset, maxX, m.activeTab) 162 } 163 for i := 0; i < 50; i++ { 164 m = send(t, m, keys.left) 165 } 166 if m.detailXOffset != 0 || m.activeTab != tabProjects || !m.projectReadmeScroll { 167 t.Fatalf("%s left: x=%d tab=%d focus=%v", keys.name, m.detailXOffset, m.activeTab, m.projectReadmeScroll) 168 } 169 } 170 } 171 172 func TestReadmeOffsetSurvivesAsyncRerender(t *testing.T) { 173 m := readmeModel(t) 174 for i := 0; i < 3; i++ { 175 m = send(t, m, tea.KeyMsg{Type: tea.KeyRight}) 176 } 177 m = send(t, m, tea.KeyMsg{Type: tea.KeyDown}) 178 x, y := m.detailXOffset, m.viewport.YOffset 179 if x == 0 { 180 t.Fatal("did not scroll") 181 } 182 183 id := m.projects[0].ID 184 m = send(t, m, projectReadmeMsg{projectID: id, readme: wideReadme}) 185 m = send(t, m, tea.WindowSizeMsg{Width: 100, Height: 40}) 186 m = send(t, m, tickMsg(m.now)) 187 if m.detailXOffset != x || m.viewport.YOffset != y { 188 t.Fatalf("re-render reset scroll: x=%d->%d y=%d->%d", x, m.detailXOffset, y, m.viewport.YOffset) 189 } 190 191 // changing the selected project resets it 192 m = send(t, m, tea.KeyMsg{Type: tea.KeyEsc}) 193 m = send(t, m, tea.KeyMsg{Type: tea.KeyDown}) 194 if m.detailXOffset != 0 || m.viewport.YOffset != 0 { 195 t.Fatalf("new project kept scroll: x=%d y=%d", m.detailXOffset, m.viewport.YOffset) 196 } 197 }