prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

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 }