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
simonfrankenandClaude Opus 5.5 c9932932bb Make the screen explain itself: labels, tools, help pages, hints
For developers new to submodules (notes/usability-plan.md, D32):
- x and X read "restore to pin" and "restore committed pins"
- problems only, filter and fetch are shown in the footer
- ? shows every key in titled columns, then a legend of the status
  words, symbols and spine roles; esc closes it
- u reads "pull (fast-forward)" on the superproject
- the detail pane suggests f when the last fetch is old
- a hint above the footer until the first key
- the header says "staged", like the tree

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 23:26:32 +02:00

140 lines
4.4 KiB
Go

package footer
import (
"strings"
"testing"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"submarine/internal/tui/theme"
)
type testKeys struct{ highlight []key.Binding }
func bind(k, desc string) key.Binding { return key.NewBinding(key.WithKeys(k), key.WithHelp(k, desc)) }
func (testKeys) Navigation() []key.Binding {
return []key.Binding{bind("j", "move"), bind("h", "fold")}
}
func (testKeys) Tools() []key.Binding {
return []key.Binding{bind("!", "problems only"), bind("/", "filter"), bind("f", "fetch")}
}
func (testKeys) Submodule() []key.Binding { return []key.Binding{bind("x", "restore to pin")} }
func (testKeys) Global() []key.Binding { return []key.Binding{bind("?", "help"), bind("Q", "quit")} }
func (k testKeys) Highlight() []key.Binding { return k.highlight }
func (testKeys) HelpGroups() []Group {
return []Group{
{Title: "Move", Keys: []key.Binding{bind("j", "down"), bind("k", "up")}},
{Title: "Selected submodule", Keys: []key.Binding{bind("x", "restore to pin")}},
{Title: "App", Keys: []key.Binding{bind("?", "what words mean"), bind("Q", "quit")}},
}
}
func (testKeys) LegendKeys() Group {
return Group{Title: "App", Keys: []key.Binding{bind("esc", "close")}}
}
func bar(w int, hl ...key.Binding) string {
m := New(theme.Default(), testKeys{hl})
m.SetWidth(w)
return ansi.Strip(m.View())
}
func TestUpdateIsRightmost(t *testing.T) {
got := strings.TrimRight(bar(80, bind("q", "quit and update to v1.2.3")), " ")
if !strings.HasSuffix(got, "? help • Q quit q quit and update to v1.2.3") {
t.Errorf("order must be help, quit, quit and update:\n%q", got)
}
if w := lipgloss.Width(bar(80, bind("q", "quit and update to v1.2.3"))); w != 80 {
t.Errorf("bar is %d wide, want 80", w)
}
}
func TestNoUpdate(t *testing.T) {
if got := bar(80); strings.Contains(got, "update") {
t.Errorf("unexpected update hint:\n%q", got)
}
}
func TestNavigationGivesWayToUpdate(t *testing.T) {
got := bar(50, bind("q", "quit and update to v1.2.3"))
if !strings.Contains(got, "q quit and update to v1.2.3") || !strings.Contains(got, "Q quit") {
t.Errorf("the right group must be kept:\n%q", got)
}
}
func TestToolsFollowNavigation(t *testing.T) {
got := bar(80)
if !strings.Contains(got, "h fold • ! problems only • / filter • f fetch") {
t.Errorf("tools must follow the navigation keys:\n%q", got)
}
}
func TestToolsGiveWayBeforeNavigation(t *testing.T) {
got := bar(40)
if strings.Contains(got, "f fetch") || !strings.Contains(got, "j move") {
t.Errorf("tools must go before navigation:\n%q", got)
}
}
func TestSubmoduleKeysGoFirst(t *testing.T) {
m := New(theme.Default(), testKeys{})
m.SetActions(true)
m.SetWidth(80)
got := ansi.Strip(m.View())
if strings.Contains(got, "restore to pin") || !strings.Contains(got, "f fetch") {
t.Errorf("submodule keys must go before the tools:\n%q", got)
}
m.SetWidth(120)
if got := ansi.Strip(m.View()); !strings.Contains(got, "f fetch • x restore to pin") {
t.Errorf("submodule keys follow the tools:\n%q", got)
}
}
func TestHelpPages(t *testing.T) {
m := New(theme.Default(), testKeys{})
m.SetWidth(120)
m.CycleHelp()
keys := ansi.Strip(m.View())
for _, want := range []string{"Move", "Selected submodule", "App", "x restore to pin", "? what words mean"} {
if !strings.Contains(keys, want) {
t.Errorf("key page lacks %q:\n%s", want, keys)
}
}
m.CycleHelp()
if !m.Legend() {
t.Fatal("second page must be the legend")
}
legend := ansi.Strip(m.View())
for _, want := range []string{"Status", "off pin", "Symbols", "Commits", "esc close"} {
if !strings.Contains(legend, want) {
t.Errorf("legend lacks %q:\n%s", want, legend)
}
}
for _, l := range strings.Split(m.View(), "\n") {
if w := lipgloss.Width(l); w > 120 {
t.Errorf("legend line is %d wide: %q", w, ansi.Strip(l))
}
}
m.CycleHelp()
if got := ansi.Strip(m.View()); !strings.Contains(got, "? help") {
t.Errorf("third ? must close the help:\n%q", got)
}
}
func TestHelpLeavesOutColumnsThatDontFit(t *testing.T) {
m := New(theme.Default(), testKeys{})
m.SetWidth(30)
m.CycleHelp()
got := ansi.Strip(m.View())
if !strings.Contains(got, "App") || strings.Contains(got, "Selected") || !strings.Contains(got, "…") {
t.Errorf("want App kept, the columns before it left out with an ellipsis:\n%s", got)
}
for _, l := range strings.Split(m.View(), "\n") {
if w := lipgloss.Width(l); w > 30 {
t.Errorf("line is %d wide: %q", w, ansi.Strip(l))
}
}
}