diff --git a/Keybinding.Test/SemanticCommandNormalizationTests.cs b/Keybinding.Test/SemanticCommandNormalizationTests.cs new file mode 100644 index 0000000..1a7e5ef --- /dev/null +++ b/Keybinding.Test/SemanticCommandNormalizationTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Keybinding.Test; + +using ktsu.Keybinding.Core.Models; +using ktsu.Keybinding.Core.Services; + +[TestClass] +public class SemanticCommandNormalizationTests +{ + private static Command CreatePaddedCommand() => + new(CommandId.Create("edit.copy"), CommandName.Create(" Copy "), CommandDescription.Create(" "), CommandCategory.Create("Editing ")); + + [TestMethod] + public void Constructor_PaddedNameAndCategory_AreTrimmed() + { + Command command = CreatePaddedCommand(); + + Assert.AreEqual("Copy", command.Name.ToString()); + Assert.AreEqual("Editing", command.Category?.ToString()); + } + + [TestMethod] + public void Constructor_WhitespaceOnlyDescriptionAndCategory_BecomeNull() + { + Command command = new(CommandId.Create("edit.copy"), CommandName.Create("Copy"), CommandDescription.Create(" "), CommandCategory.Create(" ")); + + Assert.IsNull(command.Description); + Assert.IsNull(command.Category); + } + + [TestMethod] + public void Constructor_WhitespaceOnlyName_Throws() + { + Assert.ThrowsExactly(() => new Command(CommandId.Create("edit.copy"), CommandName.Create(" "))); + } + + [TestMethod] + public void Constructor_PaddedValues_MatchStringConstructedCommand() + { + Command expected = new("edit.copy", " Copy ", " ", "Editing "); + Command actual = CreatePaddedCommand(); + + Assert.AreEqual(expected.Name.ToString(), actual.Name.ToString()); + Assert.AreEqual(expected.Description?.ToString(), actual.Description?.ToString()); + Assert.AreEqual(expected.Category?.ToString(), actual.Category?.ToString()); + } + + [TestMethod] + public void GetCommandsByCategory_PaddedSemanticCategory_FindsCommandWithoutReload() + { + CommandRegistry registry = new(); + Assert.IsTrue(registry.RegisterCommand(CreatePaddedCommand())); + + Assert.AreEqual(1, registry.GetCommandsByCategory("Editing").Count); + } +} diff --git a/Keybinding/Models/Command.cs b/Keybinding/Models/Command.cs index 6d3dc04..8a94203 100644 --- a/Keybinding/Models/Command.cs +++ b/Keybinding/Models/Command.cs @@ -33,9 +33,14 @@ public Command(CommandId id, CommandName name, CommandDescription? description = } Id = trimmedId == rawId ? id : CommandId.Create(trimmedId); - Name = name; - Description = description; - Category = category; + + // Normalize the rest the same way too, so a command reads the same before and after a save and reload. + string trimmedName = TrimToNull(name.ToString()) + ?? throw new ArgumentException("Command name cannot be null or whitespace", nameof(name)); + + Name = trimmedName == name.ToString() ? name : CommandName.Create(trimmedName); + Description = TrimToNull(description?.ToString()) is string trimmedDescription ? CommandDescription.Create(trimmedDescription) : null; + Category = TrimToNull(category?.ToString()) is string trimmedCategory ? CommandCategory.Create(trimmedCategory) : null; } /// @@ -60,10 +65,13 @@ public Command(string id, string name, string? description = null, string? categ Id = CommandId.Create(id.Trim()); Name = CommandName.Create(name.Trim()); - Description = !string.IsNullOrWhiteSpace(description) ? CommandDescription.Create(description.Trim()) : null; - Category = !string.IsNullOrWhiteSpace(category) ? CommandCategory.Create(category.Trim()) : null; + Description = TrimToNull(description) is string trimmedDescription ? CommandDescription.Create(trimmedDescription) : null; + Category = TrimToNull(category) is string trimmedCategory ? CommandCategory.Create(trimmedCategory) : null; } + private static string? TrimToNull(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + /// /// Gets the unique identifier for the command ///