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();
}