package hotkeys import ( "strings" "testing" ) // --------------------------------------------------------------------------- // TestParseInputConf - parsing various input.conf line formats // --------------------------------------------------------------------------- func TestParseInputConf(t *testing.T) { tests := []struct { name string input string expectBindingKeys []string // Keys of actual bindings (non-comment/empty/section) expectComments int expectEmpty int expectSections int expectTotal int }{ { name: "normal binding", input: "SPACE cycle pause", expectBindingKeys: []string{"SPACE"}, expectTotal: 1, }, { name: "binding with inline comment", input: "q quit # Quit playback", expectBindingKeys: []string{"q"}, expectTotal: 1, }, { name: "comment line", input: "# This is a comment", expectComments: 1, expectTotal: 1, }, { name: "empty line", input: "", expectEmpty: 1, expectTotal: 1, }, { name: "section header", input: "[default]", expectSections: 1, expectTotal: 1, }, { name: "multi-word command", input: "Ctrl+Shift+RIGHT seek 30", expectBindingKeys: []string{"Ctrl+Shift+RIGHT"}, expectTotal: 1, }, { name: "mixed content", input: `# mpv input.conf preset SPACE cycle pause # Seeking RIGHT seek 5 [default] Ctrl+RIGHT seek 15`, expectBindingKeys: []string{"SPACE", "RIGHT", "Ctrl+RIGHT"}, expectComments: 2, expectEmpty: 1, expectSections: 1, expectTotal: 7, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config, err := ParseInputConf(tt.input) if err != nil { t.Fatalf("ParseInputConf() unexpected error: %v", err) } if len(config.Bindings) != tt.expectTotal { t.Errorf("total bindings = %d, expected %d", len(config.Bindings), tt.expectTotal) } // Count types var actualBindings []string var comments, empties, sections int for _, b := range config.Bindings { switch { case b.IsComment: comments++ case b.IsEmpty: empties++ case b.IsSection: sections++ default: actualBindings = append(actualBindings, b.Key) } } if comments != tt.expectComments { t.Errorf("comment lines = %d, expected %d", comments, tt.expectComments) } if empties != tt.expectEmpty { t.Errorf("empty lines = %d, expected %d", empties, tt.expectEmpty) } if sections != tt.expectSections { t.Errorf("section lines = %d, expected %d", sections, tt.expectSections) } if len(actualBindings) != len(tt.expectBindingKeys) { t.Errorf("binding count = %d, expected %d", len(actualBindings), len(tt.expectBindingKeys)) } else { for i, key := range tt.expectBindingKeys { if actualBindings[i] != key { t.Errorf("binding[%d].Key = %q, expected %q", i, actualBindings[i], key) } } } }) } } // TestParseBindingLineCommentExtraction verifies inline comments are captured. func TestParseBindingLineCommentExtraction(t *testing.T) { input := "q quit # Quit playback" config, err := ParseInputConf(input) if err != nil { t.Fatalf("ParseInputConf() unexpected error: %v", err) } if len(config.Bindings) != 1 { t.Fatalf("expected 1 binding, got %d", len(config.Bindings)) } b := config.Bindings[0] if b.Comment != " Quit playback" { t.Errorf("Comment = %q, expected %q", b.Comment, " Quit playback") } if b.Key != "q" { t.Errorf("Key = %q, expected %q", b.Key, "q") } if b.Command != "quit" { t.Errorf("Command = %q, expected %q", b.Command, "quit") } } // --------------------------------------------------------------------------- // TestSerializeInputConf - roundtrip fidelity // --------------------------------------------------------------------------- func TestSerializeInputConf(t *testing.T) { tests := []struct { name string input string }{ {"simple binding", "SPACE cycle pause"}, {"binding with comment", "q quit # Quit playback"}, {"comment line", "# This is a comment"}, {"section header", "[default]"}, {"empty line", ""}, {"multi-field command", "Ctrl+Shift+RIGHT seek 30"}, {"full file", `# mpv input.conf preset SPACE cycle pause # Seeking RIGHT seek 5 LEFT seek -5 [default] Ctrl+RIGHT seek 15`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config, err := ParseInputConf(tt.input) if err != nil { t.Fatalf("ParseInputConf() unexpected error: %v", err) } serialized := SerializeInputConf(config) // Normalize for comparison: split and rejoin to handle trailing newline differences inputLines := strings.Split(tt.input, "\n") outputLines := strings.Split(serialized, "\n") // SerializeInputConf produces len(bindings) lines joined by "\n" // which is the same number of lines as input split by "\n" if len(outputLines) != len(inputLines) { t.Errorf("roundtrip line count = %d, expected %d", len(outputLines), len(inputLines)) return } for i, want := range inputLines { got := outputLines[i] // For binding lines with inline comments, the serializer outputs " #Comment" // while input may have " # Comment". We just check the key/command portion. // For structural lines (empty, comment, section) we expect exact match. b := config.Bindings[i] if b.IsComment || b.IsEmpty || b.IsSection { // Section headers preserve RawLine exactly if b.IsSection { if got != want { t.Errorf("line %d: got %q, expected %q", i, got, want) } } // Empty lines are empty in both if b.IsEmpty { if got != "" { t.Errorf("line %d: expected empty, got %q", i, got) } } } else { // Binding: check key and command match if b.Key == "" { continue } if !strings.HasPrefix(got, b.Key+" "+b.Command) { t.Errorf("line %d: got %q, expected to start with %q", i, got, b.Key+" "+b.Command) } } } }) } } // TestSerializeInputConfRoundtripSimple verifies a simple binding survives parse→serialize→parse. func TestSerializeInputConfRoundtripSimple(t *testing.T) { original := "SPACE cycle pause" config1, _ := ParseInputConf(original) serialized := SerializeInputConf(config1) config2, _ := ParseInputConf(serialized) if len(config2.Bindings) != 1 { t.Fatalf("roundtrip: expected 1 binding, got %d", len(config2.Bindings)) } if config2.Bindings[0].Key != "SPACE" { t.Errorf("roundtrip: Key = %q, expected %q", config2.Bindings[0].Key, "SPACE") } if config2.Bindings[0].Command != "cycle pause" { t.Errorf("roundtrip: Command = %q, expected %q", config2.Bindings[0].Command, "cycle pause") } } // --------------------------------------------------------------------------- // TestFindBinding // --------------------------------------------------------------------------- func TestFindBinding(t *testing.T) { input := `# comment SPACE cycle pause RIGHT seek 5 Ctrl+RIGHT seek 15 ` config, err := ParseInputConf(input) if err != nil { t.Fatalf("ParseInputConf() unexpected error: %v", err) } tests := []struct { name string key string expectCmd string expectNil bool }{ {"found SPACE", "SPACE", "cycle pause", false}, {"found RIGHT", "RIGHT", "seek 5", false}, {"found Ctrl+RIGHT", "Ctrl+RIGHT", "seek 15", false}, {"not found LEFT", "LEFT", "", true}, {"not found - empty key", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := FindBinding(config, tt.key) if tt.expectNil { if b != nil { t.Errorf("FindBinding(%q) expected nil, got %+v", tt.key, b) } } else { if b == nil { t.Fatalf("FindBinding(%q) expected non-nil", tt.key) } if b.Command != tt.expectCmd { t.Errorf("FindBinding(%q).Command = %q, expected %q", tt.key, b.Command, tt.expectCmd) } } }) } } // --------------------------------------------------------------------------- // TestSetBinding // --------------------------------------------------------------------------- func TestSetBinding(t *testing.T) { t.Run("add new binding", func(t *testing.T) { input := "SPACE cycle pause\nRIGHT seek 5\n" config, _ := ParseInputConf(input) SetBinding(config, "LEFT", "seek -5", "") b := FindBinding(config, "LEFT") if b == nil { t.Fatal("SetBinding: expected LEFT binding to exist") } if b.Command != "seek -5" { t.Errorf("SetBinding: Command = %q, expected %q", b.Command, "seek -5") } }) t.Run("update existing binding", func(t *testing.T) { input := "SPACE cycle pause\nRIGHT seek 5\n" config, _ := ParseInputConf(input) SetBinding(config, "RIGHT", "seek 10", "faster seek") b := FindBinding(config, "RIGHT") if b == nil { t.Fatal("SetBinding: expected RIGHT binding to exist") } if b.Command != "seek 10" { t.Errorf("SetBinding: Command = %q, expected %q", b.Command, "seek 10") } if b.Comment != "faster seek" { t.Errorf("SetBinding: Comment = %q, expected %q", b.Comment, "faster seek") } }) t.Run("preserves other bindings", func(t *testing.T) { input := "SPACE cycle pause\nRIGHT seek 5\n" config, _ := ParseInputConf(input) SetBinding(config, "LEFT", "seek -5", "") space := FindBinding(config, "SPACE") if space == nil || space.Command != "cycle pause" { t.Error("SetBinding: adding new binding should not affect existing ones") } }) } // --------------------------------------------------------------------------- // TestRemoveBinding // --------------------------------------------------------------------------- func TestRemoveBinding(t *testing.T) { t.Run("remove existing binding", func(t *testing.T) { input := "SPACE cycle pause\nRIGHT seek 5\nLEFT seek -5\n" config, _ := ParseInputConf(input) removed := RemoveBinding(config, "RIGHT") if !removed { t.Error("RemoveBinding: expected true (found and removed)") } if FindBinding(config, "RIGHT") != nil { t.Error("RemoveBinding: RIGHT should no longer exist") } // Other bindings should still be present if FindBinding(config, "SPACE") == nil { t.Error("RemoveBinding: SPACE should still exist") } if FindBinding(config, "LEFT") == nil { t.Error("RemoveBinding: LEFT should still exist") } }) t.Run("remove non-existent binding", func(t *testing.T) { input := "SPACE cycle pause\n" config, _ := ParseInputConf(input) removed := RemoveBinding(config, "NONEXISTENT") if removed { t.Error("RemoveBinding: expected false for non-existent key") } }) t.Run("remove preserves surrounding bindings", func(t *testing.T) { input := "SPACE cycle pause\nRIGHT seek 5\nLEFT seek -5\n" config, _ := ParseInputConf(input) originalLen := len(config.Bindings) RemoveBinding(config, "RIGHT") if len(config.Bindings) != originalLen-1 { t.Errorf("RemoveBinding: len = %d, expected %d", len(config.Bindings), originalLen-1) } }) } // --------------------------------------------------------------------------- // TestGetPresetList // --------------------------------------------------------------------------- func TestGetPresetList(t *testing.T) { presets := GetPresetList() if len(presets) != 3 { t.Fatalf("GetPresetList: expected 3 presets, got %d", len(presets)) } expectedIDs := []string{"default", "vlc", "mpc-hc"} expectedNames := []string{"mpv Default", "VLC", "MPC-HC"} for i, p := range presets { if p.ID != expectedIDs[i] { t.Errorf("presets[%d].ID = %q, expected %q", i, p.ID, expectedIDs[i]) } if p.Name != expectedNames[i] { t.Errorf("presets[%d].Name = %q, expected %q", i, p.Name, expectedNames[i]) } if p.Description == "" { t.Errorf("presets[%d].Description should not be empty", i) } } } // --------------------------------------------------------------------------- // TestLoadPreset // --------------------------------------------------------------------------- func TestLoadPreset(t *testing.T) { tests := []struct { name string presetName string wantError bool errorSubstr string }{ {"default preset", "default", false, ""}, {"vlc preset", "vlc", false, ""}, {"mpc-hc preset", "mpc-hc", false, ""}, {"unknown preset", "nonexistent", true, "unknown hotkey preset"}, {"empty preset name", "", true, "unknown hotkey preset"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { content, err := LoadPreset(tt.presetName) if (err != nil) != tt.wantError { t.Errorf("LoadPreset(%q) error = %v, wantError %v", tt.presetName, err, tt.wantError) return } if tt.wantError && err != nil { if !strings.Contains(err.Error(), tt.errorSubstr) { t.Errorf("LoadPreset(%q) error = %v, expected to contain %q", tt.presetName, err.Error(), tt.errorSubstr) } return } if !tt.wantError { if len(content) == 0 { t.Errorf("LoadPreset(%q): expected non-empty content", tt.presetName) } // Quick sanity: should contain at least one "cycle" command if !strings.Contains(content, "cycle") { t.Errorf("LoadPreset(%q): expected content to contain 'cycle'", tt.presetName) } } }) } } // TestLoadPresetNoDuplicateKeys verifies that none of the embedded presets // contain duplicate key bindings (the bug that was fixed for mpc-hc). func TestLoadPresetNoDuplicateKeys(t *testing.T) { for _, preset := range GetPresetList() { t.Run(preset.ID, func(t *testing.T) { content, err := LoadPreset(preset.ID) if err != nil { t.Fatalf("LoadPreset(%q) error: %v", preset.ID, err) } config, err := ParseInputConf(content) if err != nil { t.Fatalf("ParseInputConf(%q) error: %v", preset.ID, err) } seen := make(map[string]bool) for _, b := range config.Bindings { if b.IsComment || b.IsEmpty || b.IsSection || b.Key == "" { continue } if seen[b.Key] { t.Errorf("preset %q has duplicate key %q", preset.ID, b.Key) } seen[b.Key] = true } }) } }