Files
ilovetui/layout/layout_test.go
T

278 lines
8.2 KiB
Go

package layout
import (
"testing"
tea "charm.land/bubbletea/v2"
)
func newTestModel(t *testing.T, root *Node) (Model, map[string]*stubPane) {
t.Helper()
panes := map[string]*stubPane{}
walk(root, func(n *Node) {
if s, ok := n.model.(*stubPane); ok {
panes[n.id] = s
}
})
m := New(root, AsRoot())
m.Init()
return m, panes
}
func TestInitFocusesFirstLeaf(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, panes := newTestModel(t, root)
if m.state.id != "a" {
t.Fatalf("focusedID = %q, want %q", m.state.id, "a")
}
if panes["a"].focusN != 1 {
t.Fatalf("a.focusN = %d, want 1", panes["a"].focusN)
}
if panes["b"].focusN != 0 {
t.Fatalf("b.focusN = %d, want 0", panes["b"].focusN)
}
}
func TestWindowSizeDispatchesSizeMsg(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, panes := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
// newTestModel builds with AsRoot(), so the tree gets 40 minus the
// 1-row help bar - see treeRect.
if panes["a"].w != 50 || panes["a"].h != 39 {
t.Fatalf("a size = %dx%d, want 50x39", panes["a"].w, panes["a"].h)
}
if panes["b"].w != 50 || panes["b"].h != 39 {
t.Fatalf("b size = %dx%d, want 50x39", panes["b"].w, panes["b"].h)
}
if got := len(m.Leaves()); got != 2 {
t.Fatalf("len(Leaves()) = %d, want 2", got)
}
}
func TestCtrlLMovesFocusAndDispatchesBlurFocus(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, panes := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
updated, _ = m.Update(tea.KeyPressMsg{Code: 'l', Mod: tea.ModCtrl, Text: ""})
m = updated.(Model)
if m.state.id != "b" {
t.Fatalf("focusedID = %q, want %q", m.state.id, "b")
}
if panes["a"].blurN != 1 {
t.Fatalf("a.blurN = %d, want 1", panes["a"].blurN)
}
if panes["b"].focusN != 1 {
t.Fatalf("b.focusN = %d, want 1", panes["b"].focusN)
}
}
func TestKeyPressOnlyReachesFocusedPane(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, panes := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
updated, _ = m.Update(tea.KeyPressMsg{Text: "x"})
_ = updated.(Model)
if _, ok := panes["a"].last().(tea.KeyPressMsg); !ok {
t.Fatalf("focused pane a should have received the key press, got %#v", panes["a"].last())
}
if _, ok := panes["b"].last().(tea.KeyPressMsg); ok {
t.Fatalf("blurred pane b should not have received the key press")
}
}
func TestSplitLeafAddsAndSizesNewPane(t *testing.T) {
root := Leaf("a", newStub())
m, panes := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
newPane := newStub()
m, _ = m.SplitLeaf("a", Horizontal, "b", newPane)
if got := len(m.Leaves()); got != 2 {
t.Fatalf("len(Leaves()) = %d, want 2", got)
}
if newPane.w == 0 || newPane.h == 0 {
t.Fatalf("new pane never received a SizeMsg: w=%d h=%d", newPane.w, newPane.h)
}
if panes["a"].w != 50 {
t.Fatalf("a.w = %d, want 50 after 50/50 split", panes["a"].w)
}
}
func TestSetPaneSwapsContentKeepingShape(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, panes := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
// "a" is focused (first leaf).
replacement := newStub()
m, _ = m.SetPane("a", replacement)
if got := len(m.Leaves()); got != 2 {
t.Fatalf("len(Leaves()) = %d, want 2 (SetPane must not reshape the tree)", got)
}
if replacement.w != 50 || replacement.h != 39 {
t.Fatalf("replacement size = %dx%d, want 50x39 (a's existing Rect)", replacement.w, replacement.h)
}
if replacement.focusN != 1 {
t.Fatalf("replacement.focusN = %d, want 1: \"a\" currently holds focus", replacement.focusN)
}
if panes["b"].w != 50 {
t.Fatalf("b.w = %d, want unchanged 50", panes["b"].w)
}
}
func TestSetPaneOnBlurredLeafDoesNotFocus(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, _ := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
// "b" is blurred (only "a" is focused initially).
replacement := newStub()
m, _ = m.SetPane("b", replacement)
if replacement.focusN != 0 {
t.Fatalf("replacement.focusN = %d, want 0: \"b\" isn't focused", replacement.focusN)
}
if replacement.w != 50 || replacement.h != 39 {
t.Fatalf("replacement size = %dx%d, want 50x39", replacement.w, replacement.h)
}
}
func TestSetPaneUnknownIDIsNoop(t *testing.T) {
root := Leaf("a", newStub())
m, _ := newTestModel(t, root)
m2, cmd := m.SetPane("nope", newStub())
if cmd != nil {
t.Fatalf("expected nil cmd for an unknown SetPane id, got %v", cmd)
}
if len(m2.Leaves()) != len(m.Leaves()) {
t.Fatalf("tree changed after a no-op SetPane")
}
}
func TestCloseLeafReassignsFocus(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, _ := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
// focus is on "a"; closing it must move focus to what remains.
m, _ = m.CloseLeaf("a")
if got := len(m.Leaves()); got != 1 {
t.Fatalf("len(Leaves()) = %d, want 1", got)
}
if m.state.id != "b" {
t.Fatalf("focusedID = %q, want %q after closing the focused leaf", m.state.id, "b")
}
}
func TestCloseLeafRootIsNoop(t *testing.T) {
root := Leaf("only", newStub())
m, _ := newTestModel(t, root)
m2, cmd := m.CloseLeaf("only")
if cmd != nil {
t.Fatalf("expected nil cmd closing the tree's only leaf, got %v", cmd)
}
if got := len(m2.Leaves()); got != len(m.Leaves()) {
t.Fatalf("tree changed after a no-op close")
}
}
func TestResizeChangesRatio(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub())).WithID("split")
m, panes := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
m, _ = m.Resize("split", 0.8)
if panes["a"].w != 80 {
t.Fatalf("a.w = %d, want 80 after Resize to 0.8", panes["a"].w)
}
if panes["b"].w != 20 {
t.Fatalf("b.w = %d, want 20 after Resize to 0.8", panes["b"].w)
}
}
func TestSendMsgDeliversToTarget(t *testing.T) {
type payload struct{ n int }
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, panes := newTestModel(t, root)
// a is focused via Init's initial FocusMsg; b never received anything yet.
updated, _ := m.Update(SendMsg{Target: "b", Msg: payload{n: 42}})
m = updated.(Model)
if got, ok := panes["b"].last().(payload); !ok || got.n != 42 {
t.Fatalf("b should have received payload{42}, got %#v", panes["b"].last())
}
for _, msg := range panes["a"].msgs {
if _, ok := msg.(payload); ok {
t.Fatalf("a should not have received the SendMsg meant for b")
}
}
}
func TestSendMsgUnknownTargetIsIgnored(t *testing.T) {
root := Leaf("a", newStub())
m, _ := newTestModel(t, root)
updated, cmd := m.Update(SendMsg{Target: "nope", Msg: struct{}{}})
_ = updated.(Model)
if cmd != nil {
t.Fatalf("expected nil cmd for an unknown SendMsg target, got %v", cmd)
}
}
func TestRequestFocusFromFocusedPaneSucceeds(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
m, panes := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
updated, _ = m.Update(RequestFocusMsg{Source: "a", Target: "b"})
m = updated.(Model)
if m.state.id != "b" {
t.Fatalf("focusedID = %q, want %q", m.state.id, "b")
}
if panes["b"].focusN != 1 {
t.Fatalf("b.focusN = %d, want 1", panes["b"].focusN)
}
}
func TestRequestFocusFromBlurredPaneIsIgnored(t *testing.T) {
root := HSplit(0.5, Leaf("a", newStub()), Leaf("b", newStub()))
third := Leaf("c", newStub())
_ = third
m, _ := newTestModel(t, root)
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = updated.(Model)
// "a" is focused; "b" (blurred) tries to redirect focus to itself.
updated, _ = m.Update(RequestFocusMsg{Source: "b", Target: "b"})
m = updated.(Model)
if m.state.id != "a" {
t.Fatalf("focusedID = %q, want %q (unauthorized request must be ignored)", m.state.id, "a")
}
}