diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fcab1ee..ccf7cbe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # [vNext] * Support for .NET 8 projects +* New editor command: "Go To Hooks" (Ctrl B,H) to navigate to the hooks related to the scenario +* The "Go To Definition" lists hooks when invoked from scenario header (tags, header line, description) * Initial release based on v2022.1.91 of the [SpecFlow for Visual Studio](https://github.com/SpecFlowOSS/SpecFlow.VS/) extension. diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ConnectorResult.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ConnectorResult.cs index f11b322e..129955ef 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ConnectorResult.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ConnectorResult.cs @@ -2,6 +2,7 @@ namespace ReqnrollConnector.Discovery; public record ConnectorResult( ImmutableArray StepDefinitions, + ImmutableArray Hooks, ImmutableSortedDictionary SourceFiles, ImmutableSortedDictionary TypeNames, ImmutableSortedDictionary AnalyticsProperties, diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/DiscoveryResult.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/DiscoveryResult.cs index faa2aa47..c5fa73c2 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/DiscoveryResult.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/DiscoveryResult.cs @@ -2,6 +2,7 @@ namespace ReqnrollConnector.Discovery; public record DiscoveryResult( ImmutableArray StepDefinitions, + ImmutableArray Hooks, ImmutableSortedDictionary SourceFiles, ImmutableSortedDictionary TypeNames ); diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/ReqnrollDiscoverer.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/ReqnrollDiscoverer.cs index d7c17379..d627b0d2 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/ReqnrollDiscoverer.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/Discovery/ReqnrollDiscoverer.cs @@ -1,10 +1,14 @@ +using Reqnroll.VisualStudio.ReqnrollConnector.Models; +using StepDefinition = ReqnrollConnector.ReqnrollProxies.StepDefinition; + namespace ReqnrollConnector.Discovery; public class ReqnrollDiscoverer { private readonly IAnalyticsContainer _analytics; - private readonly ILogger _log; private readonly SymbolReaderCache _symbolReaders; + // ReSharper disable once NotAccessedField.Local + private readonly ILogger _log; public ReqnrollDiscoverer(ILogger log, IAnalyticsContainer analytics) { @@ -21,8 +25,10 @@ public ReqnrollDiscoverer(ILogger log, IAnalyticsContainer analytics) var typeNames = ImmutableSortedDictionary.CreateBuilder(); var sourcePaths = ImmutableSortedDictionary.CreateBuilder(); - var stepDefinitions = bindingRegistryFactory - .GetBindingRegistry(assemblyLoadContext, testAssembly, configFile) + var bindingRegistryAdapter = bindingRegistryFactory + .GetBindingRegistry(assemblyLoadContext, testAssembly, configFile); + + var stepDefinitions = bindingRegistryAdapter .GetStepDefinitions() .Select(sdb => CreateStepDefinition(sdb, sdb2 => GetParamTypes(sdb2.ParamTypes, parameterTypeName => GetKey(typeNames, parameterTypeName)), @@ -33,12 +39,24 @@ public ReqnrollDiscoverer(ILogger log, IAnalyticsContainer analytics) .OrderBy(sd => sd.SourceLocation) .ToImmutableArray(); + var hooks = bindingRegistryAdapter. + GetHooks() + .Select(sdb => CreateHook(sdb, + sourcePath => GetKey(sourcePaths, sourcePath), + assemblyLoadContext, + testAssembly) + ) + .OrderBy(sd => sd.SourceLocation) + .ToImmutableArray(); + + _analytics.AddAnalyticsProperty("TypeNames", typeNames.Count.ToString()); _analytics.AddAnalyticsProperty("SourcePaths", sourcePaths.Count.ToString()); _analytics.AddAnalyticsProperty("StepDefinitions", stepDefinitions.Length.ToString()); return new DiscoveryResult( stepDefinitions, + hooks, sourcePaths.ToImmutable(), typeNames.ToImmutable() ); @@ -52,18 +70,35 @@ public ReqnrollDiscoverer(ILogger log, IAnalyticsContainer analytics) var stepDefinition = new StepDefinition ( sdb.StepDefinitionType, - sdb.Regex.Map(r => r.ToString()).Reduce((string) null!), + sdb.Regex, sdb.Method.ToString()!, getParameterTypes(sdb), GetScope(sdb), GetSourceExpression(sdb), - GetErrorMessage(sdb), + sdb.Error, sourceLocation.Reduce((string) null!) ); return stepDefinition; } + private Hook CreateHook(HookBindingAdapter sdb, + Func getSourcePathId, + AssemblyLoadContext assemblyLoadContext, Assembly testAssembly) + { + var sourceLocation = GetSourceLocation(sdb.Method, getSourcePathId, assemblyLoadContext, testAssembly); + var stepDefinition = new Hook + { + Type = sdb.HookType, + HookOrder = sdb.HookOrder, + Method = sdb.Method.ToString(), + Scope = GetScope(sdb), + SourceLocation = sourceLocation.Reduce((string)null!) + }; + + return stepDefinition; + } + private string GetKey(ImmutableSortedDictionary.Builder dictionary, string value) { KeyValuePair found = dictionary @@ -93,23 +128,24 @@ private string GetParamType(string parameterTypeName, Func getKe return $"#{key}"; } - private static StepScope? GetScope(StepDefinitionBindingAdapter stepDefinitionBinding) + private static StepScope? GetScope(IScopedBindingAdapter scopedBinding) { - if (!stepDefinitionBinding.IsScoped) + if (!scopedBinding.IsScoped) return null; - return new StepScope( - stepDefinitionBinding.BindingScopeTag.Map(tag => $"@{tag}").Reduce((string) null!), - stepDefinitionBinding.BindingScopeFeatureTitle, - stepDefinitionBinding.BindingScopeScenarioTitle - ); + return new StepScope + { + Tag = scopedBinding.BindingScopeTag, + FeatureTitle = scopedBinding.BindingScopeFeatureTitle, + ScenarioTitle = scopedBinding.BindingScopeScenarioTitle + }; } private static string? GetSourceExpression(StepDefinitionBindingAdapter sdb) - => sdb.GetProperty("SourceExpression").Reduce(() => GetSpecifiedExpressionFromRegex(sdb)!); + => sdb.Expression ?? GetSpecifiedExpressionFromRegex(sdb); private static string? GetSpecifiedExpressionFromRegex(StepDefinitionBindingAdapter sdb) => - sdb.Regex + sdb.Regex? .Map(regex => regex.ToString()) .Map(regexString => { @@ -118,11 +154,7 @@ private string GetParamType(string parameterTypeName, Func getKe if (regexString.EndsWith("$")) regexString = regexString.Substring(0, regexString.Length - 1); return regexString; - }) - .Reduce((string) null!); - - private static string? GetErrorMessage(StepDefinitionBindingAdapter sdb) - => sdb.GetProperty("ErrorMessage").Reduce((string)null!); + }); private Option GetSourceLocation(BindingMethodAdapter bindingMethod, Func getSourcePathId, AssemblyLoadContext assemblyLoadContext, Assembly testAssembly) diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReflectionExecutor.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReflectionExecutor.cs index d0b84e4e..6fb3061b 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReflectionExecutor.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReflectionExecutor.cs @@ -33,18 +33,21 @@ public class ReflectionExecutor { return new ConnectorResult( discoveryResult.StepDefinitions, + discoveryResult.Hooks, discoveryResult.SourceFiles, discoveryResult.TypeNames, analyticsProperties, errorMessage); } return new ConnectorResult(ImmutableArray.Empty, + ImmutableArray.Empty, ImmutableSortedDictionary.Empty, ImmutableSortedDictionary.Empty, analytics.ToImmutable(), errorMessage != null ? $"{errorMessage}{Environment.NewLine}{log}" : log); }))) .Reduce(new ConnectorResult(ImmutableArray.Empty, + ImmutableArray.Empty, ImmutableSortedDictionary.Empty, ImmutableSortedDictionary.Empty, analytics.ToImmutable(), diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/BindingRegistryAdapter.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/BindingRegistryAdapter.cs index a3c2d4f1..29c00a72 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/BindingRegistryAdapter.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/BindingRegistryAdapter.cs @@ -8,4 +8,9 @@ public IEnumerable GetStepDefinitions() { return (Adaptee.StepDefinitions ?? Array.Empty()).Select(sd => new StepDefinitionBindingAdapter(sd)); } + + public IEnumerable GetHooks() + { + return (Adaptee.Hooks ?? Array.Empty()).Select(h => new HookBindingAdapter(h)); + } } \ No newline at end of file diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/BindingScopeData.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/BindingScopeData.cs index b8ec8cbd..c04e2854 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/BindingScopeData.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/BindingScopeData.cs @@ -3,7 +3,7 @@ namespace Reqnroll.Bindings.Provider.Data; public class BindingScopeData { - public string Tag { get; set; } + public string Tag { get; set; } // contains leading '@', e.g. '@mytag' public string FeatureTitle { get; set; } public string ScenarioTitle { get; set; } } diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/HookData.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/HookData.cs index f2b12189..c2fd72f4 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/HookData.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/Data/HookData.cs @@ -5,5 +5,5 @@ public class HookData public BindingSourceData Source { get; set; } public BindingScopeData Scope { get; set; } public string Type { get; set; } - public int HookOrder { get; set; } + public int? HookOrder { get; set; } } diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/HookBindingAdapter.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/HookBindingAdapter.cs new file mode 100644 index 00000000..1bba025c --- /dev/null +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/HookBindingAdapter.cs @@ -0,0 +1,14 @@ +using Reqnroll.Bindings.Provider.Data; + +namespace ReqnrollConnector.ReqnrollProxies; + +public record HookBindingAdapter(HookData Adaptee) : IScopedBindingAdapter +{ + public string HookType => Adaptee.Type; + public BindingMethodAdapter Method { get; } = new(Adaptee.Source?.Method); + public bool IsScoped => Adaptee.Scope != null; + public string? BindingScopeTag => Adaptee.Scope?.Tag; + public string? BindingScopeFeatureTitle => Adaptee.Scope?.FeatureTitle; + public string? BindingScopeScenarioTitle => Adaptee.Scope?.ScenarioTitle; + public int? HookOrder => Adaptee.HookOrder; +} \ No newline at end of file diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/IBindingRegistryAdapter.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/IBindingRegistryAdapter.cs index 973173ff..98ff9d91 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/IBindingRegistryAdapter.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/IBindingRegistryAdapter.cs @@ -3,4 +3,5 @@ namespace ReqnrollConnector.ReqnrollProxies; public interface IBindingRegistryAdapter { IEnumerable GetStepDefinitions(); + IEnumerable GetHooks(); } \ No newline at end of file diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinition.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinition.cs index 88cd9c5e..bf4e3ee9 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinition.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinition.cs @@ -1,3 +1,5 @@ +using Reqnroll.VisualStudio.ReqnrollConnector.Models; + namespace ReqnrollConnector.ReqnrollProxies; public record StepDefinition( diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinitionBindingAdapter.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinitionBindingAdapter.cs index d459156a..30f8b7e9 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinitionBindingAdapter.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepDefinitionBindingAdapter.cs @@ -2,14 +2,24 @@ namespace ReqnrollConnector.ReqnrollProxies; -public record StepDefinitionBindingAdapter(StepDefinitionData Adaptee) +public interface IScopedBindingAdapter +{ + bool IsScoped { get; } + string? BindingScopeTag { get; } + string? BindingScopeFeatureTitle { get; } + string? BindingScopeScenarioTitle { get; } +} + +public record StepDefinitionBindingAdapter(StepDefinitionData Adaptee) : IScopedBindingAdapter { public string StepDefinitionType => Adaptee.Type; public string[] ParamTypes => Adaptee.ParamTypes; - public Option Regex => Adaptee.Regex; + public string? Regex => Adaptee.Regex; + public string? Expression => Adaptee.Expression; + public string? Error => Adaptee.Error; public BindingMethodAdapter Method { get; } = new(Adaptee.Source?.Method); public bool IsScoped => Adaptee.Scope != null; - public Option BindingScopeTag => Adaptee.Scope?.Tag; + public string? BindingScopeTag => Adaptee.Scope?.Tag; public string? BindingScopeFeatureTitle => Adaptee.Scope?.FeatureTitle; public string? BindingScopeScenarioTitle => Adaptee.Scope?.ScenarioTitle; public virtual Option GetProperty(string propertyName) diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepScope.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepScope.cs deleted file mode 100644 index 58adee4d..00000000 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Generic/ReqnrollProxies/StepScope.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ReqnrollConnector.ReqnrollProxies; - -public record StepScope( - string? Tag, - string? FeatureTitle, - string? ScenarioTitle -); diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Models/DiscoveryResult.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Models/DiscoveryResult.cs index ee4135f4..59134185 100644 --- a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Models/DiscoveryResult.cs +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Models/DiscoveryResult.cs @@ -3,7 +3,8 @@ namespace Reqnroll.VisualStudio.ReqnrollConnector.Models; public class DiscoveryResult : ConnectorResult { - public StepDefinition[] StepDefinitions { get; set; } + public StepDefinition[] StepDefinitions { get; set; } = Array.Empty(); + public Hook[] Hooks { get; set; } = Array.Empty(); public Dictionary SourceFiles { get; set; } public Dictionary TypeNames { get; set; } } diff --git a/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Models/Hook.cs b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Models/Hook.cs new file mode 100644 index 00000000..d682489f --- /dev/null +++ b/Connectors/Reqnroll.VisualStudio.ReqnrollConnector.Models/Hook.cs @@ -0,0 +1,43 @@ +#nullable disable +namespace Reqnroll.VisualStudio.ReqnrollConnector.Models; + +public class Hook +{ + public string Type { get; set; } + public int? HookOrder { get; set; } + public string Method { get; set; } + //public string ParamTypes { get; set; } + public StepScope Scope { get; set; } + + public string SourceLocation { get; set; } + + #region Equality + + protected bool Equals(Hook other) + { + return Type == other.Type && HookOrder == other.HookOrder && Method == other.Method && Equals(Scope, other.Scope) && SourceLocation == other.SourceLocation; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Hook)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (Type != null ? Type.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ HookOrder.GetHashCode(); + hashCode = (hashCode * 397) ^ (Method != null ? Method.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Scope != null ? Scope.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (SourceLocation != null ? SourceLocation.GetHashCode() : 0); + return hashCode; + } + } + + #endregion +} diff --git a/Reqnroll.VisualStudio.Package/Commands/ReqnrollVsPackage.vsct b/Reqnroll.VisualStudio.Package/Commands/ReqnrollVsPackage.vsct index 8ecb0db2..bb6a0e5c 100644 --- a/Reqnroll.VisualStudio.Package/Commands/ReqnrollVsPackage.vsct +++ b/Reqnroll.VisualStudio.Package/Commands/ReqnrollVsPackage.vsct @@ -68,20 +68,20 @@ Creates step definition skeleton snippets for the undefined steps of the current feature file. - - - @@ -124,6 +124,7 @@ + @@ -137,8 +138,8 @@ - + diff --git a/Reqnroll.VisualStudio.Package/ProjectSystem/NullVsIdeScope.cs b/Reqnroll.VisualStudio.Package/ProjectSystem/NullVsIdeScope.cs index 8144ca5c..6577abd1 100644 --- a/Reqnroll.VisualStudio.Package/ProjectSystem/NullVsIdeScope.cs +++ b/Reqnroll.VisualStudio.Package/ProjectSystem/NullVsIdeScope.cs @@ -163,6 +163,10 @@ public void MonitorCommandGoToStepDefinition(bool generateSnippet) { } + public void MonitorCommandGoToHook() + { + } + public void MonitorCommandAutoFormatTable() { } diff --git a/Reqnroll.VisualStudio.UI/Icons.xaml b/Reqnroll.VisualStudio.UI/Icons.xaml index 548322dc..a9f3b222 100644 --- a/Reqnroll.VisualStudio.UI/Icons.xaml +++ b/Reqnroll.VisualStudio.UI/Icons.xaml @@ -23,6 +23,12 @@ + + + + diff --git a/Reqnroll.VisualStudio.sln.DotSettings b/Reqnroll.VisualStudio.sln.DotSettings index 55cf877f..346f4419 100644 --- a/Reqnroll.VisualStudio.sln.DotSettings +++ b/Reqnroll.VisualStudio.sln.DotSettings @@ -1,2 +1,3 @@  + ID True \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Discovery/BindingImporter.cs b/Reqnroll.VisualStudio/Discovery/BindingImporter.cs index d416710c..6b2e03b2 100644 --- a/Reqnroll.VisualStudio/Discovery/BindingImporter.cs +++ b/Reqnroll.VisualStudio/Discovery/BindingImporter.cs @@ -1,5 +1,6 @@ #nullable disable using Reqnroll.VisualStudio.Discovery.TagExpressions; +using Reqnroll.VisualStudio.ReqnrollConnector.Models; namespace Reqnroll.VisualStudio.Discovery; @@ -10,7 +11,7 @@ public class BindingImporter private static readonly string[] DoubleStringParameterTypes = {TypeShortcuts.StringType, TypeShortcuts.StringType}; private static readonly string[] SingleIntParameterTypes = {TypeShortcuts.Int32Type}; private static readonly string[] SingleDataTableParameterTypes = {TypeShortcuts.ReqnrollTableType}; - private readonly Dictionary _implementations = new(); + private readonly Dictionary _implementations = new(); private readonly IDeveroomLogger _logger; private readonly Dictionary _sourceFiles; @@ -29,8 +30,9 @@ public ProjectStepDefinitionBinding ImportStepDefinition(StepDefinition stepDefi { try { - var stepDefinitionType = (ScenarioBlock) Enum.Parse(typeof(ScenarioBlock), - stepDefinition.Type ?? ScenarioBlock.Unknown.ToString()); + var stepDefinitionType = Enum.TryParse(stepDefinition.Type, out var parsedHookType) + ? parsedHookType + : ScenarioBlock.Unknown; var regex = ParseRegex(stepDefinition); var sourceLocation = ParseSourceLocation(stepDefinition.SourceLocation); var scope = ParseScope(stepDefinition.Scope); @@ -39,7 +41,7 @@ public ProjectStepDefinitionBinding ImportStepDefinition(StepDefinition stepDefi if (!_implementations.TryGetValue(stepDefinition.Method, out var implementation)) { implementation = - new ProjectStepDefinitionImplementation(stepDefinition.Method, parameterTypes, sourceLocation); + new ProjectBindingImplementation(stepDefinition.Method, parameterTypes, sourceLocation); _implementations.Add(stepDefinition.Method, implementation); } @@ -48,7 +50,33 @@ public ProjectStepDefinitionBinding ImportStepDefinition(StepDefinition stepDefi } catch (Exception ex) { - _logger.LogWarning($"Invalid binding: {ex.Message}"); + _logger.LogWarning($"Invalid step definition binding: {ex.Message}"); + return null; + } + } + + public ProjectHookBinding ImportHook(Hook hook) + { + try + { + var hookType = Enum.TryParse(hook.Type, out var parsedHookType) + ? parsedHookType + : HookType.Unknown; + var sourceLocation = ParseSourceLocation(hook.SourceLocation); + var scope = ParseScope(hook.Scope); + + if (!_implementations.TryGetValue(hook.Method, out var implementation)) + { + implementation = + new ProjectBindingImplementation(hook.Method, null, sourceLocation); + _implementations.Add(hook.Method, implementation); + } + + return new ProjectHookBinding(implementation, scope, hookType, hook.HookOrder); + } + catch (Exception ex) + { + _logger.LogWarning($"Invalid hook binding: {ex.Message}"); return null; } } diff --git a/Reqnroll.VisualStudio/Discovery/DiscoveryInvoker.cs b/Reqnroll.VisualStudio/Discovery/DiscoveryInvoker.cs index a9b65ca6..ff74d45c 100644 --- a/Reqnroll.VisualStudio/Discovery/DiscoveryInvoker.cs +++ b/Reqnroll.VisualStudio/Discovery/DiscoveryInvoker.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Reqnroll.VisualStudio.Discovery; internal class DiscoveryInvoker @@ -37,12 +36,12 @@ public ProjectBindingRegistry InvokeDiscoveryWithTimer() .AndProjectIsReqnrollProject() .AndBindingSourceIsValid() .AndDiscoveryProviderSucceed(_discoveryResultProvider) - .ThenImportStepDefinitions(_projectScope.ProjectName) + .ThenImportBindings(_projectScope.ProjectName) .AndCreateBindingRegistry(_monitoringService); stopwatch.Stop(); _logger.LogVerbose( - $"{bindingRegistry.StepDefinitions.Length} step definitions discovered in {stopwatch.Elapsed}"); + $"{bindingRegistry.StepDefinitions.Length} step definitions and {bindingRegistry.Hooks.Length} hooks discovered in {stopwatch.Elapsed}"); return bindingRegistry; } @@ -54,6 +53,7 @@ private class Discovery : IDiscovery private DiscoveryResult _discoveryResult; private ProjectSettings _projectSettings; private ImmutableArray _stepDefinitions; + private ImmutableArray _hooks; private ConfigSource _testAssemblySource; public Discovery(IDeveroomLogger logger, IDeveroomErrorListServices errorListServices, @@ -124,7 +124,7 @@ public IDiscovery AndDiscoveryProviderSucceed(IDiscoveryResultProvider discovery return new FailedDiscovery(); } - public IDiscovery ThenImportStepDefinitions(string projectName) + public IDiscovery ThenImportBindings(string projectName) { var bindingImporter = new BindingImporter(_discoveryResult.SourceFiles, _discoveryResult.TypeNames, _logger); @@ -134,8 +134,13 @@ public IDiscovery ThenImportStepDefinitions(string projectName) .Where(psd => psd != null) .ToImmutableArray(); + _hooks = _discoveryResult.Hooks + .Select(sd => bindingImporter.ImportHook(sd)) + .Where(psd => psd != null) + .ToImmutableArray(); + _logger.LogInfo( - $"{_stepDefinitions.Length} step definitions discovered for project {projectName}"); + $"{_stepDefinitions.Length} step definitions and {_hooks.Length} hooks discovered for project {projectName}"); ReportInvalidStepDefinitions(); @@ -150,7 +155,7 @@ public ProjectBindingRegistry AndCreateBindingRegistry(IMonitoringService monito var projectHash = _invoker.CreateProjectHash(_projectSettings, _testAssemblySource); var bindingRegistry = - new ProjectBindingRegistry(_stepDefinitions, projectHash); + new ProjectBindingRegistry(_stepDefinitions, _hooks, projectHash); return bindingRegistry; } @@ -195,7 +200,7 @@ private class FailedDiscovery : IDiscovery public IDiscovery AndProjectIsReqnrollProject() => this; public IDiscovery AndBindingSourceIsValid() => this; public IDiscovery AndDiscoveryProviderSucceed(IDiscoveryResultProvider discoveryResultProvider) => this; - public IDiscovery ThenImportStepDefinitions(string projectName) => this; + public IDiscovery ThenImportBindings(string projectName) => this; public ProjectBindingRegistry AndCreateBindingRegistry(IMonitoringService monitoringService) => ProjectBindingRegistry.Invalid; @@ -206,7 +211,7 @@ private interface IDiscovery IDiscovery AndProjectIsReqnrollProject(); IDiscovery AndBindingSourceIsValid(); IDiscovery AndDiscoveryProviderSucceed(IDiscoveryResultProvider discoveryResultProvider); - IDiscovery ThenImportStepDefinitions(string projectName); + IDiscovery ThenImportBindings(string projectName); ProjectBindingRegistry AndCreateBindingRegistry(IMonitoringService monitoringService); } } diff --git a/Reqnroll.VisualStudio/Discovery/HookMatchResult.cs b/Reqnroll.VisualStudio/Discovery/HookMatchResult.cs new file mode 100644 index 00000000..a7acf3b6 --- /dev/null +++ b/Reqnroll.VisualStudio/Discovery/HookMatchResult.cs @@ -0,0 +1,12 @@ +namespace Reqnroll.VisualStudio.Discovery; +public class HookMatchResult +{ + public ProjectHookBinding[] Items { get; } + + public bool HasHooks => Items.Length > 0; + + public HookMatchResult(ProjectHookBinding[] items) + { + Items = items; + } +} diff --git a/Reqnroll.VisualStudio/Discovery/HookType.cs b/Reqnroll.VisualStudio/Discovery/HookType.cs new file mode 100644 index 00000000..c423e392 --- /dev/null +++ b/Reqnroll.VisualStudio/Discovery/HookType.cs @@ -0,0 +1,17 @@ +namespace Reqnroll.VisualStudio.Discovery; +public enum HookType +{ + Unknown = 0, + BeforeTestRun = 1, + BeforeTestThread = 2, + BeforeFeature = 3, + BeforeScenario = 4, + BeforeScenarioBlock = 5, + BeforeStep = 6, + AfterStep = 7, + AfterScenarioBlock = 8, + AfterScenario = 9, + AfterFeature = 10, + AfterTestThread = 11, + AfterTestRun = 12, +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Discovery/MatchResultItem.cs b/Reqnroll.VisualStudio/Discovery/MatchResultItem.cs index 1d894274..30492288 100644 --- a/Reqnroll.VisualStudio/Discovery/MatchResultItem.cs +++ b/Reqnroll.VisualStudio/Discovery/MatchResultItem.cs @@ -21,7 +21,7 @@ public UndefinedStepDescriptor(Step undefinedStep, string customStepText) public class MatchResultItem { - private static readonly string[] EmptyErrors = new string[0]; + private static readonly string[] EmptyErrors = Array.Empty(); private MatchResultItem(MatchResultType type, ProjectStepDefinitionBinding matchedStepDefinition, ParameterMatch parameterMatch, string[] errors, UndefinedStepDescriptor undefinedStep) diff --git a/Reqnroll.VisualStudio/Discovery/ProjectBinding.cs b/Reqnroll.VisualStudio/Discovery/ProjectBinding.cs new file mode 100644 index 00000000..d7135726 --- /dev/null +++ b/Reqnroll.VisualStudio/Discovery/ProjectBinding.cs @@ -0,0 +1,29 @@ +#nullable disable +namespace Reqnroll.VisualStudio.Discovery; + +public class ProjectBinding +{ + public Scope Scope { get; } + public ProjectBindingImplementation Implementation { get; } + + public ProjectBinding(ProjectBindingImplementation implementation, Scope scope) + { + Implementation = implementation; + Scope = scope; + } + + protected bool MatchScope(IGherkinDocumentContext context) + { + if (Scope != null) + { + if (Scope.Tag != null && !Scope.Tag.Evaluate(context.GetTagNames())) + return false; + if (Scope.FeatureTitle != null && context.AncestorOrSelfNode()?.Name != Scope.FeatureTitle) + return false; + if (Scope.ScenarioTitle != null && context.AncestorOrSelfNode()?.Name != Scope.ScenarioTitle) + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Discovery/ProjectStepDefinitionImplementation.cs b/Reqnroll.VisualStudio/Discovery/ProjectBindingImplementation.cs similarity index 54% rename from Reqnroll.VisualStudio/Discovery/ProjectStepDefinitionImplementation.cs rename to Reqnroll.VisualStudio/Discovery/ProjectBindingImplementation.cs index 048a2573..2dc69863 100644 --- a/Reqnroll.VisualStudio/Discovery/ProjectStepDefinitionImplementation.cs +++ b/Reqnroll.VisualStudio/Discovery/ProjectBindingImplementation.cs @@ -1,10 +1,10 @@ namespace Reqnroll.VisualStudio.Discovery; -public class ProjectStepDefinitionImplementation +public class ProjectBindingImplementation { - private static readonly string[] EmptyParameterTypes = new string[0]; + private static readonly string[] EmptyParameterTypes = Array.Empty(); - public ProjectStepDefinitionImplementation(string method, string[] parameterTypes, SourceLocation sourceLocation) + public ProjectBindingImplementation(string method, string[]? parameterTypes, SourceLocation sourceLocation) { Method = method; ParameterTypes = parameterTypes ?? EmptyParameterTypes; @@ -13,7 +13,7 @@ public ProjectStepDefinitionImplementation(string method, string[] parameterType public string Method { get; } //TODO: Name, URI, SourceType? public string[] ParameterTypes { get; } - public SourceLocation SourceLocation { get; } + public SourceLocation? SourceLocation { get; } public override string ToString() => Method; } diff --git a/Reqnroll.VisualStudio/Discovery/ProjectBindingRegistry.cs b/Reqnroll.VisualStudio/Discovery/ProjectBindingRegistry.cs index b0d5dfb1..d38ee9bf 100644 --- a/Reqnroll.VisualStudio/Discovery/ProjectBindingRegistry.cs +++ b/Reqnroll.VisualStudio/Discovery/ProjectBindingRegistry.cs @@ -5,17 +5,18 @@ public record ProjectBindingRegistry { private const string DataTableDefaultTypeName = TypeShortcuts.ReqnrollTableType; private const string DocStringDefaultTypeName = TypeShortcuts.StringType; - public static ProjectBindingRegistry Invalid = new(ImmutableArray.Empty); + public static ProjectBindingRegistry Invalid = new(ImmutableArray.Empty, ImmutableArray.Empty); private static int _versionCounter; - private ProjectBindingRegistry(IEnumerable stepDefinitions) + private ProjectBindingRegistry(IEnumerable stepDefinitions, IEnumerable hooks) { StepDefinitions = stepDefinitions.ToImmutableArray(); + Hooks = hooks.ToImmutableArray(); } - public ProjectBindingRegistry(IEnumerable stepDefinitions, int projectHash) - : this(stepDefinitions) + public ProjectBindingRegistry(IEnumerable stepDefinitions, IEnumerable hooks, int projectHash) + : this(stepDefinitions, hooks) { ProjectHash = projectHash; } @@ -25,9 +26,20 @@ public ProjectBindingRegistry(IEnumerable stepDefi public bool IsPatched => !ProjectHash.HasValue && this != Invalid; public ImmutableArray StepDefinitions { get; } + public ImmutableArray Hooks { get; } public override string ToString() => $"ProjectBindingRegistry_V{Version}_H{ProjectHash}"; + public HookMatchResult MatchScenarioToHooks(Scenario scenario, IGherkinDocumentContext context) + { + var hookMatches = Hooks + .Where(h => h.Match(scenario, context)) + .OrderBy(h => h.HookType) + .ThenBy(h => h.HookOrder) + .ToArray(); + + return new HookMatchResult(hookMatches); + } public MatchResult MatchStep(Step step, IGherkinDocumentContext context) { @@ -180,25 +192,25 @@ private MatchResultItem[] HandleScopeOverloads(MatchResultItem[] sdMatches) return sdMatches; } - public static ProjectBindingRegistry FromStepDefinitions( - IEnumerable projectStepDefinitionBindings) => new(projectStepDefinitionBindings); + public static ProjectBindingRegistry FromBindings( + IEnumerable projectStepDefinitionBindings, IEnumerable? hooks = null) => new(projectStepDefinitionBindings, hooks ?? Array.Empty()); public ProjectBindingRegistry WithStepDefinitions( IEnumerable projectStepDefinitionBindings) { var stepDefinitions = StepDefinitions.ToList(); stepDefinitions.AddRange(projectStepDefinitionBindings); - return new ProjectBindingRegistry(stepDefinitions); + return new ProjectBindingRegistry(stepDefinitions, Hooks); } public ProjectBindingRegistry ReplaceStepDefinition(ProjectStepDefinitionBinding original, ProjectStepDefinitionBinding replacement) { - return new ProjectBindingRegistry(StepDefinitions.Select(sd => sd == original ? replacement : sd)); + return new ProjectBindingRegistry(StepDefinitions.Select(sd => sd == original ? replacement : sd), Hooks); } public ProjectBindingRegistry Where(Func predicate) => - new(StepDefinitions.Where(predicate)); + new(StepDefinitions.Where(predicate), Hooks); public async Task ReplaceStepDefinitions(CSharpStepDefinitionFile stepDefinitionFile) { diff --git a/Reqnroll.VisualStudio/Discovery/ProjectHookBinding.cs b/Reqnroll.VisualStudio/Discovery/ProjectHookBinding.cs new file mode 100644 index 00000000..fc3ab57d --- /dev/null +++ b/Reqnroll.VisualStudio/Discovery/ProjectHookBinding.cs @@ -0,0 +1,28 @@ +#nullable disable +namespace Reqnroll.VisualStudio.Discovery; + +public class ProjectHookBinding : ProjectBinding +{ + public const int DefaultHookOrder = 10000; + + public HookType HookType { get; } + public int HookOrder { get; } + + public ProjectHookBinding(ProjectBindingImplementation implementation, Scope scope, HookType hookType, int? hookOrder) + : base(implementation, scope) + { + HookType = hookType; + HookOrder = hookOrder ?? DefaultHookOrder; + } + + public bool Match(Scenario scenario, IGherkinDocumentContext context) + { + if (!MatchScope(context)) + return false; + + return true; + } + + public override string ToString() => + Scope == null ? $"[{HookType}]: {Implementation}" : $"[{HookType}({Scope})]: {Implementation}"; +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Discovery/ProjectStepDefinitionBinding.cs b/Reqnroll.VisualStudio/Discovery/ProjectStepDefinitionBinding.cs index 727f05c7..2774b6b9 100644 --- a/Reqnroll.VisualStudio/Discovery/ProjectStepDefinitionBinding.cs +++ b/Reqnroll.VisualStudio/Discovery/ProjectStepDefinitionBinding.cs @@ -2,15 +2,14 @@ namespace Reqnroll.VisualStudio.Discovery; -public class ProjectStepDefinitionBinding +public class ProjectStepDefinitionBinding : ProjectBinding { public ProjectStepDefinitionBinding(ScenarioBlock stepDefinitionType, Regex regex, Scope scope, - ProjectStepDefinitionImplementation implementation, string specifiedExpression = null, string error = null) + ProjectBindingImplementation implementation, string specifiedExpression = null, string error = null) + : base(implementation, scope) { StepDefinitionType = stepDefinitionType; Regex = regex; - Scope = scope; - Implementation = implementation; SpecifiedExpression = specifiedExpression; Error = error; } @@ -20,8 +19,6 @@ public class ProjectStepDefinitionBinding public ScenarioBlock StepDefinitionType { get; } public string SpecifiedExpression { get; } public Regex Regex { get; } - public Scope Scope { get; } - public ProjectStepDefinitionImplementation Implementation { get; } public string Expression => SpecifiedExpression ?? GetSpecifiedExpressionFromRegex(); @@ -53,16 +50,8 @@ public MatchResultItem Match(Step step, IGherkinDocumentContext context, string if (!match.Success) return null; - //check scope - if (Scope != null) - { - if (Scope.Tag != null && !Scope.Tag.Evaluate(context.GetTagNames())) - return null; - if (Scope.FeatureTitle != null && context.AncestorOrSelfNode()?.Name != Scope.FeatureTitle) - return null; - if (Scope.ScenarioTitle != null && context.AncestorOrSelfNode()?.Name != Scope.ScenarioTitle) - return null; - } + if (!MatchScope(context)) + return null; var parameterMatch = MatchParameter(step, match); return MatchResultItem.CreateMatch(this, parameterMatch); diff --git a/Reqnroll.VisualStudio/Discovery/Scope.cs b/Reqnroll.VisualStudio/Discovery/Scope.cs index ffb10f86..cac8b351 100644 --- a/Reqnroll.VisualStudio/Discovery/Scope.cs +++ b/Reqnroll.VisualStudio/Discovery/Scope.cs @@ -1,11 +1,26 @@ -#nullable disable using Reqnroll.VisualStudio.Discovery.TagExpressions; namespace Reqnroll.VisualStudio.Discovery; public class Scope { - public ITagExpression Tag { get; set; } - public string FeatureTitle { get; set; } - public string ScenarioTitle { get; set; } + public ITagExpression? Tag { get; set; } + public string? FeatureTitle { get; set; } + public string? ScenarioTitle { get; set; } + + public override string ToString() + { + var result = Tag?.ToString() ?? ""; + if (FeatureTitle != null) + { + result = result.Length > 0 ? result + ", " : result; + result = $"{result}Feature='{FeatureTitle}'"; + } + if (ScenarioTitle != null) + { + result = result.Length > 0 ? result + ", " : result; + result = $"{result}Scenario='{ScenarioTitle}'"; + } + return result; + } } diff --git a/Reqnroll.VisualStudio/Discovery/StepDefinitionFileParser.cs b/Reqnroll.VisualStudio/Discovery/StepDefinitionFileParser.cs index 4faa1dc9..8e2dcdc4 100644 --- a/Reqnroll.VisualStudio/Discovery/StepDefinitionFileParser.cs +++ b/Reqnroll.VisualStudio/Discovery/StepDefinitionFileParser.cs @@ -33,7 +33,7 @@ public async Task> Parse(CSharpStepDefinition methodBodyEndPosition.Line + 1, methodBodyEndPosition.Character + 1); var implementation = - new ProjectStepDefinitionImplementation(FullMethodName(method), parameterTypes, sourceLocation); + new ProjectBindingImplementation(FullMethodName(method), parameterTypes, sourceLocation); foreach (var (attribute, token) in attributes) { diff --git a/Reqnroll.VisualStudio/Editor/Commands/GoToStepDefinitionCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/GoToDefinitionCommand.cs similarity index 70% rename from Reqnroll.VisualStudio/Editor/Commands/GoToStepDefinitionCommand.cs rename to Reqnroll.VisualStudio/Editor/Commands/GoToDefinitionCommand.cs index 02c49b27..aeee3bb3 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/GoToStepDefinitionCommand.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/GoToDefinitionCommand.cs @@ -3,12 +3,12 @@ namespace Reqnroll.VisualStudio.Editor.Commands; [Export(typeof(IDeveroomFeatureEditorCommand))] -public class GoToStepDefinitionCommand : DeveroomEditorCommandBase, IDeveroomFeatureEditorCommand +public class GoToDefinitionCommand : DeveroomEditorCommandBase, IDeveroomFeatureEditorCommand { - private const string PopupHeader = "Go to step definitions"; + private const string GoToStepDefinitionsPopupHeader = "Go to step definitions"; [ImportingConstructor] - public GoToStepDefinitionCommand( + public GoToDefinitionCommand( IIdeScope ideScope, IBufferTagAggregatorFactoryService aggregatorFactory, IDeveroomTaggerProvider taggerProvider) @@ -24,10 +24,9 @@ public class GoToStepDefinitionCommand : DeveroomEditorCommandBase, IDeveroomFea public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetKey commandKey, IntPtr inArgs = default) => InvokeCommand(textView); - internal bool InvokeCommand(IWpfTextView textView, - Action continueWithAfterJump = null) + internal bool InvokeCommand(IWpfTextView textView, Action continueWithAfterJump = null) { - Logger.LogVerbose("Go To Step Definition"); + Logger.LogVerbose("Go To Definition"); var textBuffer = textView.TextBuffer; @@ -46,12 +45,17 @@ public class GoToStepDefinitionCommand : DeveroomEditorCommandBase, IDeveroomFea else { Logger.LogVerbose($"Jump to list step: {matchResult}"); - IdeScope.Actions.ShowSyncContextMenu(PopupHeader, matchResult.Items.Select(m => + IdeScope.Actions.ShowSyncContextMenu(GoToStepDefinitionsPopupHeader, matchResult.Items.Select(m => new ContextMenuItem(m.ToString(), _ => { PerformGoToDefinition(m, textBuffer, continueWithAfterJump); }, GetIcon(m)) ).ToArray()); } } + else + { + var goToHookCommand = new GoToHooksCommand(IdeScope, AggregatorFactory, DeveroomTaggerProvider); + goToHookCommand.InvokeCommand(textView, continueWithAfterJump); + } return true; } @@ -67,32 +71,9 @@ public class GoToStepDefinitionCommand : DeveroomEditorCommandBase, IDeveroomFea break; case MatchResultType.Defined: case MatchResultType.Ambiguous: - PerformJump(match, continueWithAfterJump); - break; - } - } - - private void PerformJump(MatchResultItem match, Action continueWithAfterJump) - { - var sourceLocation = match.MatchedStepDefinition.Implementation.SourceLocation; - if (sourceLocation == null) - { - Logger.LogWarning($"Cannot jump to {match}: no source location"); - IdeScope.Actions.ShowProblem("Unable to jump to the step definition. No source location detected."); - return; - } - Logger.LogInfo($"Jumping to {match} at {sourceLocation}"); - if (IdeScope.Actions.NavigateTo(sourceLocation)) - { - continueWithAfterJump?.Invoke(match.MatchedStepDefinition); - } - else - { - Logger.LogWarning( - $"Cannot jump to {match}: invalid source file or position. Try to build the project to refresh positions."); - IdeScope.Actions.ShowProblem( - $"Unable to jump to the step definition. Invalid source file or file position.{Environment.NewLine}{sourceLocation}"); + PerformJump(match, match.MatchedStepDefinition, match.MatchedStepDefinition?.Implementation, continueWithAfterJump); + break; } } @@ -109,7 +90,7 @@ private void PerformOfferCopySnippet(MatchResultItem match, ITextBuffer textBuff var snippet = snippetService.GetStepDefinitionSkeletonSnippet(match.UndefinedStep, snippetService.DefaultExpressionStyle, indent, newLine); - IdeScope.Actions.ShowQuestion(new QuestionDescription(PopupHeader, + 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}", _ => PerformCopySnippet(snippet.Indent(indent + indent)))); } diff --git a/Reqnroll.VisualStudio/Editor/Commands/GoToHooksCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/GoToHooksCommand.cs new file mode 100644 index 00000000..eee57f0c --- /dev/null +++ b/Reqnroll.VisualStudio/Editor/Commands/GoToHooksCommand.cs @@ -0,0 +1,83 @@ +namespace Reqnroll.VisualStudio.Editor.Commands; + +[Export(typeof(IDeveroomFeatureEditorCommand))] +public class GoToHooksCommand : DeveroomEditorCommandBase, IDeveroomFeatureEditorCommand +{ + private const string GoToHooksPopupHeader = "Go to hooks"; + + [ImportingConstructor] + public GoToHooksCommand( + IIdeScope ideScope, + IBufferTagAggregatorFactoryService aggregatorFactory, + IDeveroomTaggerProvider taggerProvider) + : base(ideScope, aggregatorFactory, taggerProvider) + { + } + + public override DeveroomEditorCommandTargetKey[] Targets => new[] + { + new DeveroomEditorCommandTargetKey(ReqnrollVsCommands.DefaultCommandSet, + ReqnrollVsCommands.GoToHookCommandId) + }; + + public override DeveroomEditorCommandStatus QueryStatus(IWpfTextView textView, + DeveroomEditorCommandTargetKey commandKey) + { + var projectScope = IdeScope.GetProject(textView.TextBuffer); + var projectSettings = projectScope?.GetProjectSettings(); + if (projectSettings == null || !projectSettings.IsReqnrollProject || projectSettings.IsSpecFlowProject) + return DeveroomEditorCommandStatus.Disabled; + return base.QueryStatus(textView, commandKey); + } + + public override bool PreExec(IWpfTextView textView, DeveroomEditorCommandTargetKey commandKey, + IntPtr inArgs = default) => InvokeCommand(textView); + + internal bool InvokeCommand(IWpfTextView textView, Action? continueWithAfterJump = null) + { + Logger.LogVerbose("Go To Hook"); + + var hookReferenceTag = GetDeveroomTagForCaret(textView, DeveroomTagTypes.ScenarioHookReference); + + if (hookReferenceTag is { Data: HookMatchResult hookMatchResult }) + { + Logger.LogVerbose($"Linked to {hookMatchResult.Items.Length} hooks."); + IdeScope.Actions.ShowSyncContextMenu(GoToHooksPopupHeader, hookMatchResult.Items.Select(hook => + new ContextMenuItem(hook.ToString(), + _ => { PerformGoToHook(hook, continueWithAfterJump); }, GetIcon(hook)) + ).ToArray()); + } + + return true; + } + + private void PerformGoToHook(ProjectHookBinding hook, Action? continueWithAfterJump) + { + MonitoringService.MonitorCommandGoToHook(); + PerformJump(hook, hook, hook.Implementation, continueWithAfterJump); + } + + private string? GetIcon(ProjectHookBinding hook) + { + switch (hook.HookType) + { + case HookType.BeforeTestRun: + case HookType.BeforeTestThread: + case HookType.BeforeFeature: + case HookType.BeforeScenario: + case HookType.BeforeScenarioBlock: + case HookType.BeforeStep: + return "BeforeHook"; + case HookType.AfterTestRun: + case HookType.AfterTestThread: + case HookType.AfterFeature: + case HookType.AfterScenario: + case HookType.AfterScenarioBlock: + case HookType.AfterStep: + return "AfterHook"; + default: + return null; + } + } + +} diff --git a/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBase.cs b/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBase.cs index 76453400..c5b5fec6 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBase.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBase.cs @@ -95,6 +95,30 @@ protected DeveroomConfiguration GetConfiguration(IWpfTextView textView) return configuration; } + protected void PerformJump(object match, TResult result, ProjectBindingImplementation implementation, Action continueWithAfterJump) + { + var sourceLocation = implementation?.SourceLocation; + if (sourceLocation == null) + { + Logger.LogWarning($"Cannot jump to {match}: no source location"); + IdeScope.Actions.ShowProblem("Unable to jump to the step definition. No source location detected."); + return; + } + + Logger.LogInfo($"Jumping to {match} at {sourceLocation}"); + if (IdeScope.Actions.NavigateTo(sourceLocation)) + { + continueWithAfterJump?.Invoke(result); + } + else + { + Logger.LogWarning( + $"Cannot jump to {match}: invalid source file or position. Try to build the project to refresh positions."); + IdeScope.Actions.ShowProblem( + $"Unable to jump to the step definition. Invalid source file or file position.{Environment.NewLine}{sourceLocation}"); + } + } + #region Helper methods protected void SetSelectionToChangedLines(IWpfTextView textView, ITextSnapshotLine[] lines) diff --git a/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBroker.cs b/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBroker.cs index 34632ac0..a3ed810d 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBroker.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/Infrastructure/DeveroomEditorCommandBroker.cs @@ -13,7 +13,7 @@ public class DeveroomFeatureEditorCommandBroker : DeveroomEditorCommandBroker commands, IDeveroomLogger logger) : base(adaptersFactory, commands, logger) { - Debug.Assert(_commands.Count == 8, "There have to be 8 feature file editor Reqnroll commands"); + Debug.Assert(_commands.Count == 9, "There have to be 9 feature file editor Reqnroll commands"); } } diff --git a/Reqnroll.VisualStudio/Editor/Commands/RenameStepCommand.cs b/Reqnroll.VisualStudio/Editor/Commands/RenameStepCommand.cs index 13b3ed24..06bab628 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/RenameStepCommand.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/RenameStepCommand.cs @@ -37,11 +37,14 @@ public class RenameStepCommand : DeveroomEditorCommandBase, IDeveroomCodeEditorC if (textView.TextBuffer.ContentType.IsOfType(VsContentTypes.FeatureFile)) { var goToStepDefinitionCommand = - new GoToStepDefinitionCommand(IdeScope, AggregatorFactory, DeveroomTaggerProvider); - goToStepDefinitionCommand.InvokeCommand(textView, projectStepDefinitionBinding => + new GoToDefinitionCommand(IdeScope, AggregatorFactory, DeveroomTaggerProvider); + goToStepDefinitionCommand.InvokeCommand(textView, projectBinding => { + var sourceLocation = projectBinding.Implementation.SourceLocation; + var projectStepDefinitionBinding = projectBinding as ProjectStepDefinitionBinding; + if (projectStepDefinitionBinding == null || sourceLocation == null) + return; ctx.StepDefinitionBinding = projectStepDefinitionBinding; - var sourceLocation = projectStepDefinitionBinding.Implementation.SourceLocation; IdeScope.GetTextBuffer(sourceLocation, out var textBuffer); ctx.TextBufferOfStepDefinitionClass = textBuffer; var stepDefLine = diff --git a/Reqnroll.VisualStudio/Editor/Commands/ReqnrollVsCommands.cs b/Reqnroll.VisualStudio/Editor/Commands/ReqnrollVsCommands.cs index 08b7ee15..08e9358a 100644 --- a/Reqnroll.VisualStudio/Editor/Commands/ReqnrollVsCommands.cs +++ b/Reqnroll.VisualStudio/Editor/Commands/ReqnrollVsCommands.cs @@ -6,7 +6,7 @@ public static class ReqnrollVsCommands { public const int DefineStepsCommandId = 0x0100; public const int FindStepDefinitionUsagesCommandId = 0x0101; - public const int RegenerateAllFeatureFileCodeBehindCommandId = 0x0102; public const int RenameStepCommandId = 0x0103; + public const int GoToHookCommandId = 0x0104; public static readonly Guid DefaultCommandSet = new("7fd3ed5d-2cf1-4200-b28b-cf1cc6b00c5a"); } diff --git a/Reqnroll.VisualStudio/Editor/Services/DeveroomTagParser.cs b/Reqnroll.VisualStudio/Editor/Services/DeveroomTagParser.cs index b7fe3c84..ca8f79c5 100644 --- a/Reqnroll.VisualStudio/Editor/Services/DeveroomTagParser.cs +++ b/Reqnroll.VisualStudio/Editor/Services/DeveroomTagParser.cs @@ -206,6 +206,24 @@ public IReadOnlyCollection Parse(ITextSnapshot fileSnapshot) TagRowCells(fileSnapshot, scenarioOutlineExample.TableHeader, examplesBlockTag, DeveroomTagTypes.ScenarioOutlinePlaceholder); } + + if (scenarioDefinition is Scenario scenario && bindingRegistry != ProjectBindingRegistry.Invalid) + { + var match = bindingRegistry.MatchScenarioToHooks(scenario, scenarioDefinitionTag); + if (match.HasHooks) + { + var firstTagTag = scenarioDefinitionTag + .GetDescendantsOfType(DeveroomTagTypes.Tag) + .OrderBy(t => t.Span.Start) + .FirstOrDefault(); + + var startTag = firstTagTag ?? scenarioDefinitionTag; + var span = new SnapshotSpan(startTag.Span.Start, scenarioDefinitionTag.Span.End); + + var hookReferenceTag = new DeveroomTag(DeveroomTagTypes.ScenarioHookReference, span, match); + scenarioDefinitionTag.AddChild(hookReferenceTag); + } + } } private void TagRowCells(ITextSnapshot fileSnapshot, TableRow row, DeveroomTag parentTag, string tagType) diff --git a/Reqnroll.VisualStudio/Editor/Services/DeveroomTagTypes.cs b/Reqnroll.VisualStudio/Editor/Services/DeveroomTagTypes.cs index 0dd60541..458ad6fb 100644 --- a/Reqnroll.VisualStudio/Editor/Services/DeveroomTagTypes.cs +++ b/Reqnroll.VisualStudio/Editor/Services/DeveroomTagTypes.cs @@ -8,6 +8,7 @@ public static class DeveroomTagTypes public const string FeatureBlock = nameof(FeatureBlock); public const string RuleBlock = nameof(RuleBlock); public const string ScenarioDefinitionBlock = nameof(ScenarioDefinitionBlock); + public const string ScenarioHookReference = nameof(ScenarioHookReference); public const string StepBlock = nameof(StepBlock); public const string ExamplesBlock = nameof(ExamplesBlock); public const string StepKeyword = nameof(StepKeyword); diff --git a/Reqnroll.VisualStudio/Editor/Services/StepDefinitionUsageFinder.cs b/Reqnroll.VisualStudio/Editor/Services/StepDefinitionUsageFinder.cs index 24f0df16..9c124d35 100644 --- a/Reqnroll.VisualStudio/Editor/Services/StepDefinitionUsageFinder.cs +++ b/Reqnroll.VisualStudio/Editor/Services/StepDefinitionUsageFinder.cs @@ -94,7 +94,7 @@ private bool LoadAlreadyOpenedContent(string featureFilePath, out string content if (featureNode == null) yield break; - var dummyRegistry = ProjectBindingRegistry.FromStepDefinitions(stepDefinitions); + var dummyRegistry = ProjectBindingRegistry.FromBindings(stepDefinitions); var featureContext = new UsageFinderContext(featureNode); diff --git a/Reqnroll.VisualStudio/Monitoring/IMonitoringService.cs b/Reqnroll.VisualStudio/Monitoring/IMonitoringService.cs index f71ba83f..647cd489 100644 --- a/Reqnroll.VisualStudio/Monitoring/IMonitoringService.cs +++ b/Reqnroll.VisualStudio/Monitoring/IMonitoringService.cs @@ -18,6 +18,7 @@ public interface IMonitoringService void MonitorCommandDefineSteps(CreateStepDefinitionsDialogResult action, int snippetCount); void MonitorCommandFindStepDefinitionUsages(int usagesCount, bool isCancelled); void MonitorCommandGoToStepDefinition(bool generateSnippet); + void MonitorCommandGoToHook(); void MonitorCommandAutoFormatTable(); void MonitorCommandAutoFormatDocument(bool isSelectionFormatting); void MonitorCommandAddFeatureFile(ProjectSettings projectSettings); diff --git a/Reqnroll.VisualStudio/Monitoring/MonitoringService.cs b/Reqnroll.VisualStudio/Monitoring/MonitoringService.cs index bcc1a28b..4b65a6f6 100644 --- a/Reqnroll.VisualStudio/Monitoring/MonitoringService.cs +++ b/Reqnroll.VisualStudio/Monitoring/MonitoringService.cs @@ -113,6 +113,11 @@ public void MonitorCommandGoToStepDefinition(bool generateSnippet) })); } + public void MonitorCommandGoToHook() + { + _analyticsTransmitter.TransmitEvent(new GenericEvent("GoToHook command executed")); + } + public void MonitorCommandAutoFormatTable() { //TODO: re-enable tracking for real command based triggering (not by character type) diff --git a/Reqnroll.VisualStudio/ProjectSystem/IIdeScope.cs b/Reqnroll.VisualStudio/ProjectSystem/IIdeScope.cs index 88b270bc..125dc961 100644 --- a/Reqnroll.VisualStudio/ProjectSystem/IIdeScope.cs +++ b/Reqnroll.VisualStudio/ProjectSystem/IIdeScope.cs @@ -10,7 +10,7 @@ public interface IIdeScope IDeveroomOutputPaneServices DeveroomOutputPaneServices { get; } IDeveroomErrorListServices DeveroomErrorListServices { get; } IFileSystem FileSystem { get; } - IProjectScope GetProject(ITextBuffer textBuffer); + IProjectScope? GetProject(ITextBuffer textBuffer); event EventHandler WeakProjectsBuilt; event EventHandler WeakProjectOutputsUpdated; diff --git a/Reqnroll.VisualStudio/UI/ViewModels/RenameStepViewModel.cs b/Reqnroll.VisualStudio/UI/ViewModels/RenameStepViewModel.cs index 30e64995..26b5f5c4 100644 --- a/Reqnroll.VisualStudio/UI/ViewModels/RenameStepViewModel.cs +++ b/Reqnroll.VisualStudio/UI/ViewModels/RenameStepViewModel.cs @@ -66,7 +66,7 @@ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName ScenarioBlock.Given, new Regex("^invalid$"), new Scope(), - new ProjectStepDefinitionImplementation( + new ProjectBindingImplementation( "WhenIPressAdd", Array.Empty(), new SourceLocation("Steps.cs", 10, 9)) diff --git a/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ApprovalTestData/GeneratedProjectTests/DS_1.0.0-pre_nunit_nprj_net6.0_bt_992117478.approved.txt b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ApprovalTestData/GeneratedProjectTests/DS_1.0.0-pre_nunit_nprj_net6.0_bt_992117478.approved.txt index a9c242bd..7f5b2d14 100644 --- a/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ApprovalTestData/GeneratedProjectTests/DS_1.0.0-pre_nunit_nprj_net6.0_bt_992117478.approved.txt +++ b/Tests/Connector/Reqnroll.VisualStudio.ReqnrollConnector.Tests/ApprovalTestData/GeneratedProjectTests/DS_1.0.0-pre_nunit_nprj_net6.0_bt_992117478.approved.txt @@ -1,5 +1,6 @@ { "stepDefinitions": [], + "hooks": [], "sourceFiles": {}, "typeNames": {}, "analyticsProperties": { diff --git a/Tests/Reqnroll.SampleProjectGenerator.Core/GeneratorOptions.cs b/Tests/Reqnroll.SampleProjectGenerator.Core/GeneratorOptions.cs index 457503a0..f936d0d4 100644 --- a/Tests/Reqnroll.SampleProjectGenerator.Core/GeneratorOptions.cs +++ b/Tests/Reqnroll.SampleProjectGenerator.Core/GeneratorOptions.cs @@ -55,6 +55,7 @@ public class GeneratorOptions public bool AddUnicodeBinding { get; set; } public bool AddAsyncStep { get; set; } + public bool AddBeforeScenarioHook { get; set; } public bool IsBuilt { get; set; } @@ -128,6 +129,8 @@ private string GetOptionsId() result.Append("unic_"); if (AddAsyncStep) result.Append("async_"); + if (AddBeforeScenarioHook) + result.Append("hbs_"); if (IsBuilt) result.Append("bt_"); diff --git a/Tests/Reqnroll.SampleProjectGenerator.Core/ProjectGenerator.cs b/Tests/Reqnroll.SampleProjectGenerator.Core/ProjectGenerator.cs index 37e0ceab..dae9e262 100644 --- a/Tests/Reqnroll.SampleProjectGenerator.Core/ProjectGenerator.cs +++ b/Tests/Reqnroll.SampleProjectGenerator.Core/ProjectGenerator.cs @@ -151,6 +151,8 @@ private void GenerateTestArtifacts(string projectFilePath) assetGenerator.AddUnicodeSteps(); if (_options.AddAsyncStep) assetGenerator.AddAsyncStep(); + if (_options.AddBeforeScenarioHook) + assetGenerator.AddBeforeScenarioHook(); var projectChanger = CreateProjectChanger(projectFilePath); for (int i = 0; i < _options.FeatureFileCount; i++) { diff --git a/Tests/Reqnroll.SampleProjectGenerator.Core/ReqnrollAssetGenerator.cs b/Tests/Reqnroll.SampleProjectGenerator.Core/ReqnrollAssetGenerator.cs index d9d9035f..3e92db26 100644 --- a/Tests/Reqnroll.SampleProjectGenerator.Core/ReqnrollAssetGenerator.cs +++ b/Tests/Reqnroll.SampleProjectGenerator.Core/ReqnrollAssetGenerator.cs @@ -14,18 +14,20 @@ public class ReqnrollAssetGenerator {"DocString", "string"} }; - private readonly Dictionary stepDefinitions; + private readonly Dictionary _stepDefinitions; + private readonly List _hooks; private StepDef _unicodeStep; public ReqnrollAssetGenerator(int stepDefinitionCount) { - stepDefinitions = + _stepDefinitions = GetStepDefinitionList(stepDefinitionCount) .GroupBy(sd => sd.Keyword) .ToDictionary(g => g.Key, g => g.ToArray()); + _hooks = new List(); } - public int StepDefCount => stepDefinitions.Sum(g => g.Value.Length); + public int StepDefCount => _stepDefinitions.Sum(g => g.Value.Length); public int StepCount { get; private set; } private IEnumerable GetStepDefinitionList(int stepDefinitionCount) @@ -63,10 +65,10 @@ public void AddUnicodeSteps() Regex = GeneratorOptions.UnicodeBindingRegex, StepTextParams = new List() }; - if (stepDefinitions.ContainsKey("Given")) - stepDefinitions["Given"] = stepDefinitions["Given"].Concat(new[] {_unicodeStep}).ToArray(); + if (_stepDefinitions.ContainsKey("Given")) + _stepDefinitions["Given"] = _stepDefinitions["Given"].Concat(new[] {_unicodeStep}).ToArray(); else - stepDefinitions["Given"] = new[] {_unicodeStep}; + _stepDefinitions["Given"] = new[] {_unicodeStep}; } private string GetKeyword(int stepDefCount) @@ -197,7 +199,7 @@ private void AddStep(StringBuilder content, string keyword, string keywordType = StepCount++; keywordType = keywordType ?? keyword; - if (!stepDefinitions.TryGetValue(keywordType, out var sdList)) + if (!_stepDefinitions.TryGetValue(keywordType, out var sdList)) throw new Exception("keyword not found: " + keywordType); var stepDef = sdList[LoremIpsum.Rnd.Next(sdList.Length)]; @@ -244,7 +246,7 @@ private static void AppendTable(StringBuilder content, int cellCount, string[] h public List GenerateStepDefClasses(string targetFolder, int stepDefPerClassCount) { - var sdList = LoremIpsum.Randomize(stepDefinitions.SelectMany(g => g.Value)); + var sdList = LoremIpsum.Randomize(_stepDefinitions.SelectMany(g => g.Value)); int startIndex = 0; var result = new List(); @@ -254,22 +256,28 @@ public List GenerateStepDefClasses(string targetFolder, int stepDefPerCl { result.Add( GenerateStepDefClass(targetFolder, - sdList.Skip(startIndex).Take(Math.Min(stepDefPerClassCount, sdList.Length - startIndex)))); + sdList.Skip(startIndex).Take(Math.Min(stepDefPerClassCount, sdList.Length - startIndex)), + Enumerable.Empty())); startIndex += stepDefPerClassCount; } + if (_hooks.Count > 0) + { + result.Add(GenerateStepDefClass(targetFolder, Enumerable.Empty(), _hooks)); + } + return result; } - private string GenerateStepDefClass(string folder, IEnumerable stepDefs) + private string GenerateStepDefClass(string folder, IEnumerable stepDefs, IEnumerable hookDefs) { var className = ToPascalCase(LoremIpsum.GetShortText()) + "Steps"; var filePath = Path.Combine(folder, className + ".cs"); - File.WriteAllText(filePath, GenerateStepDefClassContent(stepDefs, className)); + File.WriteAllText(filePath, GenerateStepDefClassContent(stepDefs, hookDefs, className)); return filePath; } - private string GenerateStepDefClassContent(IEnumerable stepDefs, string className) + private string GenerateStepDefClassContent(IEnumerable stepDefs, IEnumerable hookDefs, string className) { var content = new StringBuilder(); content.AppendLine("using System;"); @@ -295,6 +303,21 @@ private string GenerateStepDefClassContent(IEnumerable stepDefs, string content.AppendLine(); } + foreach (var hookDef in hookDefs) + { + var asyncPrefix = hookDef.Async ? "async " : ""; + content.AppendLine($" [{hookDef.HookType}()]"); + content.AppendLine( + $" public {asyncPrefix}void {hookDef.MethodName ?? ToPascalCase(LoremIpsum.GetShortText())}()"); + content.AppendLine(" {"); + content.AppendLine( + $" AutomationStub.DoHook();"); + if (hookDef.Async) + content.AppendLine(" await System.Threading.Tasks.Task.Delay(200);"); + content.AppendLine(" }"); + content.AppendLine(); + } + content.AppendLine(" }"); content.AppendLine("}"); return content.ToString(); @@ -314,10 +337,10 @@ public void AddAsyncStep() StepTextParams = new List(), Async = true }; - if (stepDefinitions.ContainsKey("When")) - stepDefinitions["When"] = stepDefinitions["When"].Concat(new[] {stepDef}).ToArray(); + if (_stepDefinitions.ContainsKey("When")) + _stepDefinitions["When"] = _stepDefinitions["When"].Concat(new[] {stepDef}).ToArray(); else - stepDefinitions["When"] = new[] {stepDef}; + _stepDefinitions["When"] = new[] {stepDef}; } private class StepDef @@ -342,4 +365,20 @@ public IEnumerable Params } } } + + private class HookDef + { + public bool Async { get; set; } + public string HookType { get; set; } + public string MethodName { get; set; } + } + + public void AddBeforeScenarioHook() + { + _hooks.Add(new HookDef + { + HookType = "BeforeScenario", + MethodName = "BeforeScenarioHook" + }); + } } diff --git a/Tests/Reqnroll.SampleProjectGenerator.Core/Templates/CS-NEW/AutomationStub.cs.txt b/Tests/Reqnroll.SampleProjectGenerator.Core/Templates/CS-NEW/AutomationStub.cs.txt index ec65cc04..6178bcef 100644 --- a/Tests/Reqnroll.SampleProjectGenerator.Core/Templates/CS-NEW/AutomationStub.cs.txt +++ b/Tests/Reqnroll.SampleProjectGenerator.Core/Templates/CS-NEW/AutomationStub.cs.txt @@ -8,5 +8,10 @@ namespace DeveroomSample.StepDefinitions { Console.WriteLine("executing step..."); } + + public static void DoHook(params object[] stepArgs) + { + Console.WriteLine("executing hook..."); + } } } diff --git a/Tests/Reqnroll.VisualStudio.Specs/Features/Discovery/BindingDiscovery.feature b/Tests/Reqnroll.VisualStudio.Specs/Features/Discovery/BindingDiscovery.feature index e6fce5a1..1be09719 100644 --- a/Tests/Reqnroll.VisualStudio.Specs/Features/Discovery/BindingDiscovery.feature +++ b/Tests/Reqnroll.VisualStudio.Specs/Features/Discovery/BindingDiscovery.feature @@ -19,3 +19,10 @@ Scenario: Discover bindings from Reqnroll project with external bindings When the binding discovery performed Then the discovery succeeds with several step definitions And there is a "Then" step with regex "there should be a step from an external assembly" + +Scenario: Discover hooks from a Reqnroll project + Given there is a small Reqnroll project with hooks + And the project is built + When the binding discovery performed + Then the discovery succeeds with hooks + diff --git a/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToStepDefinitionCommand.feature b/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToDefinitionCommand.feature similarity index 68% rename from Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToStepDefinitionCommand.feature rename to Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToDefinitionCommand.feature index e6a3904e..e03169a4 100644 --- a/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToStepDefinitionCommand.feature +++ b/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToDefinitionCommand.feature @@ -1,14 +1,6 @@ -Feature: Go to step definition command +Feature: Go to definition command -Rules: - -* Jumps to step definition if defined - * Jumps to the step definition - * Lists step definitions if multiple step definitions matching (e.g. scenario outline) -* Do not do anything if cursor is not standing on a step - * Cursor stands in a scenario header line -* Offers copying step definition skeleton to clipboard if undefined - * Navigate from an undefined step +Rule: Jumps to step definition if defined Scenario: Jumps to the step definition Given there is a Reqnroll project scope with calculator step definitions @@ -50,6 +42,32 @@ Scenario: Lists step definitions if multiple step definitions matching | I press multiply | And invoking the first item from the jump list navigates to the "I press add" "When" step definition +Rule: Jumps to hooks when invoked from scenario header + +Scenario: Lists hooks related to the scenario + Given there is a Reqnroll project scope + And the following hooks in the project: + | type | method | + | BeforeScenario | ResetDatabase | + | AfterScenario | SaveLogs | + When the following feature file is opened in the editor + """ + Feature: Addition + + @login + Scenario: Add two{caret} numbers + When I press add + """ + And the project is built and the initial binding discovery is performed + When I invoke the "Go To Definition" command + Then a jump list "Go to hooks" is opened with the following items + | hook | hook type | + | ResetDatabase | BeforeScenario | + | SaveLogs | AfterScenario | + And invoking the first item from the jump list navigates to the "ResetDatabase" hook + +Rule: Do not do anything if cursor is not standing on a step + Scenario: Cursor stands in a scenario header line Given there is a Reqnroll project scope with calculator step definitions And the following feature file in the editor @@ -63,6 +81,8 @@ Scenario: Cursor stands in a scenario header line When I invoke the "Go To Definition" command Then there should be no navigation actions performed +Rule: Offers copying step definition skeleton to clipboard if undefined + Scenario: Navigate from an undefined step Given there is a Reqnroll project scope And the following feature file in the editor diff --git a/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToHooksCommand.feature b/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToHooksCommand.feature new file mode 100644 index 00000000..b9b3b37c --- /dev/null +++ b/Tests/Reqnroll.VisualStudio.Specs/Features/Editor/Commands/GoToHooksCommand.feature @@ -0,0 +1,43 @@ +Feature: Go to hooks command + +Rule: Jumps to hooks related to the scenario + +Scenario: Lists hooks executed for the scenario + e.g. multiple [BeforeScenario], or [BeforeScenario] and [AfterScenario] + Given there is a Reqnroll project scope + And the following hooks in the project: + | type | method | tag scope | hook order | + | BeforeScenario | ResetDatabase | | 1 | + | BeforeScenario | LoginUser | @login | 2 | + | BeforeScenario | NotInScope | @otherTag | | + | AfterScenario | SaveLogs | | | + | BeforeTestRun | StartBrowser | | | + | BeforeFeature | ClearCache | | 20 | + | BeforeFeature | ClearBasicCache | @basic | 10 | + | BeforeFeature | ClearOtherCache | @otherFeatureTag | | + | AfterTestRun | StopBrowser | | | + | BeforeScenarioBlock | PrepareData | | | + | AfterStep | LogStep | | | + When the following feature file is opened in the editor + """ + @basic + Feature: Addition + + @login + Scenario: Add two numbers + When I {caret}press add + """ + And the project is built and the initial binding discovery is performed + When I invoke the "Go To Hooks" command + Then a jump list "Go to hooks" is opened with the following items + | hook | hook type | hook scope | + | StartBrowser | BeforeTestRun | | + | ClearBasicCache | BeforeFeature | @basic | + | ClearCache | BeforeFeature | | + | ResetDatabase | BeforeScenario | | + | LoginUser | BeforeScenario | @login | + | PrepareData | BeforeScenarioBlock | | + | LogStep | AfterStep | | + | SaveLogs | AfterScenario | | + | StopBrowser | AfterTestRun | | + And invoking the first item from the jump list navigates to the "StartBrowser" hook diff --git a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/DeveroomSteps.cs b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/DeveroomSteps.cs index 868e5cbb..1c4838d4 100644 --- a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/DeveroomSteps.cs +++ b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/DeveroomSteps.cs @@ -49,7 +49,7 @@ public void GivenThereIsASimpleReqnrollProjectForVersion(NuGetVersion reqnrollVe _generatorOptions = new GeneratorOptions { - ReqnrollPackageVersion = reqnrollVersion.ToString() + ReqnrollPackageVersion = reqnrollVersion.ToString(), }; } @@ -62,7 +62,8 @@ public void GivenThereIsASmallReqnrollProject() { FeatureFileCount = 1, ScenarioPerFeatureFileCount = 1, - ScenarioOutlinePerScenarioPercent = 0 + ScenarioOutlinePerScenarioPercent = 0, + ReqnrollPackageVersion = DomainDefaults.LatestReqnrollVersion.ToString() }; } @@ -126,6 +127,14 @@ public void GivenThereIsASimpleReqnrollProjectWithUnicodeBindingsForVersion(NuGe _generatorOptions.AddUnicodeBinding = true; } + [Given("there is a small Reqnroll project with hooks")] + public void GivenThereIsASmallReqnrollProjectWithHooks() + { + GivenThereIsASmallReqnrollProject(); + _generatorOptions.AddBeforeScenarioHook = true; + } + + [Given(@"there is a simple Reqnroll project with platform target ""(.*)"" for (.*)")] public void GivenThereIsASimpleReqnrollProjectWithPlatformTargetForVersion(string platformTarget, NuGetVersion reqnrollVersion) @@ -250,6 +259,14 @@ public void ThenTheDiscoverySucceedsWithSeveralStepDefinitions() .HaveCountGreaterThan(1, "there should be step definitions discovered"); } + [Then("the discovery succeeds with hooks")] + public void ThenTheDiscoverySucceedsWithHooks() + { + _bindingRegistry.Should().NotBeNull("the binding registry should have been discovered"); + _bindingRegistry.Hooks.Should() + .HaveCountGreaterThan(0, "there should be hooks discovered"); + } + [Then(@"there is a ""(.*)"" step with regex ""(.*)""")] public void ThenThereIsAStepWithRegex(Reqnroll.VisualStudio.Editor.Services.Parser.ScenarioBlock stepType, string stepDefRegex) { diff --git a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs index b913f998..212d5c2f 100644 --- a/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs +++ b/Tests/Reqnroll.VisualStudio.Specs/StepDefinitions/ProjectSystemSteps.cs @@ -1,4 +1,5 @@ #nullable disable +using Microsoft.Build.Framework.XamlTypes; using ScenarioBlock = Reqnroll.VisualStudio.Editor.Services.Parser.ScenarioBlock; namespace Reqnroll.VisualStudio.Specs.StepDefinitions; @@ -13,7 +14,9 @@ public class ProjectSystemSteps : Steps private DeveroomEditorCommandBase _invokedCommand; private InMemoryStubProjectScope _projectScope; private ProjectStepDefinitionBinding _stepDefinitionBinding; + private ProjectHookBinding _hookBinding; private StubWpfTextView _wpfTextView; + private Random _rnd = new(42); public ProjectSystemSteps(StubIdeScope stubIdeScope) { @@ -128,13 +131,21 @@ public void WhenANewStepDefinitionIsAddedToTheProjectAs(Table stepDefinitionTabl RegisterStepDefinitions(stepDefinitions); } + [Given("the following hooks in the project:")] + public void GivenTheFollowingHooksInTheProject(DataTable hooksTable) + { + var hooks = hooksTable.CreateSet(CreateHookFromTableRow).ToArray(); + RegisterHooks(hooks); + } + private StepDefinition CreateStepDefinitionFromTableRow(DataTableRow tableRow) { var filePath = @"X:\ProjectMock\CalculatorSteps.cs"; + var line = _rnd.Next(1, 30); var stepDefinition = new StepDefinition { Method = $"M{Guid.NewGuid():N}", - SourceLocation = filePath + "|12|5" + SourceLocation = filePath + $"|{line}|5" }; tableRow.TryGetValue("tag scope", out var tagScope); @@ -161,13 +172,55 @@ private StepDefinition CreateStepDefinitionFromTableRow(DataTableRow tableRow) return stepDefinition; } + private Hook CreateHookFromTableRow(DataTableRow tableRow) + { + var filePath = @"X:\ProjectMock\Hooks.cs"; + var line = _rnd.Next(1, 30); + var hook = new Hook + { + Method = $"M{Guid.NewGuid():N}", + SourceLocation = filePath + $"|{line}|8" + }; + + tableRow.TryGetValue("tag scope", out var tagScope); + tableRow.TryGetValue("feature scope", out var featureScope); + tableRow.TryGetValue("scenario scope", out var scenarioScope); + + if (string.IsNullOrEmpty(tagScope)) + tagScope = null; + if (string.IsNullOrEmpty(featureScope)) + featureScope = null; + if (string.IsNullOrEmpty(scenarioScope)) + scenarioScope = null; + + if (tagScope != null || featureScope != null || scenarioScope != null) + hook.Scope = new StepScope + { + Tag = tagScope, + FeatureTitle = featureScope, + ScenarioTitle = scenarioScope + }; + + _projectScope.AddFile(filePath, string.Empty); + + return hook; + } + private void RegisterStepDefinitions(params StepDefinition[] stepDefinitions) { _discoveryService.LastDiscoveryResult = new DiscoveryResult { - StepDefinitions = _discoveryService.LastDiscoveryResult.StepDefinitions.Concat( - stepDefinitions - ).ToArray() + StepDefinitions = _discoveryService.LastDiscoveryResult.StepDefinitions.Concat(stepDefinitions).ToArray(), + Hooks = _discoveryService.LastDiscoveryResult.Hooks + }; + } + + private void RegisterHooks(params Hook[] hooks) + { + _discoveryService.LastDiscoveryResult = new DiscoveryResult + { + StepDefinitions = _discoveryService.LastDiscoveryResult.StepDefinitions, + Hooks = _discoveryService.LastDiscoveryResult.Hooks.Concat(hooks).ToArray() }; } @@ -308,7 +361,16 @@ public void WhenIInvokeTheCommandWithoutWaitingForTagger(string commandName) { case "Go To Definition": { - _invokedCommand = new GoToStepDefinitionCommand( + _invokedCommand = new GoToDefinitionCommand( + _ideScope, + aggregatorFactoryService, + taggerProvider); + _invokedCommand.PreExec(_wpfTextView, _invokedCommand.Targets.First()); + return; + } + case "Go To Hooks": + { + _invokedCommand = new GoToHooksCommand( _ideScope, aggregatorFactoryService, taggerProvider); @@ -582,7 +644,21 @@ public void ThenTheSourceFileOfTheStepDefinitionIsOpened(string stepRegex, Reqnr ActionsMock.LastNavigateToSourceLocation.Should().NotBeNull(); ActionsMock.LastNavigateToSourceLocation.SourceFile.Should() - .Be(_stepDefinitionBinding.Implementation.SourceLocation.SourceFile); + .Be(_stepDefinitionBinding.Implementation.SourceLocation!.SourceFile); + } + + [Then("the source file of the {string} hook is opened")] + public void ThenTheSourceFileOfTheHookIsOpened(string hookMethodName) + { + _hookBinding = _discoveryService.BindingRegistryCache.Value.Hooks + .FirstOrDefault(b => b.Implementation.Method == hookMethodName); + _hookBinding.Should().NotBeNull($"there has to be a {hookMethodName} hook"); + + ActionsMock.LastNavigateToSourceLocation.Should().NotBeNull(); + ActionsMock.LastNavigateToSourceLocation.SourceFile.Should() + .Be(_hookBinding.Implementation.SourceLocation!.SourceFile); + ActionsMock.LastNavigateToSourceLocation.SourceFileLine.Should() + .Be(_hookBinding.Implementation.SourceLocation!.SourceFileLine); } [Then(@"the caret is positioned to the step definition method")] @@ -601,9 +677,12 @@ public void ThenAJumpListIsOpenedWithTheFollowingItems(string expectedHeader, Ta new StepDefinitionJumpListData { StepDefinition = Regex.Match(i.Label, @"\((?.*?)\)").Groups["stepdef"].Value, - StepType = Regex.Match(i.Label, @"\[(?.*?)\(").Groups["stepdeftype"].Value + StepType = Regex.Match(i.Label, @"\[(?.*?)\(").Groups["stepdeftype"].Value, + Hook = Regex.Match(i.Label, @"\]\:\s*(?.*)").Groups["hook"].Value, + HookScope = Regex.Match(i.Label, @"\((?.*?)\)").Groups["hookScope"].Value, + HookType = Regex.Match(i.Label, @"\[(?.*?)[\(\]]").Groups["hookType"].Value, }).ToArray(); - expectedJumpListItemsTable.CompareToSet(actualStepDefs); + expectedJumpListItemsTable.CompareToSet(actualStepDefs, true); } [Then(@"a jump list ""(.*)"" is opened with the following steps")] @@ -634,6 +713,14 @@ private void InvokeFirstContextMenuItem() ThenTheSourceFileOfTheStepDefinitionIsOpened(stepRegex, stepType); } + [Then("invoking the first item from the jump list navigates to the {string} hook")] + public void ThenInvokingTheFirstItemFromTheJumpListNavigatesToTheHook(string hookMethodName) + { + InvokeFirstContextMenuItem(); + + ThenTheSourceFileOfTheHookIsOpened(hookMethodName); + } + [Then(@"invoking the first item from the jump list navigates to the ""([^""]*)"" step in ""([^""]*)"" line (.*)")] public void ThenInvokingTheFirstItemFromTheJumpListNavigatesToTheStepInLine(string step, string expectedFile, int expectedLine) @@ -854,6 +941,9 @@ private class StepDefinitionJumpListData { public string StepDefinition { get; set; } public string StepType { get; set; } + public string HookType { get; set; } + public string Hook { get; set; } + public string HookScope { get; set; } } private class StepDefinitionSnippetData diff --git a/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryCacheTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryCacheTests.cs index 62cd42b0..2ac66c92 100644 --- a/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryCacheTests.cs +++ b/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryCacheTests.cs @@ -17,8 +17,8 @@ public async Task CannotRevertToAnOlderVersionOfBindingRegistry() //arrange var ideScope = new StubIdeScope(_testOutputHelper); var cache = new ProjectBindingRegistryCache(ideScope); - var olderRegistry = ProjectBindingRegistry.FromStepDefinitions(Array.Empty()); - var newerRegistry = ProjectBindingRegistry.FromStepDefinitions(Array.Empty()); + var olderRegistry = ProjectBindingRegistry.FromBindings(Array.Empty()); + var newerRegistry = ProjectBindingRegistry.FromBindings(Array.Empty()); //act await cache.Update(_ => newerRegistry); @@ -36,7 +36,7 @@ public async Task DoNotRevertToAnInvalidBindingRegistry() //arrange var ideScope = new StubIdeScope(_testOutputHelper); var cache = new ProjectBindingRegistryCache(ideScope); - var existingRegistry = ProjectBindingRegistry.FromStepDefinitions(Array.Empty()); + var existingRegistry = ProjectBindingRegistry.FromBindings(Array.Empty()); var invalidRegistry = ProjectBindingRegistry.Invalid; //act @@ -54,7 +54,7 @@ public async Task UpdatesBindingRegistry() //arrange var ideScope = new StubIdeScope(_testOutputHelper); var cache = new ProjectBindingRegistryCache(ideScope); - var bindingRegistry = ProjectBindingRegistry.FromStepDefinitions(new ProjectStepDefinitionBinding[] + var bindingRegistry = ProjectBindingRegistry.FromBindings(new ProjectStepDefinitionBinding[] { new TestProjectStepDefinitionBinding(), new TestProjectStepDefinitionBinding("Error") @@ -84,7 +84,7 @@ public void ParallelUpdate() var projectBindingRegistryCache = new ProjectBindingRegistryCache(ideScope.Object); var oldVersions = new ConcurrentQueue(); - var initialRegistry = new ProjectBindingRegistry(Array.Empty(), 123456); + var initialRegistry = new ProjectBindingRegistry(Array.Empty(), Array.Empty(), 123456); var timeout = TimeSpan.FromSeconds(20); using var cts = new CancellationTokenSource(timeout); @@ -167,7 +167,7 @@ private static void WarmUpThreads(DeveroomCompositeLogger logger, TimeSpan timeo { await Task.Yield(); oldVersions.Enqueue(old.Version); - return new ProjectBindingRegistry(Array.Empty(), + return new ProjectBindingRegistry(Array.Empty(), Array.Empty(), Guid.NewGuid().GetHashCode()); }); @@ -184,13 +184,13 @@ private class TestProjectStepDefinitionBinding : ProjectStepDefinitionBinding { public TestProjectStepDefinitionBinding() : base(ScenarioBlock.Given, new Regex(string.Empty), new Scope(), - new ProjectStepDefinitionImplementation("M1", Array.Empty(), new SourceLocation("file", 0, 0))) + new ProjectBindingImplementation("M1", Array.Empty(), new SourceLocation("file", 0, 0))) { } public TestProjectStepDefinitionBinding(string error) : base(ScenarioBlock.Given, new Regex(string.Empty), new Scope(), - new ProjectStepDefinitionImplementation("M1", Array.Empty(), new SourceLocation("file", 0, 0)), + new ProjectBindingImplementation("M1", Array.Empty(), new SourceLocation("file", 0, 0)), "", error) { } diff --git a/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryTestsBase.cs b/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryTestsBase.cs index 047f6b4f..b5b32881 100644 --- a/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryTestsBase.cs +++ b/Tests/Reqnroll.VisualStudio.Tests/Discovery/ProjectBindingRegistryTestsBase.cs @@ -4,11 +4,12 @@ namespace Reqnroll.VisualStudio.Tests.Discovery; public abstract class ProjectBindingRegistryTestsBase { protected readonly List _stepDefinitionBindings = new(); - protected readonly Dictionary Implementations = new(); + protected readonly List _hookBindings = new(); + protected readonly Dictionary Implementations = new(); protected ProjectBindingRegistry CreateSut() { - var projectBindingRegistry = new ProjectBindingRegistry(_stepDefinitionBindings.ToArray(), 123456); + var projectBindingRegistry = new ProjectBindingRegistry(_stepDefinitionBindings.ToArray(), _hookBindings.ToArray(), 123456); return projectBindingRegistry; } @@ -24,7 +25,7 @@ protected ProjectBindingRegistry CreateSut() if (!Implementations.TryGetValue(methodName, out var implementation)) { implementation = - new ProjectStepDefinitionImplementation(methodName, parameterTypes, + new ProjectBindingImplementation(methodName, parameterTypes, new SourceLocation("MyClass.cs", 2, 5)); Implementations.Add(methodName, implementation); } diff --git a/Tests/Reqnroll.VisualStudio.Tests/Discovery/ReprocessStepDefinitionFileTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Discovery/ReprocessStepDefinitionFileTests.cs index 8f941d07..558c597f 100644 --- a/Tests/Reqnroll.VisualStudio.Tests/Discovery/ReprocessStepDefinitionFileTests.cs +++ b/Tests/Reqnroll.VisualStudio.Tests/Discovery/ReprocessStepDefinitionFileTests.cs @@ -43,7 +43,7 @@ public async Task Approval(string testName) //assert ProjectBindingRegistry bindingRegistry = - ProjectBindingRegistry.FromStepDefinitions(projectStepDefinitionBindings); + ProjectBindingRegistry.FromBindings(projectStepDefinitionBindings); _projectScope.IdeScope.Logger.LogVerbose( $"test retrieved reg v{bindingRegistry.Version} has {bindingRegistry.StepDefinitions.Length}"); var dumped = Dump(bindingRegistry); @@ -68,7 +68,7 @@ public class Foo{ }"); ProjectBindingRegistry bindingRegistry = ProjectBindingRegistry - .FromStepDefinitions(new[] + .FromBindings(new[] { BuildProjectStepDefinitionBinding("^outdated expression$", "Method", stepDefinitionFilePath), BuildProjectStepDefinitionBinding("^expression$", "MethodInOtherFile", otherStepDefinitionFilePath) @@ -98,7 +98,7 @@ public class Foo{ private static ProjectStepDefinitionBinding BuildProjectStepDefinitionBinding(string regex, string method, string otherStepDefinitionFilePath) => new(ScenarioBlock.Given, new Regex(regex), null, - new ProjectStepDefinitionImplementation(method, Array.Empty(), + new ProjectBindingImplementation(method, Array.Empty(), new SourceLocation(otherStepDefinitionFilePath, 0, 0))); private async Task CreateSut(StepDefinition[] initialStepDefinitions) @@ -162,7 +162,7 @@ public string Dump(ProjectStepDefinitionBinding binding) return sb.ToString(); } - public string Dump(ProjectStepDefinitionImplementation implementation) + public string Dump(ProjectBindingImplementation implementation) { IncreaseIndent(); var sb = new StringBuilder(); diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/Completions/StepDefinitionSamplerTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Editor/Completions/StepDefinitionSamplerTests.cs index b83ebd1e..870ac49a 100644 --- a/Tests/Reqnroll.VisualStudio.Tests/Editor/Completions/StepDefinitionSamplerTests.cs +++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/Completions/StepDefinitionSamplerTests.cs @@ -11,7 +11,7 @@ private ProjectStepDefinitionBinding CreateStepDefinitionBinding(string regex, p { parameterTypes = parameterTypes ?? new string[0]; return new ProjectStepDefinitionBinding(ScenarioBlock.Given, new Regex("^" + regex + "$"), null, - new ProjectStepDefinitionImplementation("M1", parameterTypes, null)); + new ProjectBindingImplementation("M1", parameterTypes, null)); } [Fact] diff --git a/Tests/Reqnroll.VisualStudio.Tests/Editor/Services/StepDefinitionUsageFinderTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Editor/Services/StepDefinitionUsageFinderTests.cs index bb037cc1..a8213140 100644 --- a/Tests/Reqnroll.VisualStudio.Tests/Editor/Services/StepDefinitionUsageFinderTests.cs +++ b/Tests/Reqnroll.VisualStudio.Tests/Editor/Services/StepDefinitionUsageFinderTests.cs @@ -17,7 +17,7 @@ private ProjectStepDefinitionBinding CreateStepDefinitionBinding(string regex, p { parameterTypes = parameterTypes ?? new string[0]; return new ProjectStepDefinitionBinding(ScenarioBlock.Given, new Regex("^" + regex + "$"), null, - new ProjectStepDefinitionImplementation("M1", parameterTypes, null)); + new ProjectBindingImplementation("M1", parameterTypes, null)); } private ProjectStepDefinitionBinding[] CreateStepDefinitionBindings(string regex, params string[] parameterTypes) diff --git a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs index 22c7ae23..29fe88ba 100644 --- a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs +++ b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs @@ -27,7 +27,10 @@ private ContextMenu CreateContextMenu() new ContextMenuItem("Defined step [regex]", command, "StepDefinitionsDefined"), new ContextMenuItem("Invalid defined step [regex]", command, "StepDefinitionsDefinedInvalid"), new ContextMenuItem("Ambiguous step [regex]", command, "StepDefinitionsAmbiguous"), - new ContextMenuItem("Undefined step", command, "StepDefinitionsUndefined")); + new ContextMenuItem("Undefined step", command, "StepDefinitionsUndefined"), + new ContextMenuItem("Before hook", command, "BeforeHook"), + new ContextMenuItem("After hook", command, "AfterHook") + ); return contextMenu; } diff --git a/Tests/Reqnroll.VisualStudio.VsxStubs/StepDefinitions/MockableDiscoveryService.cs b/Tests/Reqnroll.VisualStudio.VsxStubs/StepDefinitions/MockableDiscoveryService.cs index f443d9a6..459b0417 100644 --- a/Tests/Reqnroll.VisualStudio.VsxStubs/StepDefinitions/MockableDiscoveryService.cs +++ b/Tests/Reqnroll.VisualStudio.VsxStubs/StepDefinitions/MockableDiscoveryService.cs @@ -8,7 +8,11 @@ public class MockableDiscoveryService : DiscoveryService { } - public DiscoveryResult LastDiscoveryResult { get; set; } = new() {StepDefinitions = Array.Empty()}; + public DiscoveryResult LastDiscoveryResult { get; set; } = new() + { + StepDefinitions = Array.Empty(), + Hooks = Array.Empty() + }; public static MockableDiscoveryService Setup(IProjectScope projectScope, TimeSpan discoveryDelay) => SetupWithInitialStepDefinitions(projectScope, Array.Empty(), discoveryDelay); @@ -20,7 +24,11 @@ public class MockableDiscoveryService : DiscoveryService var discoveryService = new MockableDiscoveryService(projectScope, discoveryResultProviderMock) { - LastDiscoveryResult = new DiscoveryResult {StepDefinitions = stepDefinitions} + LastDiscoveryResult = new DiscoveryResult + { + StepDefinitions = stepDefinitions, + Hooks = Array.Empty() + } }; InMemoryStubProjectBuilder.CreateOutputAssembly(projectScope); diff --git a/Tests/Reqnroll.VisualStudio.VsxStubs/StubDiscoveryResultProvider.cs b/Tests/Reqnroll.VisualStudio.VsxStubs/StubDiscoveryResultProvider.cs index c4d0d356..fdb07a6d 100644 --- a/Tests/Reqnroll.VisualStudio.VsxStubs/StubDiscoveryResultProvider.cs +++ b/Tests/Reqnroll.VisualStudio.VsxStubs/StubDiscoveryResultProvider.cs @@ -3,7 +3,11 @@ namespace Reqnroll.VisualStudio.VsxStubs; public class StubDiscoveryResultProvider : IDiscoveryResultProvider { - public DiscoveryResult DiscoveryResult { get; set; } = new() {StepDefinitions = Array.Empty()}; + public DiscoveryResult DiscoveryResult { get; set; } = new() + { + StepDefinitions = Array.Empty(), + Hooks = Array.Empty() + }; public DiscoveryResult RunDiscovery(string testAssemblyPath, string configFilePath, ProjectSettings projectSettings) =>