diff --git a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs index 09d0eb983..d2646a8db 100644 --- a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs @@ -489,4 +489,256 @@ public async Task Uses_case_insensitive_comparison_when_no_comparer_is_passed(st // Then result.ShouldBe("Yes"); } + + [Fact] + public void Validate_BoolOverload_ShortCircuits() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a"); + }, "missing a"); + + // When + var result = prompt.Validator?.Invoke("ab"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("too short")); + secondInvoked.ShouldBeFalse(); + } + + [Fact] + public void Validate_BoolOverload_Returns_Chained_Validation_Error() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a"); + }, "missing a"); + + // When + var result = prompt.Validator?.Invoke("bbc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("missing a")); + secondInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_BoolOverload_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => s.Contains("a"), "missing a"); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + } + + [Fact] + public void Validate_FuncOverload_ShortCircuits() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length < 3 ? ValidationResult.Error("too short") : ValidationResult.Success()) + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("ab"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("too short")); + secondInvoked.ShouldBeFalse(); + } + + [Fact] + public void Validate_FuncOverload_Returns_Chained_Validation_Error() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length < 3 ? ValidationResult.Error("too short") : ValidationResult.Success()) + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("bbc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("missing a")); + secondInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_FuncOverload_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + + prompt + .Validate(s => s.Length < 3 ? ValidationResult.Error("too short") : ValidationResult.Success()) + .Validate(s => s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a") ); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + } + + [Fact] + public void Validate_MixedOverloads_ShortCircuits() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("ab"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("too short")); + secondInvoked.ShouldBeFalse(); + } + + [Fact] + public void Validate_MixedOverloads_Returns_Chained_Validation_Error() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a"); + } ); + + // When + var result = prompt.Validator?.Invoke("bbc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("missing a")); + secondInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_MixedOverloads_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => s.Contains("a") ? ValidationResult.Success() : ValidationResult.Error("missing a") ); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + } + + [Fact] + public void Validate_MixedOverloads_WithThreeValidators_Returns_ThirdValidationError() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + var thirdInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") + ? ValidationResult.Success() + : ValidationResult.Error("missing a"); + }) + .Validate(s => + { + thirdInvoked = true; + return s.EndsWith("z"); + }, "must end with z"); + + // When + var result = prompt.Validator?.Invoke("abc"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Error("must end with z")); + secondInvoked.ShouldBeTrue(); + thirdInvoked.ShouldBeTrue(); + } + + [Fact] + public void Validate_MixedOverloads_WithThreeValidators_Returns_Success_When_All_Validators_Pass() + { + // Given + var prompt = new TextPrompt("Enter:"); + var secondInvoked = false; + var thirdInvoked = false; + + prompt + .Validate(s => s.Length >= 3, "too short") + .Validate(s => + { + secondInvoked = true; + return s.Contains("a") + ? ValidationResult.Success() + : ValidationResult.Error("missing a"); + }) + .Validate(s => + { + thirdInvoked = true; + return s.EndsWith("z"); + }, "must end with z"); + + // When + var result = prompt.Validator?.Invoke("abz"); + + // Then + result.ShouldBeEquivalentTo(ValidationResult.Success()); + secondInvoked.ShouldBeTrue(); + thirdInvoked.ShouldBeTrue(); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/TextPromptExtensions.cs b/src/Spectre.Console/Prompts/TextPromptExtensions.cs index e0e9c4662..bb67f36ce 100644 --- a/src/Spectre.Console/Prompts/TextPromptExtensions.cs +++ b/src/Spectre.Console/Prompts/TextPromptExtensions.cs @@ -178,9 +178,20 @@ public static TextPrompt DefaultValue(this TextPrompt obj, T value) public static TextPrompt Validate(this TextPrompt obj, Func validator, string? message = null) { ArgumentNullException.ThrowIfNull(obj); + ArgumentNullException.ThrowIfNull(validator); + var previous = obj.Validator; obj.Validator = result => { + if (previous is not null) + { + var previousResult = previous(result); + if (!previousResult.Successful) + { + return previousResult; + } + } + if (validator(result)) { return ValidationResult.Success(); @@ -202,8 +213,23 @@ public static TextPrompt Validate(this TextPrompt obj, Func va public static TextPrompt Validate(this TextPrompt obj, Func validator) { ArgumentNullException.ThrowIfNull(obj); + ArgumentNullException.ThrowIfNull(validator); + + var previous = obj.Validator; + + obj.Validator = result => + { + if (previous is not null) + { + var previousResult = previous(result); + if (!previousResult.Successful) + { + return previousResult; + } + } - obj.Validator = validator; + return validator(result); + }; return obj; }