From 53818f8e048f2f6557c8342e8e7d0eedffa37146 Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 30 May 2026 12:17:09 +0100 Subject: [PATCH 1/2] add xunit console reporter for all tests --- .../TestStack.BDDfy.Samples.csproj | 2 +- .../FluentScanner/StepsWithChainedMethods.cs | 40 +++++++++++++++++++ .../TestStack.BDDfy.Tests.csproj | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs diff --git a/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj b/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj index 803424c9..eb227dc8 100644 --- a/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj +++ b/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs new file mode 100644 index 00000000..dc9f9677 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs @@ -0,0 +1,40 @@ +using Shouldly; +using System; +using TestStack.BDDfy.Configuration; +using Xunit; + +namespace TestStack.BDDfy.Tests.Scanner.FluentScanner +{ + public class StepsWithChainedMethods + { + private string diagnostics; + private SutStepBuilder _sutStepBuilder = new(); + private class SutStepBuilder + { + public string color { get; set; } + public string horn { get; set; } + public SutStepBuilder with_color(string color) { this.color = color; return this; } + public SutStepBuilder with_horn(string horn) { this.horn = horn; return this; } + } + + [Fact] + public void HonkingTheHornShouldSound() + { + //Configurator.Processors.Add(()=> new XUnitOutputReporter()); + + this.Given(_ => i_have_a_car().with_color("red").with_horn("crazy")) + .When(_ => i_honk_the_horn()) + .Then(_ => diagnostics_will_log("red car honked crazy horn")) + .BDDfy(); + } + + private void diagnostics_will_log(string v) => diagnostics.ShouldBe(v); + + private void i_honk_the_horn() + { + diagnostics = $"{_sutStepBuilder.color} car honked {_sutStepBuilder.horn} horn"; + } + + private SutStepBuilder i_have_a_car() => _sutStepBuilder; + } +} diff --git a/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj b/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj index f9fd8232..e4bb5a53 100644 --- a/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj +++ b/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj @@ -13,7 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From cf7fe70ccc9751a0336cabf042c2ffbdc81be9bf Mon Sep 17 00:00:00 2001 From: Gurpreet Singh Date: Sat, 30 May 2026 14:47:16 +0100 Subject: [PATCH 2/2] update expression extensions to allow visiting chained expressions updated fluent scanner to allow creating titles for chained methods added startup for tests project so that outputs are always printed to console for examination added tests for added functionality --- ...pNameWillBeBuiltFromStepTitle.approved.txt | 6 + ...sAreBuiltFromAllChanedMethods.approved.txt | 6 + .../FluentScanner/StepsWithChainedMethods.cs | 49 +++++--- src/TestStack.BDDfy.Tests/Startup.cs | 16 +++ .../Abstractions/DefaultStepTitleFactory.cs | 4 +- .../Abstractions/IStepTitleFactory.cs | 2 +- .../Fluent/ExpressionExtensions.cs | 20 ++-- .../Scanners/StepScanners/Fluent/FluentApi.cs | 26 ++-- .../StepScanners/Fluent/FluentScanner.cs | 113 ++++++++++++++---- .../StepScanners/Fluent/StepArgument.cs | 11 +- 10 files changed, 186 insertions(+), 67 deletions(-) create mode 100644 src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNameWillBeBuiltFromStepTitle.approved.txt create mode 100644 src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNamesAreBuiltFromAllChanedMethods.approved.txt create mode 100644 src/TestStack.BDDfy.Tests/Startup.cs diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNameWillBeBuiltFromStepTitle.approved.txt b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNameWillBeBuiltFromStepTitle.approved.txt new file mode 100644 index 00000000..4d533684 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNameWillBeBuiltFromStepTitle.approved.txt @@ -0,0 +1,6 @@ + +### Step name will be built from step title + i have a red car with a crazy horn + When i honk the horn at volume high + Then i verify diagnostics a red car honked crazy horn at high volume + diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNamesAreBuiltFromAllChanedMethods.approved.txt b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNamesAreBuiltFromAllChanedMethods.approved.txt new file mode 100644 index 00000000..26b83725 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNamesAreBuiltFromAllChanedMethods.approved.txt @@ -0,0 +1,6 @@ + +### Step names are built from all chaned methods + Given i have a car with color red with horn crazy + When i honk the horn + Then i verify diagnostics a red car honked crazy horn + diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs index dc9f9677..4410e187 100644 --- a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs @@ -1,40 +1,57 @@ using Shouldly; -using System; -using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Reporters; +using TestStack.BDDfy.Reporters.MarkDown; +using TestStack.BDDfy.Tests.Reporters; using Xunit; namespace TestStack.BDDfy.Tests.Scanner.FluentScanner { public class StepsWithChainedMethods { - private string diagnostics; private SutStepBuilder _sutStepBuilder = new(); private class SutStepBuilder { - public string color { get; set; } - public string horn { get; set; } + public string state; + private string color; + private string horn; + public SutStepBuilder with_color(string color) { this.color = color; return this; } public SutStepBuilder with_horn(string horn) { this.horn = horn; return this; } + internal void at_volume(string volume) { state+= $" at {volume} volume"; } + internal SutStepBuilder have_a_car() { state = "a"; return this; } + internal SutStepBuilder honk_the_horn() { state+= $" {color} car honked {horn} horn"; return this; } + + internal void verify_diagnostics(string v) => state.ShouldBe(v); } [Fact] - public void HonkingTheHornShouldSound() + public void StepNamesAreBuiltFromAllChanedMethods() { - //Configurator.Processors.Add(()=> new XUnitOutputReporter()); + var story = this.Given(_ => i().have_a_car().with_color("red").with_horn("crazy")) + .When(_ => i().honk_the_horn()) + .Then(_ => i().verify_diagnostics("a red car honked crazy horn")) + .LazyBDDfy(); - this.Given(_ => i_have_a_car().with_color("red").with_horn("crazy")) - .When(_ => i_honk_the_horn()) - .Then(_ => diagnostics_will_log("red car honked crazy horn")) - .BDDfy(); + var result = story.Run(); + var model = new[] {result}.ToReportModel(); + var fileReport = new FileReportModel(model); + ReportApprover.Approve(fileReport, new MarkDownReportBuilder()); } - private void diagnostics_will_log(string v) => diagnostics.ShouldBe(v); - - private void i_honk_the_horn() + [Fact] + public void StepNameWillBeBuiltFromStepTitle() { - diagnostics = $"{_sutStepBuilder.color} car honked {_sutStepBuilder.horn} horn"; + var story = this.Given(_ => i().have_a_car().with_color("red").with_horn("crazy"), "i have a {0} car with a {1} horn") + .When(_ => i().honk_the_horn().at_volume("high")) + .Then(_ => i().verify_diagnostics("a red car honked crazy horn at high volume")) + .LazyBDDfy(); + + var result = story.Run(); + var model = new[] { result }.ToReportModel(); + var fileReport = new FileReportModel(model); + ReportApprover.Approve(fileReport, new MarkDownReportBuilder()); } - private SutStepBuilder i_have_a_car() => _sutStepBuilder; + private SutStepBuilder i() => _sutStepBuilder; } } diff --git a/src/TestStack.BDDfy.Tests/Startup.cs b/src/TestStack.BDDfy.Tests/Startup.cs new file mode 100644 index 00000000..e4c67152 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Startup.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; +using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Tests; + +namespace TestStack.BDDfy.Samples +{ + public class Startup + { + [ModuleInitializer] + public static void Initialize() + { + Configurator.Processors.Add(() => new XUnitOutputReporter()); + Configurator.Processors.ConsoleReport.Enable(); + } + } +} \ No newline at end of file diff --git a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs index 3aacce02..374ffde8 100644 --- a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs +++ b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs @@ -10,7 +10,7 @@ internal class DefaultStepTitleFactory : IStepTitleFactory public bool IncludeInputsInStepTitle { get; set; } = true; public StepTitle Create( - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, MethodInfo methodInfo, StepArgument[] inputArguments, @@ -19,7 +19,7 @@ public StepTitle Create( { string createTitle() { - var flatInputArray = inputArguments.Select(o => o.Value).FlattenArrays(); + var flatInputArray = inputArguments.Select(o => o.Value!).FlattenArrays(); var name = methodInfo.Name; var titleAttribute = methodInfo.GetCustomAttribute(true); var executableAttribute = methodInfo.GetCustomAttribute(true); diff --git a/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs index 14c16bd5..3a264cbe 100644 --- a/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs +++ b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs @@ -7,7 +7,7 @@ public interface IStepTitleFactory bool IncludeInputsInStepTitle { get; set; } public StepTitle Create( - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, MethodInfo methodInfo, StepArgument[] inputArguments, diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs index c5eabc5e..c65331c8 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs @@ -21,10 +21,10 @@ public static IEnumerable ExtractArguments(this Expression _arguments; - private object _value; + private List _arguments = []; + private object? _value; - public IEnumerable ExtractArguments(LambdaExpression methodCallExpression, object value) + public IEnumerable ExtractArguments(LambdaExpression methodCallExpression, object? value) { _arguments = []; _value = value; @@ -34,6 +34,8 @@ public IEnumerable ExtractArguments(LambdaExpression methodCallExp protected override Expression VisitMethodCall(MethodCallExpression node) { + if (node.Object is MethodCallExpression methodCallExpression) Visit(methodCallExpression); + var arguments = node.Arguments.Select(ExtractStepArgument); _arguments.AddRange(arguments); return node; @@ -75,32 +77,32 @@ private StepArgument ExtractStepArgument(Expression a) } } - private Func GetValue(Expression a) + private Func GetValue(Expression expression) { // If the expression is a member access on the lambda parameter (e.g. _ => _.Prop) // replace the parameter with the supplied _value so the compiled delegate can be invoked - if (a is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) + if (expression is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) { var replaced = Expression.Convert(Expression.MakeMemberAccess(Expression.Constant(_value), memberExpression.Member), typeof(object)); return Expression.Lambda>(replaced).Compile(); } - return Expression.Lambda>(Expression.Convert(a, typeof(object))).Compile(); + return Expression.Lambda>(Expression.Convert(expression, typeof(object))).Compile(); } - private Action SetValue(Expression a, Type parameterType) + private Action SetValue(Expression expression, Type parameterType) { var parameter = Expression.Parameter(typeof(object)); var unaryExpression = Expression.Convert(parameter, parameterType); - if (a is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) + if (expression is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) { var memberAccess = Expression.MakeMemberAccess(Expression.Constant(_value), memberExpression.Member); var assign = Expression.Assign(memberAccess, unaryExpression); return Expression.Lambda>(assign, parameter).Compile(); } - var assignDefault = Expression.Assign(a, unaryExpression); + var assignDefault = Expression.Assign(expression, unaryExpression); return Expression.Lambda>(assignDefault, parameter).Compile(); } } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs index c5e3f0fb..dbd3fbcc 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs @@ -404,7 +404,7 @@ public FluentStepBuilder(TScenario testObject) public IFluentStepBuilder Given(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.SetupState, false, "Given"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.SetupState, false, "Given"); return this; } @@ -422,7 +422,7 @@ public IFluentStepBuilder Given(Expression> step) public IFluentStepBuilder Given(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.SetupState, false, "Given"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.SetupState, false, "Given"); return this; } @@ -463,7 +463,7 @@ public IFluentStepBuilder Given(string title) } public IFluentStepBuilder When(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Transition, false, "When"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Transition, false, "When"); return this; } @@ -481,7 +481,7 @@ public IFluentStepBuilder When(Expression> step) public IFluentStepBuilder When(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Transition, false, "When"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Transition, false, "When"); return this; } @@ -522,7 +522,7 @@ public IFluentStepBuilder When(string title) } public IFluentStepBuilder Then(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Assertion, true, "Then"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Assertion, true, "Then"); return this; } @@ -540,7 +540,7 @@ public IFluentStepBuilder Then(Expression> step) public IFluentStepBuilder Then(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Assertion, true, "Then"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Assertion, true, "Then"); return this; } @@ -581,7 +581,7 @@ public IFluentStepBuilder Then(string title) } public IFluentStepBuilder And(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "And"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "And"); return this; } @@ -599,7 +599,7 @@ public IFluentStepBuilder And(Expression> step) public IFluentStepBuilder And(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "And"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "And"); return this; } @@ -640,13 +640,13 @@ public IFluentStepBuilder And(string title) } public IFluentStepBuilder But(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "But"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "But"); return this; } public IFluentStepBuilder But(Expression> step, bool includeInputsInStepTitle) { - scanner.AddStep(step, null, includeInputsInStepTitle, true, ExecutionOrder.ConsecutiveStep, false, "But"); + scanner.AddStep(step, null, includeInputsInStepTitle, false, ExecutionOrder.ConsecutiveStep, false, "But"); return this; } @@ -658,7 +658,7 @@ public IFluentStepBuilder But(Expression> step) public IFluentStepBuilder But(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "But"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "But"); return this; } @@ -699,7 +699,7 @@ public IFluentStepBuilder But(string title) } public IFluentStepBuilder TearDownWith(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, false, ExecutionOrder.TearDown, false, ""); + scanner.AddStep(step, stepTextTemplate, false, false, ExecutionOrder.TearDown, false, ""); return this; } @@ -717,7 +717,7 @@ public IFluentStepBuilder TearDownWith(Expression> public IFluentStepBuilder TearDownWith(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, false, ExecutionOrder.TearDown, false, ""); + scanner.AddStep(step, stepTextTemplate, false, false, ExecutionOrder.TearDown, false, ""); return this; } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs index 15fd6c27..e18159c2 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs @@ -21,7 +21,9 @@ internal FluentScanner(TScenario testObject) { _testObject = testObject; _testContext = TestContext.GetContext(_testObject); - _fakeExecuteActionMethod = typeof(FluentScanner).GetMethod(nameof(ExecuteAction), BindingFlags.Instance | BindingFlags.NonPublic); + _fakeExecuteActionMethod = typeof(FluentScanner) + .GetMethod(nameof(ExecuteAction), BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Failed to retrieve method info for ExecuteAction."); } IScanner IFluentScanner.GetScanner(string scenarioTitle, Type explicitStoryType) @@ -63,7 +65,7 @@ private void ExecuteAction(ExampleAction _) public void AddStep( Expression> stepAction, - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, bool reports, ExecutionOrder executionOrder, @@ -71,16 +73,22 @@ public void AddStep( string stepPrefix) { var action = stepAction.Compile(); - var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); - var title = CreateTitle( - stepTextTemplate, - includeInputsInStepTitle, - GetMethodInfo(stepAction), - inputArguments, - stepPrefix); + StepTitle title; + List args; + + if (string.IsNullOrWhiteSpace(stepTextTemplate) && IsChainedMethodCall(stepAction.Body)) + { + title = FluentScanner.BuildChainedTitle(stepAction, stepPrefix); + args = []; + } + else + { + var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); + title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); + args = [.. inputArguments.Where(s => !string.IsNullOrWhiteSpace(s.Name))]; + } - var args = inputArguments.Where(s => !string.IsNullOrWhiteSpace(s.Name)).ToList(); var stepDelegate = StepActionFactory.GetStepAction(action); var shouldFixAsserts = FixAsserts(asserts, executionOrder); var shouldFixConsecutiveStep = FixConsecutiveStep(executionOrder); @@ -90,7 +98,7 @@ public void AddStep( public void AddStep( Expression> stepAction, - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, bool reports, ExecutionOrder executionOrder, @@ -102,21 +110,37 @@ public void AddStep( AddStep(action, stepAction, stepTextTemplate, includeInputsInStepTitle, reports, executionOrder, asserts, stepPrefix); } - private void AddStep(Action action, LambdaExpression stepAction, string stepTextTemplate, bool? includeInputsInStepTitle, - bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) + private void AddStep( + Action action, + LambdaExpression stepAction, + string? stepTextTemplate, + bool? includeInputsInStepTitle, + bool reports, + ExecutionOrder executionOrder, + bool asserts, + string stepPrefix) { - var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); + StepTitle title; + List args; - var title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); - - var args = inputArguments.Where(s => !string.IsNullOrEmpty(s.Name)).ToList(); + if (string.IsNullOrWhiteSpace(stepTextTemplate) && IsChainedMethodCall(stepAction.Body)) + { + title = FluentScanner.BuildChainedTitle(stepAction, stepPrefix); + args = []; + } + else + { + var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); + title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); + args = [.. inputArguments.Where(s => !string.IsNullOrEmpty(s.Name))]; + } _steps.Add(new Step(StepActionFactory.GetStepAction(action), title, FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, args)); } private StepTitle CreateTitle( - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, MethodInfo methodInfo, StepArgument[] inputArguments, @@ -170,10 +194,57 @@ private ExecutionOrder FixConsecutiveStep(ExecutionOrder executionOrder) return executionOrder; } - private static MethodInfo GetMethodInfo(LambdaExpression stepAction) + private static MethodInfo GetMethodInfo(LambdaExpression stepAction) => ((MethodCallExpression)stepAction.Body).Method; + + private static bool IsChainedMethodCall(Expression body) => body is MethodCallExpression { Object: MethodCallExpression }; + + private static StepTitle BuildChainedTitle(LambdaExpression stepAction, string stepPrefix) + { + var chain = new List<(MethodInfo Method, StepArgument[] Args)>(); + var node = (MethodCallExpression)stepAction.Body; + + while (node is not null) + { + var args = FluentScanner.ExtractArgumentsFromCall(node); + chain.Add((node.Method, args)); + node = node.Object as MethodCallExpression; + } + + chain.Reverse(); + + return new StepTitle(() => + { + var parts = chain.Select(entry => + { + var humanized = Configurator.Humanizer.Humanize(entry.Method.Name); + if (entry.Args.Length > 0) + { + var argValues = entry.Args.Select(a => a.Value?.FlattenArray()).ToArray(); + humanized = humanized + " " + string.Join(", ", argValues); + } + return humanized; + }); + + var title = string.Join(" ", parts).Trim(); + if (!string.IsNullOrEmpty(stepPrefix) && !title.StartsWith(stepPrefix, StringComparison.CurrentCultureIgnoreCase)) + { + title = $"{stepPrefix} {title[..1].ToLower()}{title[1..]}"; + } + return title; + }); + } + + private static StepArgument[] ExtractArgumentsFromCall(MethodCallExpression node) { - var methodCall = (MethodCallExpression)stepAction.Body; - return methodCall.Method; + var parameters = node.Method.GetParameters(); + var args = new StepArgument[node.Arguments.Count]; + for (int i = 0; i < node.Arguments.Count; i++) + { + var arg = node.Arguments[i]; + var value = Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile(); + args[i] = new StepArgument(parameters[i].Name, parameters[i].ParameterType, value, null); + } + return args; } } } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs index 2f09b6c8..f5ebf703 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs @@ -7,12 +7,13 @@ public class StepArgument private readonly Action _set = o => { }; private readonly Func _get; - public StepArgument(string name, Type argumentType, Func getValue, Action setValue) + public StepArgument(string? name, Type argumentType, Func getValue, Action? setValue) { Name = name; _get = getValue; - if (setValue != null) - _set = setValue; + + if (setValue != null) _set = setValue; + ArgumentType = argumentType; } @@ -22,9 +23,9 @@ public StepArgument(Func value) ArgumentType = typeof(object); } - public string Name { get; private set; } + public string? Name { get; private set; } - public object Value + public object? Value { get {