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) } }