mirror of
https://github.com/charmbracelet/bubbles.git
synced 2026-07-22 11:39:28 -06:00
Merge branch 'master' into v2-exp-table-tests
This commit is contained in:
commit
dffbe595a1
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -37,5 +37,5 @@ updates:
|
||||
labels:
|
||||
- "dependencies"
|
||||
commit-message:
|
||||
prefix: "feat"
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
|
||||
@ -30,10 +30,6 @@ linters:
|
||||
generated: lax
|
||||
presets:
|
||||
- common-false-positives
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
@ -43,7 +39,3 @@ formatters:
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
@ -10,11 +10,9 @@
|
||||
[](https://goreportcard.com/report/charmbracelet/bubbles)
|
||||
|
||||
Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea)
|
||||
applications. These components are used in production in [Glow][glow],
|
||||
[Charm][charm] and [many other applications][otherstuff].
|
||||
applications. These components are used in production in [Glow][glow], and [many other applications][otherstuff].
|
||||
|
||||
[glow]: https://github.com/charmbracelet/glow
|
||||
[charm]: https://github.com/charmbracelet/charm
|
||||
[otherstuff]: https://github.com/charmbracelet/bubbletea/#bubble-tea-in-the-wild
|
||||
|
||||
## Spinner
|
||||
|
||||
@ -184,11 +184,13 @@ func (m *Model) BlinkCmd() tea.Cmd {
|
||||
|
||||
m.blinkTag++
|
||||
|
||||
blinkMsg := BlinkMsg{id: m.id, tag: m.blinkTag}
|
||||
|
||||
return func() tea.Msg {
|
||||
defer cancel()
|
||||
<-ctx.Done()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return BlinkMsg{id: m.id, tag: m.blinkTag}
|
||||
return blinkMsg
|
||||
}
|
||||
return blinkCanceled{}
|
||||
}
|
||||
|
||||
50
cursor/cursor_test.go
Normal file
50
cursor/cursor_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestBlinkCmdDataRace tests for a race on [Cursor.blinkTag].
|
||||
//
|
||||
// The original [Model.BlinkCmd] implementation returned a closure over the pointer receiver:
|
||||
//
|
||||
// return func() tea.Msg {
|
||||
// defer cancel()
|
||||
// <-ctx.Done()
|
||||
// if ctx.Err() == context.DeadlineExceeded {
|
||||
// return BlinkMsg{id: m.id, tag: m.blinkTag}
|
||||
// }
|
||||
// return blinkCanceled{}
|
||||
// }
|
||||
//
|
||||
// A race on “m.blinkTag” will occur if:
|
||||
// 1. [Model.BlinkCmd] is called e.g. by calling [Model.Focus] from
|
||||
// ["github.com/charmbracelet/bubbletea".Model.Update];
|
||||
// 2. ["github.com/charmbracelet/bubbletea".handleCommands] is kept sufficiently busy that it does not recieve and
|
||||
// execute the [Model.BlinkCmd] e.g. by other long running command or commands;
|
||||
// 3. at least [Mode.BlinkSpeed] time elapses;
|
||||
// 4. [Model.BlinkCmd] is called again;
|
||||
// 5. ["github.com/charmbracelet/bubbletea".handleCommands] gets around to receiving and executing the original
|
||||
// closure.
|
||||
//
|
||||
// Even if this did not formally race, the value of the tag fetched would be semantically incorrect (likely being the
|
||||
// current value rather than the value at the time the closure was created).
|
||||
func TestBlinkCmdDataRace(t *testing.T) {
|
||||
m := New()
|
||||
cmd := m.BlinkCmd()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(m.BlinkSpeed * 3)
|
||||
cmd()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(m.BlinkSpeed * 2)
|
||||
m.BlinkCmd()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
@ -691,6 +691,7 @@ func (m *Model) setSize(width, height int) {
|
||||
m.Help.Width = width
|
||||
m.FilterInput.SetWidth(width - promptWidth - lipgloss.Width(m.spinnerView()))
|
||||
m.updatePagination()
|
||||
m.updateKeybindings()
|
||||
}
|
||||
|
||||
func (m *Model) resetFiltering() {
|
||||
|
||||
@ -305,6 +305,11 @@ func (m Model) Columns() []Column {
|
||||
// SetRows sets a new rows state.
|
||||
func (m *Model) SetRows(r []Row) {
|
||||
m.rows = r
|
||||
|
||||
if m.cursor > len(m.rows)-1 {
|
||||
m.cursor = len(m.rows) - 1
|
||||
}
|
||||
|
||||
m.UpdateViewport()
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,208 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/help"
|
||||
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/charmbracelet/x/exp/golden"
|
||||
)
|
||||
|
||||
func TestFromValues(t *testing.T) {
|
||||
var testCols = []Column{
|
||||
{Title: "col1", Width: 10},
|
||||
{Title: "col2", Width: 10},
|
||||
{Title: "col3", Width: 10},
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
opts []Option
|
||||
want Model
|
||||
}{
|
||||
"Default": {
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(0),
|
||||
viewport.WithHeight(20),
|
||||
),
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Help: help.New(),
|
||||
styles: DefaultStyles(),
|
||||
},
|
||||
},
|
||||
"WithColumns": {
|
||||
opts: []Option{
|
||||
WithColumns([]Column{
|
||||
{Title: "Foo", Width: 1},
|
||||
{Title: "Bar", Width: 2},
|
||||
}),
|
||||
},
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(0),
|
||||
viewport.WithHeight(20),
|
||||
),
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Help: help.New(),
|
||||
styles: DefaultStyles(),
|
||||
|
||||
// Modified fields
|
||||
cols: []Column{
|
||||
{Title: "Foo", Width: 1},
|
||||
{Title: "Bar", Width: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
"WithColumns; WithRows": {
|
||||
opts: []Option{
|
||||
WithColumns([]Column{
|
||||
{Title: "Foo", Width: 1},
|
||||
{Title: "Bar", Width: 2},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"1", "Foo"},
|
||||
{"2", "Bar"},
|
||||
}),
|
||||
},
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(0),
|
||||
viewport.WithHeight(20),
|
||||
),
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Help: help.New(),
|
||||
styles: DefaultStyles(),
|
||||
|
||||
// Modified fields
|
||||
cols: []Column{
|
||||
{Title: "Foo", Width: 1},
|
||||
{Title: "Bar", Width: 2},
|
||||
},
|
||||
rows: []Row{
|
||||
{"1", "Foo"},
|
||||
{"2", "Bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"WithHeight": {
|
||||
opts: []Option{
|
||||
WithHeight(10),
|
||||
},
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Help: help.New(),
|
||||
styles: DefaultStyles(),
|
||||
|
||||
// Modified fields
|
||||
// Viewport height is 1 less than the provided height when no header is present since lipgloss.Height adds 1
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(0),
|
||||
viewport.WithHeight(9),
|
||||
),
|
||||
},
|
||||
},
|
||||
"WithWidth": {
|
||||
opts: []Option{
|
||||
WithWidth(10),
|
||||
},
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Help: help.New(),
|
||||
styles: DefaultStyles(),
|
||||
|
||||
// Modified fields
|
||||
// Viewport height is 1 less than the provided height when no header is present since lipgloss.Height adds 1
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(10),
|
||||
viewport.WithHeight(20),
|
||||
),
|
||||
},
|
||||
},
|
||||
"WithFocused": {
|
||||
opts: []Option{
|
||||
WithFocused(true),
|
||||
},
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(0),
|
||||
viewport.WithHeight(20),
|
||||
),
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Help: help.New(),
|
||||
styles: DefaultStyles(),
|
||||
|
||||
// Modified fields
|
||||
focus: true,
|
||||
},
|
||||
},
|
||||
"WithStyles": {
|
||||
opts: []Option{
|
||||
WithStyles(Styles{}),
|
||||
},
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(0),
|
||||
viewport.WithHeight(20),
|
||||
),
|
||||
KeyMap: DefaultKeyMap(),
|
||||
Help: help.New(),
|
||||
|
||||
// Modified fields
|
||||
styles: Styles{},
|
||||
},
|
||||
},
|
||||
"WithKeyMap": {
|
||||
opts: []Option{
|
||||
WithKeyMap(KeyMap{}),
|
||||
},
|
||||
want: Model{
|
||||
// Default fields
|
||||
cursor: 0,
|
||||
viewport: viewport.New(
|
||||
viewport.WithWidth(0),
|
||||
viewport.WithHeight(20),
|
||||
),
|
||||
Help: help.New(),
|
||||
styles: DefaultStyles(),
|
||||
|
||||
// Modified fields
|
||||
KeyMap: KeyMap{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc.want.UpdateViewport()
|
||||
|
||||
got := New(tc.opts...)
|
||||
|
||||
if !reflect.DeepEqual(tc.want, got) {
|
||||
t.Errorf("\n\nwant %v\n\ngot %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_FromValues(t *testing.T) {
|
||||
input := "foo1,bar1\nfoo2,bar2\nfoo3,bar3"
|
||||
table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}}))
|
||||
table.FromValues(input, ",")
|
||||
@ -23,12 +216,12 @@ func TestFromValues(t *testing.T) {
|
||||
{"foo2", "bar2"},
|
||||
{"foo3", "bar3"},
|
||||
}
|
||||
if !deepEqual(table.rows, expect) {
|
||||
t.Fatal("table rows is not equals to the input")
|
||||
if !reflect.DeepEqual(table.rows, expect) {
|
||||
t.Fatalf("\n\nwant %v\n\ngot %v", expect, table.rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromValuesWithTabSeparator(t *testing.T) {
|
||||
func TestModel_FromValues_WithTabSeparator(t *testing.T) {
|
||||
input := "foo1.\tbar1\nfoo,bar,baz\tbar,2"
|
||||
table := New(WithColumns([]Column{{Title: "Foo"}, {Title: "Bar"}}))
|
||||
table.FromValues(input, "\t")
|
||||
@ -41,32 +234,12 @@ func TestFromValuesWithTabSeparator(t *testing.T) {
|
||||
{"foo1.", "bar1"},
|
||||
{"foo,bar,baz", "bar,2"},
|
||||
}
|
||||
if !deepEqual(table.rows, expect) {
|
||||
t.Fatal("table rows is not equals to the input")
|
||||
if !reflect.DeepEqual(table.rows, expect) {
|
||||
t.Fatalf("\n\nwant %v\n\ngot %v", expect, table.rows)
|
||||
}
|
||||
}
|
||||
|
||||
func deepEqual(a, b []Row) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, r := range a {
|
||||
for j, f := range r {
|
||||
if f != b[i][j] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var cols = []Column{
|
||||
{Title: "col1", Width: 10},
|
||||
{Title: "col2", Width: 10},
|
||||
{Title: "col3", Width: 10},
|
||||
}
|
||||
|
||||
func TestRenderRow(t *testing.T) {
|
||||
func TestModel_RenderRow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
table *Model
|
||||
@ -76,7 +249,7 @@ func TestRenderRow(t *testing.T) {
|
||||
name: "simple row",
|
||||
table: &Model{
|
||||
rows: []Row{{"Foooooo", "Baaaaar", "Baaaaaz"}},
|
||||
cols: cols,
|
||||
cols: testCols,
|
||||
styles: Styles{Cell: lipgloss.NewStyle()},
|
||||
},
|
||||
expected: "Foooooo Baaaaar Baaaaaz ",
|
||||
@ -85,7 +258,7 @@ func TestRenderRow(t *testing.T) {
|
||||
name: "simple row with truncations",
|
||||
table: &Model{
|
||||
rows: []Row{{"Foooooooooo", "Baaaaaaaaar", "Quuuuuuuuux"}},
|
||||
cols: cols,
|
||||
cols: testCols,
|
||||
styles: Styles{Cell: lipgloss.NewStyle()},
|
||||
},
|
||||
expected: "Foooooooo…Baaaaaaaa…Quuuuuuuu…",
|
||||
@ -94,7 +267,7 @@ func TestRenderRow(t *testing.T) {
|
||||
name: "simple row avoiding truncations",
|
||||
table: &Model{
|
||||
rows: []Row{{"Fooooooooo", "Baaaaaaaar", "Quuuuuuuux"}},
|
||||
cols: cols,
|
||||
cols: testCols,
|
||||
styles: Styles{Cell: lipgloss.NewStyle()},
|
||||
},
|
||||
expected: "FoooooooooBaaaaaaaarQuuuuuuuux",
|
||||
@ -164,3 +337,432 @@ func ansiStrip(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
return ansi.Strip(s)
|
||||
}
|
||||
|
||||
func TestCursorNavigation(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
rows []Row
|
||||
action func(*Model)
|
||||
want int
|
||||
}{
|
||||
"New": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
},
|
||||
action: func(_ *Model) {},
|
||||
want: 0,
|
||||
},
|
||||
"MoveDown": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.MoveDown(2)
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
"MoveUp": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.cursor = 3
|
||||
t.MoveUp(2)
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
"GotoBottom": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.GotoBottom()
|
||||
},
|
||||
want: 3,
|
||||
},
|
||||
"GotoTop": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.cursor = 3
|
||||
t.GotoTop()
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
"SetCursor": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.SetCursor(2)
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
"MoveDown with overflow": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.MoveDown(5)
|
||||
},
|
||||
want: 3,
|
||||
},
|
||||
"MoveUp with overflow": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.cursor = 3
|
||||
t.MoveUp(5)
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
"Blur does not stop movement": {
|
||||
rows: []Row{
|
||||
{"r1"},
|
||||
{"r2"},
|
||||
{"r3"},
|
||||
{"r4"},
|
||||
},
|
||||
action: func(t *Model) {
|
||||
t.Blur()
|
||||
t.MoveDown(2)
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
table := New(WithColumns(testCols), WithRows(tc.rows))
|
||||
tc.action(&table)
|
||||
|
||||
if table.Cursor() != tc.want {
|
||||
t.Errorf("want %d, got %d", tc.want, table.Cursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_SetRows(t *testing.T) {
|
||||
table := New(WithColumns(testCols))
|
||||
|
||||
if len(table.rows) != 0 {
|
||||
t.Fatalf("want 0, got %d", len(table.rows))
|
||||
}
|
||||
|
||||
table.SetRows([]Row{{"r1"}, {"r2"}})
|
||||
|
||||
if len(table.rows) != 2 {
|
||||
t.Fatalf("want 2, got %d", len(table.rows))
|
||||
}
|
||||
|
||||
want := []Row{{"r1"}, {"r2"}}
|
||||
if !reflect.DeepEqual(table.rows, want) {
|
||||
t.Fatalf("\n\nwant %v\n\ngot %v", want, table.rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_SetColumns(t *testing.T) {
|
||||
table := New()
|
||||
|
||||
if len(table.cols) != 0 {
|
||||
t.Fatalf("want 0, got %d", len(table.cols))
|
||||
}
|
||||
|
||||
table.SetColumns([]Column{{Title: "Foo"}, {Title: "Bar"}})
|
||||
|
||||
if len(table.cols) != 2 {
|
||||
t.Fatalf("want 2, got %d", len(table.cols))
|
||||
}
|
||||
|
||||
want := []Column{{Title: "Foo"}, {Title: "Bar"}}
|
||||
if !reflect.DeepEqual(table.cols, want) {
|
||||
t.Fatalf("\n\nwant %v\n\ngot %v", want, table.cols)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_View(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
modelFunc func() Model
|
||||
skip bool
|
||||
}{
|
||||
// TODO(?): should the view/output of empty tables use the same default height? (this has height 21)
|
||||
"Empty": {
|
||||
modelFunc: func() Model {
|
||||
return New()
|
||||
},
|
||||
},
|
||||
"Single row and column": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives"},
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
"Multiple rows and columns": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
// TODO(fix): since the table height is tied to the viewport height, adding vertical padding to the headers' height directly increases the table height.
|
||||
"Extra padding": {
|
||||
modelFunc: func() Model {
|
||||
s := DefaultStyles()
|
||||
s.Header = lipgloss.NewStyle().Padding(2, 2)
|
||||
s.Cell = lipgloss.NewStyle().Padding(2, 2)
|
||||
|
||||
return New(
|
||||
WithHeight(10),
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
WithStyles(s),
|
||||
)
|
||||
},
|
||||
},
|
||||
"No padding": {
|
||||
modelFunc: func() Model {
|
||||
s := DefaultStyles()
|
||||
s.Header = lipgloss.NewStyle()
|
||||
s.Cell = lipgloss.NewStyle()
|
||||
|
||||
return New(
|
||||
WithHeight(10),
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
WithStyles(s),
|
||||
)
|
||||
},
|
||||
},
|
||||
// TODO(?): the total height is modified with borderd headers, however not with bordered cells. Is this expected/desired?
|
||||
"Bordered headers": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
WithStyles(Styles{
|
||||
Header: lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()),
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
// TODO(fix): Headers are not horizontally aligned with cells due to the border adding width to the cells.
|
||||
"Bordered cells": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
WithStyles(Styles{
|
||||
Cell: lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()),
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
"Manual height greater than rows": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithHeight(6),
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
"Manual height less than rows": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithHeight(2),
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
// TODO(fix): spaces are added to the right of the viewport to fill the width, but the headers end as though they are not aware of the width.
|
||||
"Manual width greater than columns": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithWidth(80),
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
// TODO(fix): Setting the table width does not affect the total headers' width. Cells are wrapped.
|
||||
// Headers are not affected. Truncation/resizing should match lipgloss.table functionality.
|
||||
"Manual width less than columns": {
|
||||
modelFunc: func() Model {
|
||||
return New(
|
||||
WithWidth(30),
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
)
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
"Modified viewport height": {
|
||||
modelFunc: func() Model {
|
||||
m := New(
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
)
|
||||
|
||||
m.viewport.SetHeight(2)
|
||||
|
||||
return m
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if tc.skip {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
table := tc.modelFunc()
|
||||
|
||||
got := ansi.Strip(table.View())
|
||||
|
||||
golden.RequireEqual(t, []byte(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix table to make this test will pass.
|
||||
func TestModel_View_CenteredInABox(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
boxStyle := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
Align(lipgloss.Center)
|
||||
|
||||
table := New(
|
||||
WithHeight(6),
|
||||
WithWidth(80),
|
||||
WithColumns([]Column{
|
||||
{Title: "Name", Width: 25},
|
||||
{Title: "Country of Origin", Width: 16},
|
||||
{Title: "Dunk-able", Width: 12},
|
||||
}),
|
||||
WithRows([]Row{
|
||||
{"Chocolate Digestives", "UK", "Yes"},
|
||||
{"Tim Tams", "Australia", "No"},
|
||||
{"Hobnobs", "UK", "Yes"},
|
||||
}),
|
||||
)
|
||||
|
||||
tableView := ansi.Strip(table.View())
|
||||
got := boxStyle.Render(tableView)
|
||||
|
||||
golden.RequireEqual(t, []byte(got))
|
||||
}
|
||||
|
||||
21
table/testdata/TestModel_View/Bordered_cells.golden
vendored
Normal file
21
table/testdata/TestModel_View/Bordered_cells.golden
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
Name Country of Orig…Dunk-able
|
||||
┌─────────────────────────┐┌────────────────┐┌────────────┐
|
||||
│Chocolate Digestives ││UK ││Yes │
|
||||
└─────────────────────────┘└────────────────┘└────────────┘
|
||||
┌─────────────────────────┐┌────────────────┐┌────────────┐
|
||||
│Tim Tams ││Australia ││No │
|
||||
└─────────────────────────┘└────────────────┘└────────────┘
|
||||
┌─────────────────────────┐┌────────────────┐┌────────────┐
|
||||
│Hobnobs ││UK ││Yes │
|
||||
└─────────────────────────┘└────────────────┘└────────────┘
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
23
table/testdata/TestModel_View/Bordered_headers.golden
vendored
Normal file
23
table/testdata/TestModel_View/Bordered_headers.golden
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
┌─────────────────────────┐┌────────────────┐┌────────────┐
|
||||
│Name ││Country of Orig…││Dunk-able │
|
||||
└─────────────────────────┘└────────────────┘└────────────┘
|
||||
Chocolate Digestives UK Yes
|
||||
Tim Tams Australia No
|
||||
Hobnobs UK Yes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
20
table/testdata/TestModel_View/Empty.golden
vendored
Normal file
20
table/testdata/TestModel_View/Empty.golden
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
14
table/testdata/TestModel_View/Extra_padding.golden
vendored
Normal file
14
table/testdata/TestModel_View/Extra_padding.golden
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
|
||||
Name Country of Orig… Dunk-able
|
||||
|
||||
|
||||
|
||||
|
||||
Chocolate Digestives UK Yes
|
||||
|
||||
|
||||
|
||||
|
||||
Tim Tams Australia No
|
||||
|
||||
6
table/testdata/TestModel_View/Manual_height_greater_than_rows.golden
vendored
Normal file
6
table/testdata/TestModel_View/Manual_height_greater_than_rows.golden
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
Name Country of Orig… Dunk-able
|
||||
Chocolate Digestives UK Yes
|
||||
Tim Tams Australia No
|
||||
Hobnobs UK Yes
|
||||
|
||||
|
||||
2
table/testdata/TestModel_View/Manual_height_less_than_rows.golden
vendored
Normal file
2
table/testdata/TestModel_View/Manual_height_less_than_rows.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
Name Country of Orig… Dunk-able
|
||||
Chocolate Digestives UK Yes
|
||||
21
table/testdata/TestModel_View/Manual_width_greater_than_columns.golden
vendored
Normal file
21
table/testdata/TestModel_View/Manual_width_greater_than_columns.golden
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
Name Country of Orig… Dunk-able
|
||||
Chocolate Digestives UK Yes
|
||||
Tim Tams Australia No
|
||||
Hobnobs UK Yes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
21
table/testdata/TestModel_View/Manual_width_less_than_columns.golden
vendored
Normal file
21
table/testdata/TestModel_View/Manual_width_less_than_columns.golden
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
Name Country of Origin Dunk-able
|
||||
Chocolate Digestives UK Yes
|
||||
Tim Tams Australia No
|
||||
Hobnobs UK Yes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3
table/testdata/TestModel_View/Modified_viewport_height.golden
vendored
Normal file
3
table/testdata/TestModel_View/Modified_viewport_height.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
Name Country of Orig… Dunk-able
|
||||
Chocolate Digestives UK Yes
|
||||
Tim Tams Australia No
|
||||
21
table/testdata/TestModel_View/Multiple_rows_and_columns.golden
vendored
Normal file
21
table/testdata/TestModel_View/Multiple_rows_and_columns.golden
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
Name Country of Orig… Dunk-able
|
||||
Chocolate Digestives UK Yes
|
||||
Tim Tams Australia No
|
||||
Hobnobs UK Yes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
10
table/testdata/TestModel_View/No_padding.golden
vendored
Normal file
10
table/testdata/TestModel_View/No_padding.golden
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
Name Country of Orig…Dunk-able
|
||||
Chocolate Digestives UK Yes
|
||||
Tim Tams Australia No
|
||||
Hobnobs UK Yes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
21
table/testdata/TestModel_View/Single_row_and_column.golden
vendored
Normal file
21
table/testdata/TestModel_View/Single_row_and_column.golden
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
Name
|
||||
Chocolate Digestives
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
8
table/testdata/TestModel_View_CenteredInABox.golden
vendored
Normal file
8
table/testdata/TestModel_View_CenteredInABox.golden
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
┌────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Name Country of Orig… Dunk-able │
|
||||
│ Chocolate Digestives UK Yes │
|
||||
│ Tim Tams Australia No │
|
||||
│ Hobnobs UK Yes │
|
||||
│ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────────────┘
|
||||
Loading…
x
Reference in New Issue
Block a user