diff --git a/CHANGELOG.md b/CHANGELOG.md index 0265dcdc..0c97efb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * New Project Wizard references the latest versions of supported test frameworks and .NET frameworks * Format Document will now right-align numeric values in tables. This can be overridden to left align them by setting `gherkin_table_cell_right_align_numeric_content = false` in .editorconfig file within a `[*.feature]` section. +* Define Steps Command now supports generating async method definitions. ## Bug fixes: diff --git a/Reqnroll.VisualStudio.UI/Dialogs/CreateStepDefinitionsDialog.xaml b/Reqnroll.VisualStudio.UI/Dialogs/CreateStepDefinitionsDialog.xaml index 524459d0..365f2165 100644 --- a/Reqnroll.VisualStudio.UI/Dialogs/CreateStepDefinitionsDialog.xaml +++ b/Reqnroll.VisualStudio.UI/Dialogs/CreateStepDefinitionsDialog.xaml @@ -55,6 +55,9 @@ Regular Expressions Cucumber Expressions --> + diff --git a/Reqnroll.VisualStudio/Configuration/DeveroomConfiguration.cs b/Reqnroll.VisualStudio/Configuration/DeveroomConfiguration.cs index 084582d9..8bda616d 100644 --- a/Reqnroll.VisualStudio/Configuration/DeveroomConfiguration.cs +++ b/Reqnroll.VisualStudio/Configuration/DeveroomConfiguration.cs @@ -21,7 +21,7 @@ public class DeveroomConfiguration public string ConfiguredBindingCulture { get; set; } = null; public string BindingCulture => ConfiguredBindingCulture ?? DefaultFeatureLanguage; public SnippetExpressionStyle SnippetExpressionStyle { get; set; } = SnippetExpressionStyle.CucumberExpression; - + public bool GenerateAsyncSkeletonMethods { get; set; } = true; private void FixEmptyContainers() { diff --git a/Reqnroll.VisualStudio/Configuration/ReqnrollConfigDeserializer.cs b/Reqnroll.VisualStudio/Configuration/ReqnrollConfigDeserializer.cs index 2f4c49a5..863171c1 100644 --- a/Reqnroll.VisualStudio/Configuration/ReqnrollConfigDeserializer.cs +++ b/Reqnroll.VisualStudio/Configuration/ReqnrollConfigDeserializer.cs @@ -26,6 +26,12 @@ public void Populate(string jsonString, DeveroomConfiguration config) if (sdSnippetStyle == "RegexAttribute") config.SnippetExpressionStyle = SnippetExpressionStyle.RegularExpression; } + if (reqnrollJsonConfiguration.Trace != null && + reqnrollJsonConfiguration.Trace.TryGetValue("generateStepDefinitionSkeletonAsAsync", out var generateStepDefinitionSkeletonAsAsync) && + bool.TryParse(generateStepDefinitionSkeletonAsAsync, out bool generateAsyncSkeletonMethods)) + { + config.GenerateAsyncSkeletonMethods = generateAsyncSkeletonMethods; + } } private class ReqnrollJsonConfiguration diff --git a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs index 507d804f..1d2e8ab5 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs @@ -63,24 +63,22 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK const string indent = " "; string newLine = Environment.NewLine; - var feature = (Feature) featureTag.Data; - var viewModel = new CreateStepDefinitionsDialogViewModel(); - viewModel.ClassName = feature.Name.ToIdentifier() + "StepDefinitions"; - viewModel.ExpressionStyle = snippetService.DefaultExpressionStyle; - - foreach (var undefinedStepTag in undefinedStepTags) + var feature = (Feature)featureTag.Data; + var viewModel = new CreateStepDefinitionsDialogViewModel { - var matchResult = (MatchResult) undefinedStepTag.Data; - foreach (var match in matchResult.Items.Where(mi => mi.Type == MatchResultType.Undefined)) - { - var snippet = snippetService.GetStepDefinitionSkeletonSnippet(match.UndefinedStep, - viewModel.ExpressionStyle, indent, newLine); - if (viewModel.Items.Any(i => i.Snippet == snippet)) - continue; - - viewModel.Items.Add(new StepDefinitionSnippetItemViewModel {Snippet = snippet}); - } - } + IsInitializing = true, + Generator = GenerateSnippets, + ClassName = feature.Name.ToIdentifier() + "StepDefinitions", + ExpressionStyle = snippetService.DefaultExpressionStyle, + GenerateAsyncMethods = snippetService.DefaultGenerateSkeletonMethodsAsAsync, + Indent = indent, + NewLine = newLine, + UndefinedStepTags = undefinedStepTags, + SnippetService = snippetService + }; + viewModel.IsInitializing = false; + + viewModel.Items = new ObservableCollection(GenerateSnippets(viewModel)); IdeScope.WindowManager.ShowDialog(viewModel); @@ -113,6 +111,25 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK return true; } + public IEnumerable GenerateSnippets(CreateStepDefinitionsDialogViewModel viewModel) + { + var result = new List(); + foreach (var undefinedStepTag in viewModel.UndefinedStepTags) + { + var matchResult = (MatchResult)undefinedStepTag.Data; + foreach (var match in matchResult.Items.Where(mi => mi.Type == MatchResultType.Undefined)) + { + var snippet = viewModel.SnippetService.GetStepDefinitionSkeletonSnippet(match.UndefinedStep, + viewModel.ExpressionStyle, viewModel.GenerateAsyncMethods, viewModel.Indent, viewModel.NewLine); + if (result.Any(i => i.Snippet == snippet)) + continue; + + result.Add(new StepDefinitionSnippetItemViewModel { Snippet = snippet }); + } + } + return result; + } + private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combinedSnippet, string className, string indent, string newLine) { diff --git a/Reqnroll.VisualStudio/Editor/Commands/GoToDefinitionCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/GoToDefinitionCommand.cs index aeee3bb3..7d0fdbce 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/GoToDefinitionCommand.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/GoToDefinitionCommand.cs @@ -88,7 +88,7 @@ private void PerformOfferCopySnippet(MatchResultItem match, ITextBuffer textBuff string newLine = Environment.NewLine; var snippet = snippetService.GetStepDefinitionSkeletonSnippet(match.UndefinedStep, - snippetService.DefaultExpressionStyle, indent, newLine); + snippetService.DefaultExpressionStyle, snippetService.DefaultGenerateSkeletonMethodsAsAsync, indent, newLine); IdeScope.Actions.ShowQuestion(new QuestionDescription(GoToStepDefinitionsPopupHeader, $"The step is undefined. Do you want to copy a step definition skeleton snippet to the clipboard?{Environment.NewLine}{Environment.NewLine}{snippet}", diff --git a/Reqnroll.VisualStudio/Snippets/Fallback/DeveroomStepDefinitionSkeletonProvider.cs b/Reqnroll.VisualStudio/Snippets/Fallback/DeveroomStepDefinitionSkeletonProvider.cs index 11f0afa9..538eedf9 100644 --- a/Reqnroll.VisualStudio/Snippets/Fallback/DeveroomStepDefinitionSkeletonProvider.cs +++ b/Reqnroll.VisualStudio/Snippets/Fallback/DeveroomStepDefinitionSkeletonProvider.cs @@ -13,7 +13,7 @@ protected DeveroomStepDefinitionSkeletonProvider(ReqnrollProjectTraits projectTr } public string GetStepDefinitionSkeletonSnippet(UndefinedStepDescriptor undefinedStep, - string indent, string newLine, string bindingCultureName) + string indent, bool generateAsyncSnippet, string newLine, string bindingCultureName) { var bindingCulture = CultureInfo.GetCultureInfo(bindingCultureName); @@ -23,9 +23,10 @@ public string GetStepDefinitionSkeletonSnippet(UndefinedStepDescriptor undefined var methodName = GetMethodName(undefinedStep, analyzedStepText); var parameters = string.Join(", ", analyzedStepText.Parameters.Select(ToDeclaration)); var stringPrefix = UseVerbatimStringForExpression ? "@" : ""; + var returnSignature = generateAsyncSnippet ? "async Task" : "void"; var method = $"[{undefinedStep.ScenarioBlock}({stringPrefix}\"{regex}\")]" + newLine + - $"public void {methodName}({parameters})" + newLine + + $"public {returnSignature} {methodName}{(generateAsyncSnippet ? "Async" : "")}({parameters})" + newLine + "{" + newLine + $"{indent}throw new PendingStepException();" + newLine + "}" + newLine; diff --git a/Reqnroll.VisualStudio/Snippets/SnippetService.cs b/Reqnroll.VisualStudio/Snippets/SnippetService.cs index 9a5c9f1f..348ae470 100644 --- a/Reqnroll.VisualStudio/Snippets/SnippetService.cs +++ b/Reqnroll.VisualStudio/Snippets/SnippetService.cs @@ -14,9 +14,10 @@ public SnippetService(IProjectScope projectScope) public SnippetExpressionStyle DefaultExpressionStyle => _projectScope.GetDeveroomConfiguration().SnippetExpressionStyle; + public bool DefaultGenerateSkeletonMethodsAsAsync => _projectScope.GetDeveroomConfiguration().GenerateAsyncSkeletonMethods; public string GetStepDefinitionSkeletonSnippet(UndefinedStepDescriptor undefinedStep, - SnippetExpressionStyle expressionStyle, string indent = " ", string newLine = null) + SnippetExpressionStyle expressionStyle, bool generateAsyncSkeletonSnippet, string indent = " ", string newLine = null) { try { @@ -28,7 +29,7 @@ public string GetStepDefinitionSkeletonSnippet(UndefinedStepDescriptor undefined var configuration = _projectScope.GetDeveroomConfiguration(); newLine = newLine ?? Environment.NewLine; var result = - skeletonProvider.GetStepDefinitionSkeletonSnippet(undefinedStep, indent, newLine, + skeletonProvider.GetStepDefinitionSkeletonSnippet(undefinedStep, indent, generateAsyncSkeletonSnippet, newLine, configuration.BindingCulture); _logger.LogInfo( $"Step definition snippet generated for step '{undefinedStep.StepText}': {Environment.NewLine}{result}"); diff --git a/Reqnroll.VisualStudio/UI/ViewModels/CreateStepDefinitionsDialogViewModel.cs b/Reqnroll.VisualStudio/UI/ViewModels/CreateStepDefinitionsDialogViewModel.cs index f9ab5ac6..5785468a 100644 --- a/Reqnroll.VisualStudio/UI/ViewModels/CreateStepDefinitionsDialogViewModel.cs +++ b/Reqnroll.VisualStudio/UI/ViewModels/CreateStepDefinitionsDialogViewModel.cs @@ -1,17 +1,21 @@ #nullable disable +using Reqnroll.VisualStudio.Snippets; using System; using System.Linq; +using System.ComponentModel; +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; namespace Reqnroll.VisualStudio.UI.ViewModels; -public class CreateStepDefinitionsDialogViewModel +public class CreateStepDefinitionsDialogViewModel : INotifyPropertyChanged { #if DEBUG public static CreateStepDefinitionsDialogViewModel DesignData = new() { ClassName = "MyFeatureSteps", ExpressionStyle = SnippetExpressionStyle.CucumberExpression, - Items = new List + Items = new ObservableCollection { new() { @@ -33,6 +37,38 @@ public void GivenThereIsASimpleReqnrollProjectForVersion(Version reqnrollVersion { Snippet = @"[When(@""there is a simple Reqnroll project for (.*)"")] public void GivenThereIsASimpleReqnrollProjectForVersion(Version reqnrollVersion) +{ + throw new PendingStepException(); +}" + } + } + }; + public static CreateStepDefinitionsDialogViewModel DesignDataAsync = new() + { + ClassName = "MyFeatureSteps", + ExpressionStyle = SnippetExpressionStyle.CucumberExpression, + Items = new ObservableCollection + { + new() + { + Snippet = @"[Given(@""there is a simple Reqnroll project for (.*)"")] +public async Task GivenThereIsASimpleReqnrollProjectForVersionAsync(Version reqnrollVersion) +{ + throw new PendingStepException(); +}" + }, + new() + { + Snippet = @"[When(@""there is a simple Reqnroll project for (.*)"")] +public async Task GivenThereIsASimpleReqnrollProjectForVersionAsync(Version reqnrollVersion) +{ + throw new PendingStepException(); +}" + }, + new() + { + Snippet = @"[When(@""there is a simple Reqnroll project for (.*)"")] +public async Task GivenThereIsASimpleReqnrollProjectForVersionAsync(Version reqnrollVersion) { throw new PendingStepException(); }" @@ -40,8 +76,50 @@ public void GivenThereIsASimpleReqnrollProjectForVersion(Version reqnrollVersion } }; #endif + public string ClassName { get; set; } public SnippetExpressionStyle ExpressionStyle { get; set; } - public List Items { get; set; } = new(); + private bool _generateAsyncMethods = true; + public bool GenerateAsyncMethods + { + get => _generateAsyncMethods; + set + { + if (_generateAsyncMethods != value) + { + _generateAsyncMethods = value; + OnPropertyChanged(nameof(GenerateAsyncMethods)); + if (!IsInitializing) + { + RegenerateItems(); // Regenerate when property changes + } + } + } + } + public ObservableCollection Items { get; set; } = new(); public CreateStepDefinitionsDialogResult Result { get; set; } + public Func> Generator { get; set; } + public DeveroomTag[] UndefinedStepTags { get; set; } + public SnippetService SnippetService { get; set; } + public string Indent { get; set; } + public string NewLine { get; set; } + + public bool IsInitializing; + + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public void RegenerateItems() + { + Items.Clear(); + foreach (var item in Generator(this)) + { + Items.Add(item); + } + OnPropertyChanged(nameof(Items)); + } } diff --git a/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/DefineStepsCommand.feature b/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/DefineStepsCommand.feature index 2e3ebe70..cdb0b7bc 100644 --- a/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/DefineStepsCommand.feature +++ b/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/DefineStepsCommand.feature @@ -47,7 +47,7 @@ Scenario: Two undefined step has the same step definition skeleton And the project is built and the initial binding discovery is performed When I invoke the "Define Steps" command Then the define steps dialog should be opened with the following step definition skeletons - | type | expression | + | type | expression | | Given | the operand {int} has been entered | Scenario: All steps are defined @@ -121,11 +121,11 @@ Scenario: DefineSteps command abides by reqnroll.json configuration for regex sk "trace": { "stepDefinitionSkeletonStyle": "RegexAttribute" } } """ - And the project is built and the initial binding discovery is performed - When I invoke the "Define Steps" command - Then the define steps dialog should be opened with the following step definition skeletons - | type | expression | - | Given | the client added (.*) pcs to the basket | + And the project is built and the initial binding discovery is performed + When I invoke the "Define Steps" command + Then the define steps dialog should be opened with the following step definition skeletons + | type | expression | + | Given | the client added (.*) pcs to the basket | Scenario: DefineSteps command properly escapes empty brackets when using Cucumber expressions Given there is a Reqnroll project scope @@ -139,8 +139,8 @@ Scenario: DefineSteps command properly escapes empty brackets when using Cucumbe And the project is built and the initial binding discovery is performed When I invoke the "Define Steps" command Then the define steps dialog should be opened with the following step definition skeletons - | type | expression | - | When | I use \\(parenthesis), \\{curly braces} and\\/or \\\ backslash | + | type | expression | + | When | I use \\(parenthesis), \\{curly braces} and\\/or \\\\ backslash | Scenario: DefineSteps command properly escapes empty brackets when using Regex expressions Given there is a Reqnroll project scope @@ -160,6 +160,48 @@ Scenario: DefineSteps command properly escapes empty brackets when using Regex e And the project is built and the initial binding discovery is performed When I invoke the "Define Steps" command Then the define steps dialog should be opened with the following step definition skeletons - | type | expression | - | When | I use \\(parenthesis\), \\{curly braces}, \\\ backslash, and/or \\. period | + | type | expression | + | When | I use \\(parenthesis\\), \\{curly braces}, \\\\ backslash, and/or \\. period | + +Scenario: DefineSteps command abides by reqnroll.json configuration for async method declaration + Given there is a Reqnroll project scope + And the following feature file in the editor + """ + Feature: Feature Using Regex Style + + Scenario: Client has a simple basket + Given the client has a basket + """ + And the reqnroll.json configuration file contains + """ + { + "trace": { "generateStepDefinitionSkeletonAsAsync": true } + } + """ + And the project is built and the initial binding discovery is performed + When I invoke the "Define Steps" command + Then the define steps dialog should be opened with the following step definition skeletons + | Method | + | MyProject.StepDefinitions1.GivenTheClientHasABasketAsync | + +Scenario: DefineSteps command abides by reqnroll.json configuration for synchronous method declaration + Given there is a Reqnroll project scope + And the following feature file in the editor + """ + Feature: Feature Using Regex Style + + Scenario: Client has a simple basket + Given the client has a basket + """ + And the reqnroll.json configuration file contains + """ + { + "trace": { "generateStepDefinitionSkeletonAsAsync": false } + } + """ + And the project is built and the initial binding discovery is performed + When I invoke the "Define Steps" command + Then the define steps dialog should be opened with the following step definition skeletons + | Method | + | MyProject.StepDefinitions1.GivenTheClientHasABasket | diff --git a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs index 17bc2a84..67b71e42 100644 --- a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs +++ b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs @@ -797,7 +797,8 @@ private StepDefinitionSnippetData[] ParseSnippetsFromFile(string text, { Type = sd.Type, Regex = sd.Regex, - Expression = sd.Expression + Expression = sd.Expression, + Method = sd.Method }).ToArray(); } @@ -987,5 +988,6 @@ private class StepDefinitionSnippetData public string Type { get; set; } public string Regex { get; set; } public string Expression { get; set; } + public string Method { get; set; } } } diff --git a/Tests/Reqnroll.VisualStudio.UI.Tester/Reqnroll.VisualStudio.UI.Tester.csproj b/Tests/Reqnroll.VisualStudio.UI.Tester/Reqnroll.VisualStudio.UI.Tester.csproj index fe9fa4d3..e20ba396 100644 --- a/Tests/Reqnroll.VisualStudio.UI.Tester/Reqnroll.VisualStudio.UI.Tester.csproj +++ b/Tests/Reqnroll.VisualStudio.UI.Tester/Reqnroll.VisualStudio.UI.Tester.csproj @@ -12,4 +12,19 @@ + + + + True + True + Settings.settings + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + \ No newline at end of file diff --git a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs index 520dbf0f..d9925011 100644 --- a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs +++ b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs @@ -48,7 +48,25 @@ private void Test_ContextMenu_At100x100(object sender, RoutedEventArgs e) private void Test_GenerateStepDefinitions(object sender, RoutedEventArgs e) { - var viewModel = CreateStepDefinitionsDialogViewModel.DesignData; + var syncViewModel = CreateStepDefinitionsDialogViewModel.DesignData; + var viewModel = new CreateStepDefinitionsDialogViewModel() { + ClassName = syncViewModel.ClassName, + ExpressionStyle = syncViewModel.ExpressionStyle }; + viewModel.IsInitializing = true; + foreach (var item in syncViewModel.Items) + { + viewModel.Items.Add(item); + } + viewModel.GenerateAsyncMethods = false; + viewModel.Generator = (m) => + { + if (m.GenerateAsyncMethods) + return CreateStepDefinitionsDialogViewModel.DesignDataAsync.Items.ToList(); + else + return CreateStepDefinitionsDialogViewModel.DesignData.Items.ToList(); + }; + viewModel.IsInitializing = false; + var dialog = new CreateStepDefinitionsDialog(viewModel); dialog.ShowDialog();