This repository has been archived on 2026-10-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
submarine/internal/tui/tree/tree_test.go
T
Simon FrankenandClaude Opus 5.5 27f0c0b5d2 Fetch in the background and follow changes in the repositories
- On startup, after the local state is shown, fetch the superproject and
  every initialized submodule (all remotes, prune, at most 8 at once).
  Fetches never prompt; failures show git's message. --no-fetch skips it,
  f fetches again.
- The header shows "fetching n/m", then the fetch age and failures; the
  detail pane shows each submodule's last fetch and fetch errors.
- The git provider polls each repository's metadata (refs, HEAD, index,
  .gitmodules) every 2 s and reports changes. The app reloads only what
  changed, keeping old values, expansion and cursor; loads are
  single-flight with coalesced reruns. Refresh is driven by the
  repositories, so a fetch in another shell updates the view like ours.
- r reloads everything (worktree edits are not watched).
- Docs: F50-F53, N1/N8 rewritten, D18-D20 (D12 superseded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:15:22 +02:00

252 lines
5.3 KiB
Go

package tree
import (
"os"
"testing"
zone "github.com/lrstanley/bubblezone"
"submarine/internal/submodule"
"submarine/internal/tui/theme"
)
func TestMain(m *testing.M) {
zone.NewGlobal()
os.Exit(m.Run())
}
// fixture:
//
// a
// src/
// b
// modules/
// c
// d
// e
// f
func fixture() []*submodule.Submodule {
mk := func(path, rel string, parent *submodule.Submodule) *submodule.Submodule {
s := &submodule.Submodule{Name: path, Path: path, RelPath: rel, Parent: parent}
if parent != nil {
parent.Children = append(parent.Children, s)
}
return s
}
a := mk("a", "a", nil)
b := mk("b", "src/b", nil)
c := mk("c", "modules/c", b)
mk("d", "d", c)
mk("e", "modules/e", b)
f := mk("f", "src/f", nil)
return []*submodule.Submodule{a, b, f}
}
func newTree(t *testing.T) *Model {
t.Helper()
m := New(theme.Default())
m.SetRoots(fixture())
m.SetSize(80, 20)
return &m
}
func assertCursor(t *testing.T, m *Model, want string) {
t.Helper()
if got := m.Selected().Path; got != want {
t.Fatalf("cursor = %q, want %q", got, want)
}
}
func visible(m *Model) []string {
var out []string
for _, r := range m.rows() {
if r.kind == rowDir {
out = append(out, r.label)
} else {
out = append(out, r.node.Path)
}
}
return out
}
func TestReplaceRootsKeepsState(t *testing.T) {
m := newTree(t)
m.Down() // b
m.Right() // expand b, cursor on c
m.Right() // expand c, cursor on d
assertCursor(t, m, "d")
m.ReplaceRoots(fixture()) // same paths, new nodes
assertCursor(t, m, "d")
if got := len(visible(m)); got != 8 {
t.Errorf("visible rows = %v, want b and c still expanded", visible(m))
}
m.ReplaceRoots(fixture()[:1]) // only "a" left
assertCursor(t, m, "a")
}
func TestInitiallyCollapsedWithHeaders(t *testing.T) {
m := newTree(t)
got := visible(m)
want := []string{"a", "src/", "b", "f"}
if len(got) != len(want) {
t.Fatalf("rows = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("rows = %v, want %v", got, want)
}
}
assertCursor(t, m, "a")
}
func TestDownSkipsHeaders(t *testing.T) {
m := newTree(t)
m.Down()
assertCursor(t, m, "b")
m.Down()
assertCursor(t, m, "f")
m.Down() // stays at end
assertCursor(t, m, "f")
m.Up()
m.Up()
m.Up() // stays at start
assertCursor(t, m, "a")
}
func TestRightExpandsAndSelectsFirstChild(t *testing.T) {
m := newTree(t)
m.Down()
m.Right()
assertCursor(t, m, "c")
if !m.expanded["b"] {
t.Fatal("b should be expanded")
}
m.Right()
assertCursor(t, m, "d")
m.Right() // leaf: no-op
assertCursor(t, m, "d")
}
func TestRightOnLeafIsNoop(t *testing.T) {
m := newTree(t)
m.Right()
assertCursor(t, m, "a")
}
func TestLeftCollapsesParentAndSelectsIt(t *testing.T) {
m := newTree(t)
m.Down()
m.Right() // b expanded, cursor c
m.Down() // e
assertCursor(t, m, "e")
m.Left()
assertCursor(t, m, "b")
if m.expanded["b"] {
t.Fatal("b should be collapsed")
}
}
func TestLeftOnExpandedNodeCollapsesIt(t *testing.T) {
m := newTree(t)
m.Down()
m.Toggle() // expand b, cursor stays
assertCursor(t, m, "b")
m.Left()
assertCursor(t, m, "b")
if m.expanded["b"] {
t.Fatal("b should be collapsed")
}
m.Left() // top level and collapsed: no-op
assertCursor(t, m, "b")
}
func TestExpansionStateKept(t *testing.T) {
m := newTree(t)
m.Down()
m.Right() // c
m.Right() // d, c expanded
m.Left() // collapse c, select c
m.Right() // d again
m.Left() // c
m.Toggle() // re-expand c
m.Left() // c is expanded -> collapse c
m.Toggle() // expand c
m.Up() // b
m.Toggle() // collapse b
m.Toggle() // expand b: c should still be expanded
got := visible(m)
want := []string{"a", "src/", "b", "modules/", "c", "d", "e", "f"}
if len(got) != len(want) {
t.Fatalf("rows = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("rows = %v, want %v", got, want)
}
}
}
func TestCollapsingAncestorMovesCursor(t *testing.T) {
m := newTree(t)
m.Down()
m.Right()
m.Right() // cursor d
m.setExpanded(m.roots[1], false)
assertCursor(t, m, "b")
}
func TestScrollKeepsCursorVisible(t *testing.T) {
m := newTree(t)
m.SetSize(80, 2)
m.Down()
m.Right() // rows: a src/ b modules/ c e f ; cursor c (index 4)
if i := m.cursorRow(m.rows()); i < m.offset || i >= m.offset+m.height {
t.Fatalf("cursor row %d not in [%d,%d)", i, m.offset, m.offset+m.height)
}
m.Home()
if m.offset != 0 {
t.Fatalf("offset = %d after Home, want 0", m.offset)
}
}
func TestCollapseAllSelectsTopLevelAncestor(t *testing.T) {
m := newTree(t)
m.Down()
m.Right() // c
m.Right() // d
m.CollapseAll()
assertCursor(t, m, "b")
got := visible(m)
want := []string{"a", "src/", "b", "f"}
if len(got) != len(want) {
t.Fatalf("rows = %v, want %v", got, want)
}
m.Toggle() // re-expanding b must not restore c's expansion
if m.expanded["c"] {
t.Fatal("c should be collapsed after CollapseAll")
}
}
func TestExpandAllDescendsFirstChildren(t *testing.T) {
m := newTree(t)
m.Down() // b
m.ExpandAll()
assertCursor(t, m, "d")
got := visible(m)
want := []string{"a", "src/", "b", "modules/", "c", "d", "e", "f"}
if len(got) != len(want) {
t.Fatalf("rows = %v, want %v", got, want)
}
}
func TestExpandAllOnLeafKeepsSelection(t *testing.T) {
m := newTree(t) // cursor a, a leaf
m.ExpandAll()
assertCursor(t, m, "a")
if !m.expanded["b"] || !m.expanded["c"] {
t.Fatal("all nodes with children should be expanded")
}
}