mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
The number of side panels is about to become configurable, so a fixed count of jump-to-panel keys no longer makes sense: a user who configures six panels shouldn't be forced to also extend jumpToBlock, and one who hides a panel shouldn't have to trim it. Drop the count check entirely (individual keys are still validated) and assign keys to panels positionally, for as many panels as there are keys. Surplus panels go without a jump key but remain reachable via the next/previous-panel keys. This also removes the log.Fatal that the count check guarded against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
394 lines
12 KiB
Go
394 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/samber/lo"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestUserConfigValidate_enums(t *testing.T) {
|
|
type testCase struct {
|
|
value string
|
|
valid bool
|
|
}
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
setup func(config *UserConfig, value string)
|
|
testCases []testCase
|
|
}{
|
|
{
|
|
name: "Gui.StatusPanelView",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Gui.StatusPanelView = value
|
|
},
|
|
testCases: []testCase{
|
|
{value: "dashboard", valid: true},
|
|
{value: "allBranchesLog", valid: true},
|
|
{value: "", valid: false},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Gui.ShowDivergenceFromBaseBranch",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Gui.ShowDivergenceFromBaseBranch = value
|
|
},
|
|
testCases: []testCase{
|
|
{value: "none", valid: true},
|
|
{value: "onlyArrow", valid: true},
|
|
{value: "arrowAndNumber", valid: true},
|
|
{value: "", valid: false},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Git.AutoForwardBranches",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Git.AutoForwardBranches = value
|
|
},
|
|
testCases: []testCase{
|
|
{value: "none", valid: true},
|
|
{value: "onlyMainBranches", valid: true},
|
|
{value: "allBranches", valid: true},
|
|
{value: "", valid: false},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Git.LocalBranchSortOrder",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Git.LocalBranchSortOrder = value
|
|
},
|
|
testCases: []testCase{
|
|
{value: "date", valid: true},
|
|
{value: "recency", valid: true},
|
|
{value: "alphabetical", valid: true},
|
|
{value: "", valid: false},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Git.RemoteBranchSortOrder",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Git.RemoteBranchSortOrder = value
|
|
},
|
|
testCases: []testCase{
|
|
{value: "date", valid: true},
|
|
{value: "recency", valid: false},
|
|
{value: "alphabetical", valid: true},
|
|
{value: "", valid: false},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Git.Log.Order",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Git.Log.Order = value
|
|
},
|
|
testCases: []testCase{
|
|
{value: "date-order", valid: true},
|
|
{value: "author-date-order", valid: true},
|
|
{value: "topo-order", valid: true},
|
|
{value: "default", valid: true},
|
|
|
|
{value: "", valid: false},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Git.Log.ShowGraph",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Git.Log.ShowGraph = value
|
|
},
|
|
testCases: []testCase{
|
|
{value: "always", valid: true},
|
|
{value: "never", valid: true},
|
|
{value: "when-maximised", valid: true},
|
|
|
|
{value: "", valid: false},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Keybindings",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.Keybinding.Universal.Quit = Keybinding{value}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: true},
|
|
{value: "<disabled>", valid: true},
|
|
{value: "q", valid: true},
|
|
{value: "<c-c>", valid: true},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "JumpToBlock keybinding",
|
|
setup: func(config *UserConfig, value string) {
|
|
labels := strings.Split(value, ",")
|
|
config.Keybinding.Universal.JumpToBlock = lo.Map(labels, func(label string, _ int) Keybinding {
|
|
return Keybinding{label}
|
|
})
|
|
},
|
|
testCases: []testCase{
|
|
// The number of entries no longer has to match the number of side
|
|
// panels, so only the validity of the individual keys matters.
|
|
{value: "1,2,3", valid: true},
|
|
{value: "1,2,3,4,5", valid: true},
|
|
{value: "1,2,3,4,5,6", valid: true},
|
|
{value: "1,2,3,4,invalid", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom command keybinding",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.CustomCommands = []CustomCommand{
|
|
{
|
|
Key: Keybinding{value},
|
|
Command: "echo 'hello'",
|
|
},
|
|
}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: true},
|
|
{value: "<disabled>", valid: true},
|
|
{value: "q", valid: true},
|
|
{value: "<c-c>", valid: true},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom command keybinding in sub menu",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.CustomCommands = []CustomCommand{
|
|
{
|
|
Key: Keybinding{"X"},
|
|
Description: "My Custom Commands",
|
|
CommandMenu: []CustomCommand{
|
|
{Key: Keybinding{value}, Command: "echo 'hello'", Context: "global"},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: true},
|
|
{value: "<disabled>", valid: true},
|
|
{value: "q", valid: true},
|
|
{value: "<c-c>", valid: true},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom command keybinding in prompt menu",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.CustomCommands = []CustomCommand{
|
|
{
|
|
Key: Keybinding{"X"},
|
|
Description: "My Custom Commands",
|
|
Prompts: []CustomCommandPrompt{
|
|
{
|
|
Options: []CustomCommandMenuOption{
|
|
{Key: Keybinding{value}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: true},
|
|
{value: "<disabled>", valid: true},
|
|
{value: "q", valid: true},
|
|
{value: "<c-c>", valid: true},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom command output",
|
|
setup: func(config *UserConfig, value string) {
|
|
config.CustomCommands = []CustomCommand{
|
|
{
|
|
Output: value,
|
|
},
|
|
}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: true},
|
|
{value: "none", valid: true},
|
|
{value: "terminal", valid: true},
|
|
{value: "log", valid: true},
|
|
{value: "logWithPty", valid: true},
|
|
{value: "popup", valid: true},
|
|
{value: "invalid_value", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom command sub menu",
|
|
setup: func(config *UserConfig, _ string) {
|
|
config.CustomCommands = []CustomCommand{
|
|
{
|
|
Key: Keybinding{"X"},
|
|
Description: "My Custom Commands",
|
|
CommandMenu: []CustomCommand{
|
|
{Key: Keybinding{"1"}, Command: "echo 'hello'", Context: "global"},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: true},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom command sub menu",
|
|
setup: func(config *UserConfig, _ string) {
|
|
config.CustomCommands = []CustomCommand{
|
|
{
|
|
Key: Keybinding{"X"},
|
|
Context: "global", // context is not allowed for submenus
|
|
CommandMenu: []CustomCommand{
|
|
{Key: Keybinding{"1"}, Command: "echo 'hello'", Context: "global"},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: false},
|
|
},
|
|
},
|
|
{
|
|
name: "Custom command sub menu",
|
|
setup: func(config *UserConfig, _ string) {
|
|
config.CustomCommands = []CustomCommand{
|
|
{
|
|
Key: Keybinding{"X"},
|
|
LoadingText: "loading", // other properties are not allowed for submenus (using loadingText as an example)
|
|
CommandMenu: []CustomCommand{
|
|
{Key: Keybinding{"1"}, Command: "echo 'hello'", Context: "global"},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
testCases: []testCase{
|
|
{value: "", valid: false},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
for _, testCase := range s.testCases {
|
|
config := GetDefaultConfig()
|
|
s.setup(config, testCase.value)
|
|
err := config.Validate()
|
|
|
|
if testCase.valid {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserConfigValidate_spinnerFrames(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
frames []string
|
|
valid bool
|
|
}{
|
|
{name: "empty", frames: []string{}, valid: false},
|
|
{name: "single frame", frames: []string{"|"}, valid: true},
|
|
{name: "all same width", frames: []string{"|", "/", "-", "\\"}, valid: true},
|
|
{name: "all same width, multi-char", frames: []string{". ", ".. ", "..."}, valid: true},
|
|
{name: "all same width, wide runes", frames: []string{"⠋", "⠙", "⠹"}, valid: true},
|
|
{name: "differing widths", frames: []string{"|", "//"}, valid: false},
|
|
{name: "first differs from rest", frames: []string{"||", "/", "-"}, valid: false},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
config := GetDefaultConfig()
|
|
config.Gui.Spinner.Frames = s.frames
|
|
err := config.Validate()
|
|
|
|
if s.valid {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserConfigValidate_sidePanels(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
panels []SidePanel
|
|
valid bool
|
|
}{
|
|
{name: "default layout", panels: []SidePanel{{"status"}, {"files", "worktrees", "submodules"}, {"branches", "remotes", "tags"}, {"commits", "reflog"}, {"stash"}}, valid: true},
|
|
{name: "reordered", panels: []SidePanel{{"status"}, {"files"}, {"commits"}, {"branches"}, {"stash"}}, valid: true},
|
|
{name: "hidden stash panel", panels: []SidePanel{{"status"}, {"files"}, {"branches"}, {"commits"}}, valid: true},
|
|
{name: "promoted tab", panels: []SidePanel{{"files", "submodules"}, {"worktrees"}, {"branches"}, {"commits"}}, valid: true},
|
|
{name: "core panels only", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}}, valid: true},
|
|
{name: "empty", panels: []SidePanel{}, valid: false},
|
|
{name: "empty panel", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}, {}}, valid: false},
|
|
{name: "unknown name", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}, {"bogus"}}, valid: false},
|
|
{name: "duplicate within panel", panels: []SidePanel{{"files", "files"}, {"branches"}, {"commits"}}, valid: false},
|
|
{name: "duplicate across panels", panels: []SidePanel{{"files"}, {"branches", "files"}, {"commits"}}, valid: false},
|
|
{name: "missing files", panels: []SidePanel{{"branches"}, {"commits"}}, valid: false},
|
|
{name: "missing branches", panels: []SidePanel{{"files"}, {"commits"}}, valid: false},
|
|
{name: "missing commits", panels: []SidePanel{{"files"}, {"branches"}}, valid: false},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
config := GetDefaultConfig()
|
|
config.Gui.SidePanels = s.panels
|
|
err := config.Validate()
|
|
|
|
if s.valid {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserConfigValidate_pagers(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
pager PagingConfig
|
|
valid bool
|
|
}{
|
|
{name: "empty", pager: PagingConfig{}, valid: true},
|
|
{name: "pager only", pager: PagingConfig{Pager: "delta"}, valid: true},
|
|
{name: "external diff command only", pager: PagingConfig{ExternalDiffCommand: "difft"}, valid: true},
|
|
{name: "git config external diff only", pager: PagingConfig{UseExternalDiffGitConfig: true}, valid: true},
|
|
{name: "pager and external diff command", pager: PagingConfig{Pager: "delta", ExternalDiffCommand: "difft"}, valid: false},
|
|
{name: "pager and git config external diff", pager: PagingConfig{Pager: "delta", UseExternalDiffGitConfig: true}, valid: false},
|
|
{name: "both external diff mechanisms", pager: PagingConfig{ExternalDiffCommand: "difft", UseExternalDiffGitConfig: true}, valid: false},
|
|
{name: "all three", pager: PagingConfig{Pager: "delta", ExternalDiffCommand: "difft", UseExternalDiffGitConfig: true}, valid: false},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
config := GetDefaultConfig()
|
|
config.Git.Pagers = []PagingConfig{s.pager}
|
|
err := config.Validate()
|
|
|
|
if s.valid {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|