diff --git a/CHANGELOG.md b/CHANGELOG.md index 72271a8d..2f0d7331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,14 @@ ## Improvements: -* Improved handling of scope tag expressions, hook and scope errors (#150) -* Improved logging for binding discovery (#154) +* Support file scoped namespace declarations when generating code (#140) ## Bug fixes: -*Contributors of this release (in alphabetical order):* @clrudolphi, @gasparnagy +* Improved handling of scope tag expressions, hook and scope errors (#150) +* Improved logging for binding discovery (#154) + +*Contributors of this release (in alphabetical order):* @304NotModified, @clrudolphi, @gasparnagy # v2025.3.395 - 2025-12-17 diff --git a/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs b/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs new file mode 100644 index 00000000..48ccf0bb --- /dev/null +++ b/Reqnroll.VisualStudio/Configuration/CSharpCodeGenerationConfiguration.cs @@ -0,0 +1,18 @@ +namespace Reqnroll.VisualStudio.Configuration; + +public class CSharpCodeGenerationConfiguration +{ + /// + /// Specifies the namespace declaration style for generated C# code. + /// Uses file-scoped namespaces when set to "file_scoped", otherwise uses block-scoped namespaces. + /// + [EditorConfigSetting("csharp_style_namespace_declarations")] + public string? NamespaceDeclarationStyle { get; set; } = "block_scoped"; + + /// + /// Determines if file-scoped namespaces should be used based on the EditorConfig setting. + /// + public bool UseFileScopedNamespaces => + NamespaceDeclarationStyle != null && + NamespaceDeclarationStyle.StartsWith("file_scoped", StringComparison.OrdinalIgnoreCase); +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs index 507d804f..32e17710 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/DefineStepsCommand.cs @@ -4,13 +4,17 @@ namespace Reqnroll.VisualStudio.Editor.Commands; [Export(typeof(IDeveroomFeatureEditorCommand))] public class DefineStepsCommand : DeveroomEditorCommandBase, IDeveroomFeatureEditorCommand { + private readonly IEditorConfigOptionsProvider _editorConfigOptionsProvider; + [ImportingConstructor] public DefineStepsCommand( IIdeScope ideScope, IBufferTagAggregatorFactoryService aggregatorFactory, - IDeveroomTaggerProvider taggerProvider) + IDeveroomTaggerProvider taggerProvider, + IEditorConfigOptionsProvider editorConfigOptionsProvider) : base(ideScope, aggregatorFactory, taggerProvider) { + _editorConfigOptionsProvider = editorConfigOptionsProvider; } public override DeveroomEditorCommandTargetKey[] Targets => new[] @@ -94,7 +98,7 @@ public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetK } var combinedSnippet = string.Join(newLine, - viewModel.Items.Where(i => i.IsSelected).Select(i => i.Snippet.Indent(indent + indent))); + viewModel.Items.Where(i => i.IsSelected).Select(i => i.Snippet.Indent(indent))); MonitoringService.MonitorCommandDefineSteps(viewModel.Result, viewModel.Items.Count(i => i.IsSelected)); @@ -124,27 +128,28 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin if (IdeScope.FileSystem.Directory.Exists(stepDefinitionsFolder)) { targetFolder = stepDefinitionsFolder; - fileNamespace = fileNamespace + ".StepDefinitions"; + fileNamespace += ".StepDefinitions"; } - var projectTraits = projectScope.GetProjectSettings().ReqnrollProjectTraits; - var isSpecFlow = projectTraits.HasFlag(ReqnrollProjectTraits.LegacySpecFlow) || projectTraits.HasFlag(ReqnrollProjectTraits.SpecFlowCompatibility); - var libraryNameSpace = isSpecFlow ? "SpecFlow" : "Reqnroll"; - var template = "using System;" + newLine + - $"using {libraryNameSpace};" + newLine + - newLine + - $"namespace {fileNamespace}" + newLine + - "{" + newLine + - $"{indent}[Binding]" + newLine + - $"{indent}public class {className}" + newLine + - $"{indent}{{" + newLine + - combinedSnippet + - $"{indent}}}" + newLine + - "}" + newLine; + // Get C# code generation configuration from EditorConfig using target .cs file path + var targetFilePath = Path.Combine(targetFolder, className + ".cs"); + var csharpConfig = new CSharpCodeGenerationConfiguration(); + var editorConfigOptions = _editorConfigOptionsProvider.GetEditorConfigOptionsByPath(targetFilePath); + editorConfigOptions.UpdateFromEditorConfig(csharpConfig); + + var projectTraits = projectScope.GetProjectSettings().ReqnrollProjectTraits; + var generatedContent = GenerateStepDefinitionClass( + combinedSnippet, + className, + fileNamespace, + projectTraits, + csharpConfig, + indent, + newLine); var targetFile = FileDetails .FromPath(targetFolder, className + ".cs") - .WithCSharpContent(template); + .WithCSharpContent(generatedContent); if (IdeScope.FileSystem.File.Exists(targetFile.FullName)) if (IdeScope.Actions.ShowSyncQuestion("Overwrite file?", @@ -152,7 +157,7 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin defaultButton: MessageBoxResult.No) != MessageBoxResult.Yes) return; - projectScope.AddFile(targetFile, template); + projectScope.AddFile(targetFile, generatedContent); projectScope.IdeScope.Actions.NavigateTo(new SourceLocation(targetFile, 9, 1)); IDiscoveryService discoveryService = projectScope.GetDiscoveryService(); @@ -160,6 +165,67 @@ private void SaveAsStepDefinitionClass(IProjectScope projectScope, string combin () => RebuildBindingRegistry(discoveryService, targetFile), _ => { Finished.Set(); }); } + internal static string GenerateStepDefinitionClass( + string combinedSnippet, + string className, + string fileNamespace, + ReqnrollProjectTraits projectTraits, + CSharpCodeGenerationConfiguration csharpConfig, + string indent, + string newLine) + { + var isSpecFlow = projectTraits.HasFlag(ReqnrollProjectTraits.LegacySpecFlow) || + projectTraits.HasFlag(ReqnrollProjectTraits.SpecFlowCompatibility); + var libraryNameSpace = isSpecFlow ? "SpecFlow" : "Reqnroll"; + + // Estimate template size for StringBuilder capacity + var estimatedSize = 200 + fileNamespace.Length + className.Length + combinedSnippet.Length; + var template = new StringBuilder(estimatedSize); + template.AppendLine("using System;"); + template.AppendLine($"using {libraryNameSpace};"); + template.AppendLine(); + + // Determine indentation level based on namespace style + var classIndent = csharpConfig.UseFileScopedNamespaces ? "" : indent; + + // Add namespace declaration + if (csharpConfig.UseFileScopedNamespaces) + { + template.AppendLine($"namespace {fileNamespace};"); + template.AppendLine(); + } + else + { + template.AppendLine($"namespace {fileNamespace}"); + template.AppendLine("{"); + } + + // Add class declaration (common structure with appropriate indentation) + template.AppendLine($"{classIndent}[Binding]"); + template.AppendLine($"{classIndent}public class {className}"); + template.AppendLine($"{classIndent}{{"); + + // Add snippet with appropriate indentation based on namespace style + if (csharpConfig.UseFileScopedNamespaces) + { + template.AppendLine(combinedSnippet); + } + else + { + AppendLinesWithIndent(template, combinedSnippet, indent, newLine); + } + + template.AppendLine($"{classIndent}}}"); + + // Close namespace if block-scoped + if (!csharpConfig.UseFileScopedNamespaces) + { + template.AppendLine("}"); + } + + return template.ToString(); + } + private async Task RebuildBindingRegistry(IDiscoveryService discoveryService, CSharpStepDefinitionFile stepDefinitionFile) { @@ -168,4 +234,27 @@ await discoveryService.BindingRegistryCache Finished.Set(); } + + private static void AppendLinesWithIndent(StringBuilder builder, string content, string indent, string newLine) + { + if (string.IsNullOrEmpty(content)) + return; + + var lines = content.Split(new[] { newLine }, StringSplitOptions.None); + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i]; + + // Add indentation to non-empty lines + if (!string.IsNullOrWhiteSpace(line)) + { + builder.Append(indent).AppendLine(line); + } + else + { + builder.AppendLine(line); + } + } + } } diff --git a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs index bac62066..7d1fe9a2 100644 --- a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs +++ b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsExtensions.cs @@ -26,16 +26,16 @@ public static void UpdateFromEditorConfig(this IEditorConfigOptions edi .Select(p => new { PropertyInfo = p, - ((EditorConfigSettingAttribute) Attribute.GetCustomAttribute(p, typeof(EditorConfigSettingAttribute))) + EditorConfigKey = ((EditorConfigSettingAttribute) Attribute.GetCustomAttribute(p, typeof(EditorConfigSettingAttribute))) ?.EditorConfigSettingName }) - .Where(p => p.EditorConfigSettingName != null); + .Where(p => p.EditorConfigKey != null); foreach (var property in propertiesWithEditorConfig) { var currentValue = property.PropertyInfo.GetValue(config); var updatedValue = editorConfigOptions.GetOption(property.PropertyInfo.PropertyType, - property.EditorConfigSettingName, currentValue); + property.EditorConfigKey, currentValue); if (!Equals(currentValue, updatedValue)) property.PropertyInfo.SetValue(config, updatedValue); } diff --git a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs index 2045bdea..da66f035 100644 --- a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs +++ b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/EditorConfigOptionsProvider.cs @@ -26,6 +26,21 @@ public IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView) return new EditorConfigOptions(options); } + public IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + return NullEditorConfigOptions.Instance; + + var document = CreateAdHocDocumentByPath(filePath); + if (document == null) + return NullEditorConfigOptions.Instance; + + var options = + ThreadHelper.JoinableTaskFactory.Run(() => document.GetOptionsAsync()); + + return new EditorConfigOptions(options); + } + private Document GetDocument(IWpfTextView textView) => textView.TextBuffer.GetRelatedDocuments().FirstOrDefault() ?? CreateAdHocDocument(textView); @@ -35,10 +50,32 @@ private Document CreateAdHocDocument(IWpfTextView textView) var editorFilePath = GetPath(textView); if (editorFilePath == null) return null; - var project = _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault(); + return CreateAdHocDocumentByPath(editorFilePath); + } + + private Document CreateAdHocDocumentByPath(string filePath) + { + bool IsInProject(Project project) + { + if (project.FilePath == null) + return false; + var projectDir = Path.GetDirectoryName(project.FilePath); + if (projectDir == null) return false; + return Path.GetFullPath(filePath) + .StartsWith(projectDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); + } + + if (string.IsNullOrEmpty(filePath)) + return null; + + // We try to create the ad-hoc document in the project that contains (or would contain) the file, + // because otherwise the editorconfig options may not be correctly resolved. + var project = + _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault(IsInProject) ?? + _visualStudioWorkspace.CurrentSolution.Projects.FirstOrDefault(); if (project == null) return null; - return project.AddDocument(editorFilePath, string.Empty, filePath: editorFilePath); + return project.AddDocument(filePath, string.Empty, filePath: filePath); } public static string GetPath(IWpfTextView textView) diff --git a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs index 22ba4b0e..3d8e9964 100644 --- a/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs +++ b/Reqnroll.VisualStudio/Editor/Services/EditorConfig/IEditorConfigOptionsProvider.cs @@ -3,4 +3,5 @@ namespace Reqnroll.VisualStudio.Editor.Services.EditorConfig; public interface IEditorConfigOptionsProvider { IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView); + IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath); } diff --git a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs index 1f6687c1..c166b096 100644 --- a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs +++ b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs @@ -478,7 +478,8 @@ private void PerformCommand(string commandName, string parameter = null, } case "Define Steps": { - _invokedCommand = new DefineStepsCommand(_ideScope, aggregatorFactoryService, taggerProvider); + _invokedCommand = new DefineStepsCommand(_ideScope, aggregatorFactoryService, taggerProvider, + new StubEditorConfigOptionsProvider()); _invokedCommand.PreExec(_wpfTextView, _invokedCommand.Targets.First()); return; } diff --git a/Tests/Reqnroll.VisualStudio.Tests/Configuration/CSharpCodeGenerationConfigurationTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Configuration/CSharpCodeGenerationConfigurationTests.cs new file mode 100644 index 00000000..b29aeb2d --- /dev/null +++ b/Tests/Reqnroll.VisualStudio.Tests/Configuration/CSharpCodeGenerationConfigurationTests.cs @@ -0,0 +1,154 @@ +using Reqnroll.VisualStudio.Configuration; +using Reqnroll.VisualStudio.Editor.Services.EditorConfig; +using Xunit; + +namespace Reqnroll.VisualStudio.Tests.Configuration; + +public class CSharpCodeGenerationConfigurationTests +{ + [Fact] + public void UseFileScopedNamespaces_WhenFileScopedSet_ReturnsTrue() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration + { + NamespaceDeclarationStyle = "file_scoped" + }; + + // Act & Assert + Assert.True(config.UseFileScopedNamespaces); + } + + [Fact] + public void UseFileScopedNamespaces_WhenFileScopedWithSeveritySet_ReturnsTrue() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration + { + NamespaceDeclarationStyle = "file_scoped:warning" + }; + + // Act & Assert + Assert.True(config.UseFileScopedNamespaces); + } + + [Fact] + public void UseFileScopedNamespaces_WhenBlockScopedSet_ReturnsFalse() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration + { + NamespaceDeclarationStyle = "block_scoped" + }; + + // Act & Assert + Assert.False(config.UseFileScopedNamespaces); + } + + [Fact] + public void UseFileScopedNamespaces_WhenDefaultValue_ReturnsFalse() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration(); + + // Act & Assert + Assert.False(config.UseFileScopedNamespaces); + } + + [Fact] + public void UseFileScopedNamespaces_WhenNullValue_ReturnsFalse() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration + { + NamespaceDeclarationStyle = null + }; + + // Act & Assert + Assert.False(config.UseFileScopedNamespaces); + } + + [Fact] + public void UseFileScopedNamespaces_WhenUnknownValue_ReturnsFalse() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration + { + NamespaceDeclarationStyle = "unknown_style" + }; + + // Act & Assert + Assert.False(config.UseFileScopedNamespaces); + } + + [Fact] + public void UpdateFromEditorConfig_WhenFileScopedValue_SetsCorrectValue() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration(); + var editorConfigOptions = new TestEditorConfigOptions("file_scoped:silent"); + + // Act + editorConfigOptions.UpdateFromEditorConfig(config); + + // Assert + Assert.Equal("file_scoped:silent", config.NamespaceDeclarationStyle); + Assert.True(config.UseFileScopedNamespaces); + } + + [Fact] + public void UpdateFromEditorConfig_WhenBlockScopedValue_SetsCorrectValue() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration(); + var editorConfigOptions = new TestEditorConfigOptions("block_scoped"); + + // Act + editorConfigOptions.UpdateFromEditorConfig(config); + + // Assert + Assert.Equal("block_scoped", config.NamespaceDeclarationStyle); + Assert.False(config.UseFileScopedNamespaces); + } + + [Fact] + public void UpdateFromEditorConfig_WhenNoValue_KeepsDefault() + { + // Arrange + var config = new CSharpCodeGenerationConfiguration(); + var editorConfigOptions = new TestEditorConfigOptions(null); + + // Act + editorConfigOptions.UpdateFromEditorConfig(config); + + // Assert + Assert.Equal("block_scoped", config.NamespaceDeclarationStyle); // Should keep default + Assert.False(config.UseFileScopedNamespaces); + } +} + +// Test EditorConfig options provider that simulates reading specific values +public class TestEditorConfigOptions : IEditorConfigOptions +{ + private readonly string _namespaceStyle; + + public TestEditorConfigOptions(string namespaceStyle) + { + _namespaceStyle = namespaceStyle; + } + + public TResult GetOption(string editorConfigKey, TResult defaultValue) + { + if (editorConfigKey == "csharp_style_namespace_declarations" && _namespaceStyle != null) + { + return (TResult)(object)_namespaceStyle; + } + + return defaultValue; + } + + public bool GetBoolOption(string editorConfigKey, bool defaultValue) + { + return defaultValue; + } +} \ No newline at end of file diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt new file mode 100644 index 00000000..e49d0e45 --- /dev/null +++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_BlockScoped.approved.txt @@ -0,0 +1,15 @@ +using System; +using Reqnroll; + +namespace MyNamespace.MyProject +{ + [Binding] + public class Feature1StepDefinitions + { + [When(@"I press add")] + public void WhenIPressAdd() + { + throw new PendingStepException(); + } + } +} diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt new file mode 100644 index 00000000..5317920a --- /dev/null +++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.Reqnroll_FileScoped.approved.txt @@ -0,0 +1,14 @@ +using System; +using Reqnroll; + +namespace MyNamespace.MyProject; + +[Binding] +public class Feature1StepDefinitions +{ + [When(@"I press add")] + public void WhenIPressAdd() + { + throw new PendingStepException(); + } +} diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt new file mode 100644 index 00000000..426c1745 --- /dev/null +++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_BlockScoped.approved.txt @@ -0,0 +1,15 @@ +using System; +using SpecFlow; + +namespace MyNamespace.MyProject +{ + [Binding] + public class Feature1StepDefinitions + { + [When(@"I press add")] + public void WhenIPressAdd() + { + throw new PendingStepException(); + } + } +} diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt new file mode 100644 index 00000000..6420a851 --- /dev/null +++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/ApprovalTestData/DefineStepsCommandTests.GenerateStepDefinitionClass.ForScenario.SpecFlow_FileScoped.approved.txt @@ -0,0 +1,14 @@ +using System; +using SpecFlow; + +namespace MyNamespace.MyProject; + +[Binding] +public class Feature1StepDefinitions +{ + [When(@"I press add")] + public void WhenIPressAdd() + { + throw new PendingStepException(); + } +} diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs index fba8031c..f8ce1e46 100644 --- a/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs +++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/Commands/DefineStepsCommandTests.cs @@ -2,11 +2,14 @@ namespace Reqnroll.VisualStudio.Tests.Editor.Commands; +[UseReporter /*(typeof(VisualStudioReporter))*/] +[UseApprovalSubdirectory("../ApprovalTestData")] public class DefineStepsCommandTests : CommandTestBase { public DefineStepsCommandTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, (ps, tp) => - new DefineStepsCommand(ps.IdeScope, new StubBufferTagAggregatorFactoryService(tp), tp), + new DefineStepsCommand(ps.IdeScope, new StubBufferTagAggregatorFactoryService(tp), tp, + new StubEditorConfigOptionsProvider()), "ShowProblem: User Notification: ") { } @@ -67,4 +70,79 @@ public async Task Step_definition_class_saved(string _, string expression) await BindingRegistryIsModified(expression); } + + [Theory] + [InlineData(ProjectType.Reqnroll, NamespaceStyle.BlockScoped)] + [InlineData(ProjectType.Reqnroll, NamespaceStyle.FileScoped)] + [InlineData(ProjectType.SpecFlow, NamespaceStyle.BlockScoped)] + [InlineData(ProjectType.SpecFlow, NamespaceStyle.FileScoped)] + public void GenerateStepDefinitionClass(ProjectType projectType, NamespaceStyle namespaceStyle) + { + // Arrange + // Snippet should have single indentation (4 spaces) which will be used for file-scoped namespaces + // and will have extra indentation added for block-scoped namespaces + var snippet = """ + [When(@"I press add")] + public void WhenIPressAdd() + { + throw new PendingStepException(); + } + """; + const string className = "Feature1StepDefinitions"; + const string @namespace = "MyNamespace.MyProject"; + + var projectTraits = GetProjectTraits(projectType); + var csharpConfig = new CSharpCodeGenerationConfiguration + { + NamespaceDeclarationStyle = GetNamespaceStyleValue(namespaceStyle) + }; + + // Act + var result = DefineStepsCommand.GenerateStepDefinitionClass( + snippet, className, @namespace, projectTraits, csharpConfig, " ", Environment.NewLine); + + // Assert + VerifyWithScenario(result, projectType, namespaceStyle); + } + + private static void VerifyWithScenario(string result, ProjectType projectType, NamespaceStyle namespaceStyle) + { + var scenarioName = $"{projectType}_{namespaceStyle}"; + using (ApprovalResults.ForScenario(scenarioName)) + { + Approvals.Verify(result); + } + } + + private static ReqnrollProjectTraits GetProjectTraits(ProjectType projectType) + { + return projectType switch + { + ProjectType.Reqnroll => ReqnrollProjectTraits.CucumberExpression | ReqnrollProjectTraits.MsBuildGeneration, + ProjectType.SpecFlow => ReqnrollProjectTraits.LegacySpecFlow | ReqnrollProjectTraits.CucumberExpression | ReqnrollProjectTraits.MsBuildGeneration, + _ => throw new ArgumentOutOfRangeException(nameof(projectType)) + }; + } + + private static string GetNamespaceStyleValue(NamespaceStyle namespaceStyle) + { + return namespaceStyle switch + { + NamespaceStyle.BlockScoped => "block_scoped", + NamespaceStyle.FileScoped => "file_scoped", + _ => throw new ArgumentOutOfRangeException(nameof(namespaceStyle)) + }; + } + + public enum ProjectType + { + Reqnroll, + SpecFlow + } + + public enum NamespaceStyle + { + BlockScoped, + FileScoped + } } diff --git a/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs b/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs index dd45a7b1..d24b4823 100644 --- a/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs +++ b/Tests/Reqnroll.VisualStudio.VsxStubs/StubEditorConfigOptionsProvider.cs @@ -3,4 +3,5 @@ namespace Reqnroll.VisualStudio.VsxStubs; public class StubEditorConfigOptionsProvider : IEditorConfigOptionsProvider { public IEditorConfigOptions GetEditorConfigOptions(IWpfTextView textView) => new NullEditorConfigOptions(); + public IEditorConfigOptions GetEditorConfigOptionsByPath(string filePath) => new NullEditorConfigOptions(); }