Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
6fab41703e
111 files changed
+22988
No files matched your search
@@ -0,0 +1,179 @@
|
||||
package panes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
|
||||
)
|
||||
|
||||
func newStringList(n int) List[string] {
|
||||
l := NewList("Items", Item[string]{
|
||||
ID: func(s string) string { return s },
|
||||
Filter: func(s string) string { return s },
|
||||
Render: func(_ styles.Styles, s string, _ int) string { return s },
|
||||
})
|
||||
var items []string
|
||||
for i := range n {
|
||||
items = append(items, fmt.Sprintf("item-%02d", i))
|
||||
}
|
||||
l.SetItems(items)
|
||||
l.SetSize(20, 7) // 5 rows
|
||||
return l
|
||||
}
|
||||
|
||||
func TestListScrollKeepsCursorVisible(t *testing.T) {
|
||||
l := newStringList(20)
|
||||
l.Move(9)
|
||||
view := ansi.Strip(l.View(styles.Default(), true, ""))
|
||||
if !strings.Contains(view, "item-09") || strings.Contains(view, "item-04") {
|
||||
t.Fatalf("cursor row not scrolled into view:\n%s", view)
|
||||
}
|
||||
// Moving up within the window must not scroll.
|
||||
l.Move(-2)
|
||||
view = ansi.Strip(l.View(styles.Default(), true, ""))
|
||||
if !strings.Contains(view, "item-05") {
|
||||
t.Fatalf("window jumped when moving up inside it:\n%s", view)
|
||||
}
|
||||
l.Top()
|
||||
if l.SelectedID() != "item-00" || l.offset != 0 {
|
||||
t.Fatalf("Top: selected %q offset %d", l.SelectedID(), l.offset)
|
||||
}
|
||||
l.Bottom()
|
||||
if l.SelectedID() != "item-19" || l.offset != 15 {
|
||||
t.Fatalf("Bottom: selected %q offset %d", l.SelectedID(), l.offset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSetItemsKeepsSelection(t *testing.T) {
|
||||
l := newStringList(5)
|
||||
l.Move(3)
|
||||
l.SetItems([]string{"new", "item-03", "item-04"})
|
||||
if l.SelectedID() != "item-03" {
|
||||
t.Fatalf("selected = %q", l.SelectedID())
|
||||
}
|
||||
l.SetItems([]string{"other"})
|
||||
if l.SelectedID() != "other" {
|
||||
t.Fatalf("selected = %q after item vanished", l.SelectedID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoxExactSize(t *testing.T) {
|
||||
out := Box(styles.Default(), "a very long title that will not fit", "footer", []string{"x", strings.Repeat("y", 50)}, 12, 5, true)
|
||||
lines := strings.Split(out, "\n")
|
||||
if len(lines) != 5 {
|
||||
t.Fatalf("height = %d", len(lines))
|
||||
}
|
||||
for _, line := range lines {
|
||||
if w := ansi.StringWidth(line); w != 12 {
|
||||
t.Errorf("width %d: %q", w, ansi.Strip(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterList(t *testing.T, items []string, query string) List[string] {
|
||||
t.Helper()
|
||||
l := NewList("Items", Item[string]{
|
||||
ID: func(s string) string { return s },
|
||||
Filter: func(s string) string { return s },
|
||||
Render: func(_ styles.Styles, s string, _ int) string { return s },
|
||||
})
|
||||
l.SetItems(items)
|
||||
l.SetSize(40, 10)
|
||||
l.StartFilter()
|
||||
for _, r := range query {
|
||||
l.UpdateFilter(tea.KeyPressMsg{Code: r, Text: string(r)})
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func visibleNames(l List[string]) []string {
|
||||
var out []string
|
||||
for _, i := range l.visible {
|
||||
out = append(out, l.items[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestFuzzyFilterRanksAndSkips(t *testing.T) {
|
||||
l := filterList(t, []string{"api-key", "db-password", "storage-key", "dbx-pat"}, "dbpw")
|
||||
if got := visibleNames(l); !reflect.DeepEqual(got, []string{"db-password"}) {
|
||||
t.Errorf("dbpw matched %v", got)
|
||||
}
|
||||
l = filterList(t, []string{"monkey", "key-vault", "api-key"}, "key")
|
||||
if got := visibleNames(l); got[0] != "key-vault" || len(got) != 3 {
|
||||
t.Errorf("prefix match not ranked first: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterHighlightsMatches(t *testing.T) {
|
||||
l := filterList(t, []string{"db-password", "api-key"}, "dpw")
|
||||
l.AcceptFilter()
|
||||
view := l.View(styles.Default(), false, "")
|
||||
// d, p and w are styled; with ANSI stripped the text is unchanged.
|
||||
if !strings.Contains(ansi.Strip(view), "db-password") {
|
||||
t.Fatalf("row missing:\n%s", ansi.Strip(view))
|
||||
}
|
||||
if hits := l.highlight(0, "db-password"); !reflect.DeepEqual(hits, []int{0, 3, 7}) {
|
||||
t.Errorf("hits = %v", hits)
|
||||
}
|
||||
// Rows that show the Filter text at an offset get shifted hits.
|
||||
if hits := l.highlight(0, " db-password (disabled)"); !reflect.DeepEqual(hits, []int{2, 5, 9}) {
|
||||
t.Errorf("shifted hits = %v", hits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterKeepsRowColours(t *testing.T) {
|
||||
blue := lipgloss.NewStyle().Foreground(lipgloss.Blue)
|
||||
match := styles.Default().Match
|
||||
|
||||
if got := matchRanges("platform-dev", []int{9, 10, 11}, match); len(got) != 1 || got[0].Start != 9 || got[0].End != 12 {
|
||||
t.Errorf("ranges = %+v, want one range 9-12", got)
|
||||
}
|
||||
|
||||
// Only the matches change style; the rest keeps the row's colour.
|
||||
styled := " " + blue.Render("platform-dev")
|
||||
out := lipgloss.StyleRanges(styled, matchRanges(" platform-dev", []int{11, 12, 13}, match)...)
|
||||
if ansi.Strip(out) != " platform-dev" {
|
||||
t.Fatalf("text changed: %q", ansi.Strip(out))
|
||||
}
|
||||
if !strings.Contains(out, "\x1b[34m") {
|
||||
t.Errorf("unmatched text lost its colour: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCounterInBottomRight(t *testing.T) {
|
||||
out := BoxCounted(styles.Default(), "T", "footer", "3 of 12", []string{"x"}, 30, 3, true)
|
||||
bottom := ansi.Strip(strings.Split(out, "\n")[2])
|
||||
if !strings.HasSuffix(bottom, " 3 of 12 ─╯") || !strings.HasPrefix(bottom, "╰─ footer ") || ansi.StringWidth(bottom) != 30 {
|
||||
t.Errorf("bottom edge = %q", bottom)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighlightKeepsColours(t *testing.T) {
|
||||
blue := lipgloss.NewStyle().Foreground(lipgloss.Blue)
|
||||
sel := lipgloss.NewStyle().Background(lipgloss.BrightBlack).Bold(true)
|
||||
row := " " + blue.Render("platform-prod") + lipgloss.NewStyle().Faint(true).Render(" note")
|
||||
out := Highlight(row, sel, 30)
|
||||
if ansi.StringWidth(out) != 30 || ansi.Strip(out) != ansi.Strip(row)+strings.Repeat(" ", 30-ansi.StringWidth(row)) {
|
||||
t.Fatalf("text or width changed: %q", ansi.Strip(out))
|
||||
}
|
||||
if !strings.Contains(out, "\x1b[34m") {
|
||||
t.Error("row lost its blue")
|
||||
}
|
||||
// The background is re-applied after every reset, so it spans the row.
|
||||
resets := strings.Count(out, "\x1b[m") + strings.Count(out, "\x1b[0m")
|
||||
if bgs := strings.Count(out, "\x1b[100;1m"); bgs < resets {
|
||||
t.Errorf("background not re-applied after each reset: %d resets, %d backgrounds\n%q", resets, bgs, out)
|
||||
}
|
||||
// Reverse styles fall back to plain text.
|
||||
if got := Highlight(row, lipgloss.NewStyle().Reverse(true), 30); strings.Contains(got, "34m") {
|
||||
t.Error("reverse kept the colours")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user