star/bed/tui/tui_test.go
2025-04-26 19:33:14 +08:00

416 lines
12 KiB
Go

package tui
import (
"errors"
"strings"
"testing"
"github.com/gdamore/tcell"
"b612.me/apps/b612/bed/event"
"b612.me/apps/b612/bed/key"
"b612.me/apps/b612/bed/layout"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
)
func (ui *Tui) initForTest(eventCh chan<- event.Event, screen tcell.SimulationScreen) (err error) {
ui.eventCh = eventCh
ui.mode = mode.Normal
ui.screen = screen
ui.waitCh = make(chan struct{})
return ui.screen.Init()
}
func mockKeyManager() map[mode.Mode]*key.Manager {
kms := make(map[mode.Mode]*key.Manager)
km := key.NewManager(true)
km.Register(event.Quit, "Z", "Q")
km.Register(event.CursorDown, "j")
kms[mode.Normal] = km
return kms
}
func getContents(screen tcell.SimulationScreen) string {
width, _ := screen.Size()
cells, _, _ := screen.GetContents()
var runes []rune
for i, cell := range cells {
runes = append(runes, cell.Runes...)
if (i+1)%width == 0 {
runes = append(runes, '\n')
}
}
return string(runes)
}
func shouldContain(t *testing.T, screen tcell.SimulationScreen, expected []string) {
got := getContents(screen)
for _, str := range expected {
if !strings.Contains(got, str) {
t.Errorf("screen should contain %q but got\n%v", str, got)
}
}
}
func TestTuiRun(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(90, 20)
go ui.Run(mockKeyManager())
screen.InjectKey(tcell.KeyRune, 'Z', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, 'Q', tcell.ModNone)
e := <-eventCh
if e.Type != event.Rune {
t.Errorf("pressing Z should emit event.Rune but got: %+v", e)
}
e = <-eventCh
if e.Type != event.Quit {
t.Errorf("pressing ZQ should emit event.Quit but got: %+v", e)
}
screen.InjectKey(tcell.KeyRune, '7', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, '0', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, '9', tcell.ModNone)
screen.InjectKey(tcell.KeyRune, 'j', tcell.ModNone)
e = <-eventCh
e = <-eventCh
e = <-eventCh
e = <-eventCh
if e.Type != event.CursorDown {
t.Errorf("pressing 709j should emit event.CursorDown but got: %+v", e)
}
if e.Count != 709 {
t.Errorf("pressing 709j should emit event with count %d but got: %+v", 709, e)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiEmpty(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(90, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "",
Modified: false,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte(strings.Repeat("\x00", 16*(height-1))),
Size: 16 * (height - 1),
Length: 0,
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ",
" 000000 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" 000010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" 000020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" 000100 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" [No name] : 0x00 : '\\x00' 0/0 : 0x000000/0x000000 : 0.00%",
})
x, y, visible := screen.GetCursor()
if x != 10 || y != 1 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 10, 1, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiScrollBar(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(90, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "",
Modified: true,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte(strings.Repeat("a", 16*(height-1))),
Size: 16 * (height - 1),
Length: int64(16 * (height - 1) * 3),
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ",
" 000000 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa # ",
" 000050 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa # ",
" 000060 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa | ",
" 000100 | 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa | ",
" [No name] : + : 0x61 : 'a' 0/912 : 0x000000/0x000390 : 0.00%",
})
x, y, visible := screen.GetCursor()
if x != 10 || y != 1 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 10, 1, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiHorizontalSplit(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(110, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "test0",
Modified: false,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 0." + strings.Repeat("\x00", 110*10)),
Size: 110 * 10,
Length: 600,
Mode: mode.Normal,
},
1: {
Name: "test1",
Modified: false,
Width: 16,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 1." + strings.Repeat(" ", 110*10)),
Size: 110 * 10,
Length: 800,
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).SplitBottom(1).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" 000000 | 54 65 73 74 20 77 69 6e 64 6f 77 20 30 2e 00 00 | Test window 0... #",
" 000010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ #",
" test0 : 0x54 : 'T' 0/600 : 0x000000/0x000258 : 0.00%",
" | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ",
" 000000 | 54 65 73 74 20 77 69 6e 64 6f 77 20 31 2e 20 20 | Test window 1. #",
" 000010 | 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | #",
" test1 : 0x54 : 'T' 0/800 : 0x000000/0x000320 : 0.00%",
})
x, y, visible := screen.GetCursor()
if x != 10 || y != 10 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 10, 10, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiVerticalSplit(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(110, 20)
width, height := screen.Size()
go ui.Run(mockKeyManager())
s := state.State{
WindowStates: map[int]*state.WindowState{
0: {
Name: "test0",
Modified: false,
Width: 8,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 0." + strings.Repeat("\x00", 55*19)),
Size: 55 * 19,
Length: 600,
Mode: mode.Normal,
},
1: {
Name: "test1",
Modified: false,
Width: 8,
Offset: 0,
Cursor: 0,
Bytes: []byte("Test window 1." + strings.Repeat(" ", 54*19)),
Size: 54 * 19,
Length: 800,
Mode: mode.Normal,
},
},
Layout: layout.NewLayout(0).SplitRight(1).Resize(0, 0, width, height-1),
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" | 0 1 2 3 4 5 6 7 | | | 0 1 2 3 4 5 6 7 |",
" 000000 | 54 65 73 74 20 77 69 6e | Test win # | 000000 | 54 65 73 74 20 77 69 6e | Test win #",
" 000008 | 64 6f 77 20 30 2e 00 00 | dow 0... # | 000008 | 64 6f 77 20 31 2e 20 20 | dow 1. #",
" 000010 | 00 00 00 00 00 00 00 00 | ........ # | 000010 | 20 20 20 20 20 20 20 20 | #",
" test0 : 0x54 : 'T' 0/600 : 0x000000/0x000258 : 0.00% | test1 : 0x54 : 'T' 0/800 : 0x000000/0x000320 : 0.00",
})
x, y, visible := screen.GetCursor()
if x != 66 || y != 1 {
t.Errorf("cursor position should be (%d, %d) but got (%d, %d)", 66, 1, x, y)
}
if visible != true {
t.Errorf("cursor should be visible but got %v", visible)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiCmdline(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(20, 15)
getCmdline := func() string {
cells, _, _ := screen.GetContents()
var runes []rune
for _, cell := range cells[20*14:] {
runes = append(runes, cell.Runes...)
}
return string(runes)
}
go ui.Run(mockKeyManager())
s := state.State{
Mode: mode.Cmdline,
Cmdline: []rune("vnew test"),
CmdlineCursor: 9,
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
got, expected := getCmdline(), ":vnew test "
if !strings.HasPrefix(got, expected) {
t.Errorf("cmdline should start with %q but got %q", expected, got)
}
s = state.State{
Mode: mode.Normal,
Error: errors.New("error"),
Cmdline: []rune("vnew test"),
CmdlineCursor: 9,
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
got, expected = getCmdline(), "error "
if !strings.HasPrefix(got, expected) {
t.Errorf("cmdline should start with %q but got %q", expected, got)
}
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}
func TestTuiCmdlineCompletionCandidates(t *testing.T) {
ui := NewTui()
eventCh := make(chan event.Event)
screen := tcell.NewSimulationScreen("")
if err := ui.initForTest(eventCh, screen); err != nil {
t.Fatal(err)
}
screen.SetSize(20, 15)
go ui.Run(mockKeyManager())
s := state.State{
Mode: mode.Cmdline,
Cmdline: []rune("new test2"),
CmdlineCursor: 9,
CompletionResults: []string{"test1", "test2", "test3", "test9/", "/bin/ls"},
CompletionIndex: 1,
}
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" test1 test2 test3",
":new test2",
})
s.CompletionIndex += 2
s.Cmdline = []rune("new test9/")
if err := ui.Redraw(s); err != nil {
t.Errorf("ui.Redraw should return nil but got: %v", err)
}
shouldContain(t, screen, []string{
" test3 test9/ /bin",
":new test9/",
})
if err := ui.Close(); err != nil {
t.Errorf("ui.Close should return nil but got %v", err)
}
}