diff --git a/Reqnroll.VisualStudio.ProjectTemplate/ProjectTemplate.csproj b/Reqnroll.VisualStudio.ProjectTemplate/ProjectTemplate.csproj index af39a4ff..4dd793d7 100644 --- a/Reqnroll.VisualStudio.ProjectTemplate/ProjectTemplate.csproj +++ b/Reqnroll.VisualStudio.ProjectTemplate/ProjectTemplate.csproj @@ -7,17 +7,7 @@ - $if$ ('$unittestframework$' == 'xUnit') - - - $endif$$if$ ('$unittestframework$' == 'NUnit') - - - $endif$$if$ ('$unittestframework$' == 'MSTest') - - - $endif$$if$ ('$fluentassertionsincluded$' == 'True') - $endif$ + $nugetpackagereferences$ diff --git a/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml b/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml index db226e1a..80456c1c 100644 --- a/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml +++ b/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml @@ -32,7 +32,6 @@ BasedOn="{StaticResource VsCloseButton}"> - @@ -217,41 +216,50 @@ + + - - .NET Framework 4.6.2 - .NET Framework 4.7 - .NET Framework 4.7.1 - .NET Framework 4.7.2 - .NET Framework 4.8 - .NET Framework 4.8.1 - .NET 6.0 - .NET 7.0 - .NET 8.0 - .NET 9.0 - + - + - + + + + + + + + + + + + + diff --git a/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml.cs b/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml.cs index 4efa5c1a..014409b3 100644 --- a/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml.cs +++ b/Reqnroll.VisualStudio.UI/Dialogs/AddNewReqnrollProjectDialog.xaml.cs @@ -1,8 +1,8 @@ -#nullable disable -using System.Windows; -using System.Windows.Controls; +using System.Diagnostics; using Microsoft.VisualStudio.Shell.Interop; using Reqnroll.VisualStudio.UI.ViewModels; +using System.Windows; +using System.Windows.Navigation; namespace Reqnroll.VisualStudio.UI.Dialogs; @@ -16,14 +16,30 @@ public AddNewReqnrollProjectDialog() InitializeComponent(); } - public AddNewReqnrollProjectDialog(AddNewReqnrollProjectViewModel viewModel, IVsUIShell vsUiShell = null) : + public AddNewReqnrollProjectDialog(AddNewReqnrollProjectViewModel viewModel, IVsUIShell? vsUiShell = null) : base(vsUiShell) { ViewModel = viewModel; InitializeComponent(); + Loaded += AddNewReqnrollProjectDialog_LoadedAsync; } - public AddNewReqnrollProjectViewModel ViewModel { get; } + public AddNewReqnrollProjectViewModel? ViewModel { get; } + +#pragma warning disable VSTHRD100 + private async void AddNewReqnrollProjectDialog_LoadedAsync(object sender, RoutedEventArgs e) +#pragma warning restore VSTHRD100 + { + try + { + if (ViewModel != null) + await ViewModel.InitializeAsync(); + } + catch (Exception ex) + { + Debug.WriteLine(ex, "Error during AddNewReqnrollProjectDialog_LoadedAsync"); + } + } private void CreateButton_Click(object sender, RoutedEventArgs e) { @@ -31,10 +47,8 @@ private void CreateButton_Click(object sender, RoutedEventArgs e) Close(); } - private void TestFramework_SelectionChanged(object sender, SelectionChangedEventArgs e) + private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) { - if (e.AddedItems.Count == 0) return; - ViewModel.UnitTestFramework = e.AddedItems[0].ToString(); - e.Handled = true; + OnLinkClicked(sender, e); } } diff --git a/Reqnroll.VisualStudio/Reqnroll.VisualStudio.csproj b/Reqnroll.VisualStudio/Reqnroll.VisualStudio.csproj index d7ff0e86..11f3b47f 100644 --- a/Reqnroll.VisualStudio/Reqnroll.VisualStudio.csproj +++ b/Reqnroll.VisualStudio/Reqnroll.VisualStudio.csproj @@ -8,8 +8,13 @@ TRACE; + + + + + diff --git a/Reqnroll.VisualStudio/Resources/TestFrameworkDescriptors.json b/Reqnroll.VisualStudio/Resources/TestFrameworkDescriptors.json new file mode 100644 index 00000000..462c54c6 --- /dev/null +++ b/Reqnroll.VisualStudio/Resources/TestFrameworkDescriptors.json @@ -0,0 +1,120 @@ +{ + "testFrameworks": [ + { + "tag": "xunit", + "label": "xUnit", + "description": "Use xUnit v2 test executor with Reqnroll", + "url": "https://xunit.net", + "dependencies": [ + { + "name": "Reqnroll.xUnit", + "version": "2.4.1" + }, + { + "name": "xunit", + "version": "2.8.1" + }, + { + "name": "xunit.runner.visualstudio", + "version": "2.8.1" + } + ] + }, + { + "tag": "mstest", + "label": "MsTest", + "description": "Use MsTest v3 test executor with Reqnroll", + "url": "https://github.com/microsoft/testfx?tab=readme-ov-file", + "dependencies": [ + { + "name": "Reqnroll.MsTest", + "version": "2.4.1" + }, + { + "name": "MSTest.TestFramework", + "version": "3.4.3" + }, + { + "name": "MSTest.TestAdapter", + "version": "3.4.3" + } + ] + }, + { + "tag": "nunit", + "label": "NUnit", + "description": "Use NUnit v3 test executor with Reqnroll", + "url": "https://nunit.org", + "dependencies": [ + { + "name": "Reqnroll.NUnit", + "version": "2.4.1" + }, + { + "name": "nunit", + "version": "3.14.0" + }, + { + "name": "NUnit3TestAdapter", + "version": "4.5.0" + } + ] + } + ], + "dotNetFrameworks": [ + { + "tag": "net462", + "label": ".NET Framework 4.6.2" + }, + { + "tag": "net47", + "label": ".NET Framework 4.7" + }, + { + "tag": "net471", + "label": ".NET Framework 4.7.1" + }, + { + "tag": "net472", + "label": ".NET Framework 4.7.2" + }, + { + "tag": "net48", + "label": ".NET Framework 4.8" + }, + { + "tag": "net48", + "label": ".NET Framework 4.8.1" + }, + { + "tag": "net6.0", + "label": ".NET 6.0" + }, + { + "tag": "net7.0", + "label": ".NET 7.0" + }, + { + "tag": "net8.0", + "label": ".NET 8.0", + "default": true + }, + { + "tag": "net9.0", + "label": ".NET 9.0" + } + ], + "validationFrameworks": [ + { + "label": "FluentAssertions", + "description": "Use Fluent Assertions library with Reqnroll", + "url": "https://fluentassertions.com", + "dependencies": [ + { + "name": "FluentAssertions", + "version": "8.3.0" + } + ] + } + ] +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/UI/ViewModels/AddNewReqnrollProjectViewModel.cs b/Reqnroll.VisualStudio/UI/ViewModels/AddNewReqnrollProjectViewModel.cs index 50c42a64..3d4388bf 100644 --- a/Reqnroll.VisualStudio/UI/ViewModels/AddNewReqnrollProjectViewModel.cs +++ b/Reqnroll.VisualStudio/UI/ViewModels/AddNewReqnrollProjectViewModel.cs @@ -1,43 +1,170 @@ -#nullable disable +using Reqnroll.VisualStudio.Wizards.Infrastructure; + namespace Reqnroll.VisualStudio.UI.ViewModels; public class AddNewReqnrollProjectViewModel : INotifyPropertyChanged { - private const string MsTest = "MsTest"; - private const string Net8 = "net8.0"; - #if DEBUG + private static readonly List DesignDataDotNetFrameworks = new() + { + new DotNetFrameworkViewModel("net471", ".NET Framework 4.7.1"), + new DotNetFrameworkViewModel("net8.0",".NET 8.0"), + }; + + private static readonly List DesignDataUnitTestFrameworks = new() + { + new UnitTestFrameworkViewModel("nunit", "NUnit", "Use Reqnroll with NUnit", "https://nunit.org"), + new UnitTestFrameworkViewModel("mstest","MsTest", "Use Reqnroll with MsTest", "https://github.com/microsoft/testfx?tab=readme-ov-file"), + new UnitTestFrameworkViewModel("WithoutDetailsKey", "Without Details", null, null), + }; + public static AddNewReqnrollProjectViewModel DesignData = new() { - DotNetFramework = Net8, - UnitTestFramework = MsTest, - FluentAssertionsIncluded = false + DotNetFrameworks = DesignDataDotNetFrameworks, + DotNetFramework = DesignDataDotNetFrameworks[1], + UnitTestFrameworks = DesignDataUnitTestFrameworks, + UnitTestFramework = DesignDataUnitTestFrameworks[1], }; #endif - private string _dotNetFramework = Net8; - public string DotNetFramework + public class DotNetFrameworkViewModel + { + public string Tag { get; set; } + public string Label { get; set; } + + public DotNetFrameworkViewModel(string tag, string label) + { + Tag = tag; + Label = label; + } + } + + public class UnitTestFrameworkViewModel + { + public string Tag { get; set; } + public string Label { get; set; } + public string? Description { get; set; } + public string? Url { get; set; } + + public UnitTestFrameworkViewModel(string tag, string label, string? description, string? url) + { + Tag = tag; + Label = label; + Description = description; + Url = url; + } + } + + public AddNewReqnrollProjectViewModel() + { + + } + + public AddNewReqnrollProjectViewModel(INewProjectMetaDataProvider metaDataProvider) + { + _metaDataProvider = metaDataProvider; + LoadMetadata(_metaDataProvider.GetFallbackMetadata()); + } + + public async Task InitializeAsync() + { + if (_metaDataProvider == null) + return; // design time + + var metadata = await _metaDataProvider.RetrieveNewProjectMetaDataAsync(); + if (!metadata.IsFallback) // we already loaded the fallback + LoadMetadata(metadata); + } + + private void LoadMetadata(NewProjectMetaData metadata) + { + DotNetFrameworks = metadata.DotNetFrameworksMetadata + .Select(fmd => new DotNetFrameworkViewModel(fmd.Tag, fmd.Label)) + .ToList(); + DotNetFramework = DotNetFrameworks.FirstOrDefault(f => f.Tag == metadata.DotNetFrameworkDefault)!; + UnitTestFrameworks = metadata.TestFrameworkMetaData + .Select(fmd => new UnitTestFrameworkViewModel(fmd.Key, fmd.Value.Label, fmd.Value.Description,fmd.Value.Url)) + .ToList(); + UnitTestFramework = UnitTestFrameworks.FirstOrDefault(f => f.Tag == metadata.TestFrameworkDefault)!; + } + + private readonly INewProjectMetaDataProvider? _metaDataProvider; + + private DotNetFrameworkViewModel _dotNetFramework = new DotNetFrameworkViewModel("net8.0", ".NET 8.0"); + public DotNetFrameworkViewModel DotNetFramework { get => _dotNetFramework; set { + if (value == _dotNetFramework) + { + return; + } + _dotNetFramework = value; - OnPropertyChanged(nameof(TestFrameworks)); + OnPropertyChanged(); + } + } + + private List _dotNetFrameworks = new(); + public List DotNetFrameworks + { + get => _dotNetFrameworks; + set + { + if (Equals(value, _dotNetFrameworks)) + { + return; + } + + _dotNetFrameworks = value; + OnPropertyChanged(); + } + } + + private UnitTestFrameworkViewModel _unitTestFramework = new("mstest", "MsTest", null, null); + public UnitTestFrameworkViewModel UnitTestFramework + { + get => _unitTestFramework; + set + { + if (Equals(value, _unitTestFramework)) + { + return; + } + + _unitTestFramework = value; + OnPropertyChanged(); + } + } + + private List _unitTestFrameworks = new(); + public List UnitTestFrameworks + { + get => _unitTestFrameworks; + set + { + if (Equals(value, _unitTestFrameworks)) + { + return; + } + + _unitTestFrameworks = value; + OnPropertyChanged(); } } - public string UnitTestFramework { get; set; } = MsTest; // FluentAssertions suggestion is temporarily hidden from the UI as it is not free for commercial use anymore. // See https://xceed.com/fluent-assertions-faq/ // Maybe we could consider suggesting https://github.com/shouldly/shouldly instead. public bool FluentAssertionsIncluded { get; set; } = false; - public ObservableCollection TestFrameworks { get; } = new(new List { "MSTest", "NUnit", "xUnit" }); - public event PropertyChangedEventHandler PropertyChanged; + #region INotifyPropertyChanged implementation + public event PropertyChangedEventHandler? PropertyChanged; - [NotifyPropertyChangedInvocator] - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } -} + #endregion +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Wizards/Infrastructure/EnvironmentWrapper.cs b/Reqnroll.VisualStudio/Wizards/Infrastructure/EnvironmentWrapper.cs new file mode 100644 index 00000000..1df9eab4 --- /dev/null +++ b/Reqnroll.VisualStudio/Wizards/Infrastructure/EnvironmentWrapper.cs @@ -0,0 +1,16 @@ +namespace Reqnroll.VisualStudio.Wizards.Infrastructure +{ + public interface IEnvironmentWrapper + { + string? GetEnvironmentVariable(string name); + } + + [Export(typeof(IEnvironmentWrapper))] + public class EnvironmentWrapper : IEnvironmentWrapper + { + public string? GetEnvironmentVariable(string name) + { + return Environment.GetEnvironmentVariable(name); + } + } +} diff --git a/Reqnroll.VisualStudio/Wizards/Infrastructure/HttpClientWrapper.cs b/Reqnroll.VisualStudio/Wizards/Infrastructure/HttpClientWrapper.cs new file mode 100644 index 00000000..3c1efd08 --- /dev/null +++ b/Reqnroll.VisualStudio/Wizards/Infrastructure/HttpClientWrapper.cs @@ -0,0 +1,15 @@ +namespace Reqnroll.VisualStudio.Wizards.Infrastructure; + +[Export(typeof(IHttpClient))] +public class HttpClientWrapper : IHttpClient +{ + public async Task GetStringAsync(string url, CancellationTokenSource cts) + { + using (var client = new System.Net.Http.HttpClient()) + using (var response = await client.GetAsync(url, cts.Token)) + { + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Wizards/Infrastructure/IHttpClient.cs b/Reqnroll.VisualStudio/Wizards/Infrastructure/IHttpClient.cs new file mode 100644 index 00000000..3ba6ded2 --- /dev/null +++ b/Reqnroll.VisualStudio/Wizards/Infrastructure/IHttpClient.cs @@ -0,0 +1,5 @@ +namespace Reqnroll.VisualStudio.Wizards.Infrastructure; +public interface IHttpClient +{ + Task GetStringAsync(string url, CancellationTokenSource cts); +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaData.cs b/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaData.cs new file mode 100644 index 00000000..06ed59c7 --- /dev/null +++ b/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaData.cs @@ -0,0 +1,23 @@ +using System.Linq; + +namespace Reqnroll.VisualStudio.Wizards.Infrastructure +{ + public class NewProjectMetaData + { + public bool IsFallback; + public readonly DotNetFrameworkInfo[] DotNetFrameworksMetadata; + public readonly string TestFrameworkDefault; + public readonly string DotNetFrameworkDefault; + public readonly IDictionary TestFrameworkMetaData; + + public NewProjectMetaData(NewProjectMetaRecord retrievedData, bool isFallback = false) + { + TestFrameworkDefault = retrievedData.TestFrameworks.First().Tag; + DotNetFrameworkDefault = retrievedData.DotNetFrameworks.First(dn => dn.Default == true).Tag; + TestFrameworkMetaData = retrievedData.TestFrameworks + .ToDictionary(tf => tf.Tag, tf => tf); + DotNetFrameworksMetadata = retrievedData.DotNetFrameworks.ToArray(); + IsFallback = isFallback; + } + } +} \ No newline at end of file diff --git a/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaDataModels.cs b/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaDataModels.cs new file mode 100644 index 00000000..1a4e78a7 --- /dev/null +++ b/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaDataModels.cs @@ -0,0 +1,29 @@ +namespace Reqnroll.VisualStudio.Wizards.Infrastructure; + +// Root object +public record NewProjectMetaRecord +{ + public List TestFrameworks { get; init; } + public List DotNetFrameworks { get; init; } + public List? ValidationFrameworks { get; init; } +} + +// Describes a framework. Used to describe both Testing Frameworks(such as Nunit) and accessory frameworks (eg, Validation frameworks like FluentAssertions) +public record FrameworkInfo +{ + public string Tag { get; init; } + public string Label { get; init; } + public string Description { get; init; } + public string Url { get; init; } + public List Dependencies { get; init; } +} + +// Framework information +public record DotNetFrameworkInfo +{ + public string Tag { get; init; } + public string Label { get; init; } + public bool Default { get; init; } +} + +public record NugetPackageDescriptor(string name, string version); diff --git a/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaDataProvider.cs b/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaDataProvider.cs new file mode 100644 index 00000000..94033857 --- /dev/null +++ b/Reqnroll.VisualStudio/Wizards/Infrastructure/NewProjectMetaDataProvider.cs @@ -0,0 +1,92 @@ +namespace Reqnroll.VisualStudio.Wizards.Infrastructure; + +public interface INewProjectMetaDataProvider +{ + IEnumerable DependenciesOf(string testFramework); + Task RetrieveNewProjectMetaDataAsync(); + NewProjectMetaData GetFallbackMetadata(); +} + +[Export(typeof(INewProjectMetaDataProvider))] +public class NewProjectMetaDataProvider : INewProjectMetaDataProvider +{ + private const string EnvironmentVariableOverrideOfMetaDataEndpointUrl = "REQNROLL_VISUALSTUDIOEXTENSION_NPW_FRAMEWORKMETADATAENDPOINTURL"; + private const string MetaDataEndpointUrl = "https://assets.reqnroll.net/testframeworkmetadata/testframeworks.json"; + private NewProjectMetaData _metadata; + private readonly IHttpClient _httpClient; + private readonly IEnvironmentWrapper _environmentWrapper; + + [ImportingConstructor] + public NewProjectMetaDataProvider(IHttpClient httpClient, IEnvironmentWrapper environmentWrapper) + { + _httpClient = httpClient; + _environmentWrapper = environmentWrapper; + _metadata = GetFallbackMetadata(); + } + + public async Task RetrieveNewProjectMetaDataAsync() + { + try + { + using var cts = new DebuggableCancellationTokenSource(TimeSpan.FromSeconds(10)); + + var overrideUrl = _environmentWrapper.GetEnvironmentVariable(EnvironmentVariableOverrideOfMetaDataEndpointUrl); + var url = overrideUrl ?? MetaDataEndpointUrl; + var httpJson = await _httpClient.GetStringAsync(url, cts); + var httpData = JsonSerialization.DeserializeObject(httpJson); + if (httpData != null) + _metadata = new NewProjectMetaData(httpData); + return _metadata; + } + catch + { + return _metadata; + } + } + + public NewProjectMetaData GetFallbackMetadata() => new(CreateFallBackMetaDataRecord(), isFallback: true); + + public IEnumerable DependenciesOf(string testFramework) + { + IEnumerable dependencies = Enumerable.Empty(); + if (_metadata.TestFrameworkMetaData.TryGetValue(testFramework, out var framework)) + { + dependencies = framework.Dependencies; + } + return dependencies; + } + + internal virtual NewProjectMetaRecord CreateFallBackMetaDataRecord() + { + NewProjectMetaRecord CreateEmpty() => + new() + { + DotNetFrameworks = new(), + TestFrameworks = new(), + ValidationFrameworks = new() + }; + + try + { + // read static metadata from a resource file, deserialize the resulting json + var resourceName = "Reqnroll.VisualStudio.Resources.TestFrameworkDescriptors.json"; + var assembly = typeof(NewProjectMetaDataProvider).Assembly; + using var stream = assembly.GetManifestResourceStream(resourceName); + + if (stream == null) + { + return CreateEmpty(); // Resource not found + } + + using var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + var data = JsonSerialization.DeserializeObject(json); + return data ?? CreateEmpty(); // Could be null if deserialization fails + } + catch + { + // Any exception during resource reading or deserialization + return CreateEmpty(); + } + } +} diff --git a/Reqnroll.VisualStudio/Wizards/ReqnrollProjectWizard.cs b/Reqnroll.VisualStudio/Wizards/ReqnrollProjectWizard.cs index fc783a2d..5390cd27 100644 --- a/Reqnroll.VisualStudio/Wizards/ReqnrollProjectWizard.cs +++ b/Reqnroll.VisualStudio/Wizards/ReqnrollProjectWizard.cs @@ -1,7 +1,5 @@ -using System; -using System.Globalization; -using System.Linq; using Reqnroll.VisualStudio.Wizards.Infrastructure; +using System.Globalization; namespace Reqnroll.VisualStudio.Wizards; @@ -10,31 +8,63 @@ public class ReqnrollProjectWizard : IDeveroomWizard { private readonly IDeveroomWindowManager _deveroomWindowManager; private readonly IMonitoringService _monitoringService; + private readonly INewProjectMetaDataProvider _newProjectMetaDataProvider; [ImportingConstructor] - public ReqnrollProjectWizard(IDeveroomWindowManager deveroomWindowManager, IMonitoringService monitoringService) + public ReqnrollProjectWizard(IDeveroomWindowManager deveroomWindowManager, IMonitoringService monitoringService, INewProjectMetaDataProvider newProjectMetaDataProvider) { _deveroomWindowManager = deveroomWindowManager; _monitoringService = monitoringService; + _newProjectMetaDataProvider = newProjectMetaDataProvider; } public bool RunStarted(WizardRunParameters wizardRunParameters) { _monitoringService.MonitorProjectTemplateWizardStarted(); - var viewModel = new AddNewReqnrollProjectViewModel(); + var viewModel = new AddNewReqnrollProjectViewModel(_newProjectMetaDataProvider); var dialogResult = _deveroomWindowManager.ShowDialog(viewModel); if (!dialogResult.HasValue || !dialogResult.Value) return false; - _monitoringService.MonitorProjectTemplateWizardCompleted(viewModel.DotNetFramework, viewModel.UnitTestFramework, + _monitoringService.MonitorProjectTemplateWizardCompleted(viewModel.DotNetFramework.Tag, viewModel.UnitTestFramework.Tag, viewModel.FluentAssertionsIncluded); - // Add custom parameters. - wizardRunParameters.ReplacementsDictionary.Add("$dotnetframework$", viewModel.DotNetFramework); - wizardRunParameters.ReplacementsDictionary.Add("$unittestframework$", viewModel.UnitTestFramework); + // insert set of replacement variables for the SDK package + AddPackageToReplacementDictionary(wizardRunParameters, "Microsoft.NET.Test.Sdk", "17.10.0"); + + var dependencies = _newProjectMetaDataProvider.DependenciesOf(viewModel.UnitTestFramework.Tag); + + foreach (var package in dependencies) + { + var name = package.name; + var version = package.version; + AddPackageToReplacementDictionary(wizardRunParameters, name, version); + } + + if (viewModel.FluentAssertionsIncluded) + AddPackageToReplacementDictionary(wizardRunParameters, "FluentAssertions", "6.12.0"); + + wizardRunParameters.ReplacementsDictionary.Add("$dotnetframework$", viewModel.DotNetFramework.Tag); wizardRunParameters.ReplacementsDictionary.Add("$fluentassertionsincluded$", viewModel.FluentAssertionsIncluded.ToString(CultureInfo.InvariantCulture)); return true; + + static void AddPackageToReplacementDictionary(WizardRunParameters wizardRunParameters, string name, string version) + { + var refText = $""; + const string key = "$nugetpackagereferences$"; + if (wizardRunParameters.ReplacementsDictionary.TryGetValue(key, out string existingValue)) + { + wizardRunParameters.ReplacementsDictionary[key] = + existingValue + + "\r\n " + + refText; + } + else + { + wizardRunParameters.ReplacementsDictionary.Add(key, refText); + } + } } } diff --git a/Tests/Reqnroll.VisualStudio.Tests/Wizards/Infrastructure/NewProjectMetaDataProviderTests.cs b/Tests/Reqnroll.VisualStudio.Tests/Wizards/Infrastructure/NewProjectMetaDataProviderTests.cs new file mode 100644 index 00000000..70a0e4a5 --- /dev/null +++ b/Tests/Reqnroll.VisualStudio.Tests/Wizards/Infrastructure/NewProjectMetaDataProviderTests.cs @@ -0,0 +1,254 @@ +using Reqnroll.VisualStudio.Wizards.Infrastructure; + +namespace Reqnroll.VisualStudio.Tests.Wizards.Infrastructure +{ + class StubNewProjectDataProvider : NewProjectMetaDataProvider + { + internal NewProjectMetaRecord Fallback = new NewProjectMetaRecord + { + TestFrameworks = new List { new FrameworkInfo { Tag = "fallbackframework", Label = "FallbackFramework" } }, + DotNetFrameworks = new List { new DotNetFrameworkInfo { Tag = "fallbackDotNet", Label = "FallbackDotNetFramework", Default=true} } + }; + internal StubNewProjectDataProvider(IHttpClient httpClient, IEnvironmentWrapper environmentWrapper) + : base(httpClient, environmentWrapper) + { + } + internal override NewProjectMetaRecord CreateFallBackMetaDataRecord() + { + return Fallback; + } + } + public class NewProjectMetaDataProviderTests + { + private readonly Mock _httpClientMock; + private readonly Mock _environmentWrapperMock; + private readonly NewProjectMetaDataProvider _sut; + + public NewProjectMetaDataProviderTests() + { + _httpClientMock = new Mock(); + _environmentWrapperMock = new Mock(); + + _sut = new NewProjectMetaDataProvider(_httpClientMock.Object, _environmentWrapperMock.Object); + } + + [Fact] + public async Task RetrieveNewProjectMetaDataAsync_ReturnsMetaData() + { + // Arrange + var validJson = CreateValidMetadataJson(); + _httpClientMock + .Setup(x => x.GetStringAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(validJson)); + + NewProjectMetaData? receivedMetadata = null; + + // Act + receivedMetadata = await _sut.RetrieveNewProjectMetaDataAsync(); + + // Assert + receivedMetadata.Should().NotBeNull(); + receivedMetadata!.TestFrameworkMetaData.Values.Select(fi => fi.Label).Should().Contain("Unique"); + } + + [Fact] + public async Task RetrieveNewProjectMetaDataAsync_UsesDefaultUrl_WhenNoOverride() + { + // Arrange + var validJson = CreateValidMetadataJson(); + _environmentWrapperMock + .Setup(x => x.GetEnvironmentVariable(It.IsAny())) + .Returns((string)null!); + + _httpClientMock + .Setup(x => x.GetStringAsync("https://assets.reqnroll.net/testframeworkmetadata/testframeworks.json", It.IsAny())) + .Returns(Task.FromResult(validJson)); + + // Act + var result = await _sut.RetrieveNewProjectMetaDataAsync(); + + // Assert + result.Should().NotBeNull(); + _httpClientMock.Verify( + x => x.GetStringAsync("https://assets.reqnroll.net/testframeworkmetadata/testframeworks.json", It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RetrieveNewProjectMetaDataAsync_UsesOverrideUrl_WhenSpecified() + { + // Arrange + var customUrl = "https://custom-url/metadata.json"; + var validJson = CreateValidMetadataJson(); + + _environmentWrapperMock + .Setup(x => x.GetEnvironmentVariable("REQNROLL_VISUALSTUDIOEXTENSION_NPW_FRAMEWORKMETADATAENDPOINTURL")) + .Returns(customUrl); + + _httpClientMock + .Setup(x => x.GetStringAsync(customUrl, It.IsAny())) + .Returns(Task.FromResult(validJson)); + + // Act + var result = await _sut.RetrieveNewProjectMetaDataAsync(); + + // Assert + result.Should().NotBeNull(); + _httpClientMock.Verify( + x => x.GetStringAsync(customUrl, It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RetrieveNewProjectMetaDataAsync_UsesFallback_WhenHttpRequestFails() + { + // Arrange + _httpClientMock + .Setup(x => x.GetStringAsync(It.IsAny(), It.IsAny())) + .Throws(new Exception("Connection error")); + + + var sutWithFallbackMock = new StubNewProjectDataProvider(_httpClientMock.Object, _environmentWrapperMock.Object); + + // Act + var result = await sutWithFallbackMock.RetrieveNewProjectMetaDataAsync(); + + // Assert + result.IsFallback.Should().BeTrue(); + result.Should().BeEquivalentTo(new NewProjectMetaData(sutWithFallbackMock.Fallback, true)); + } + + [Fact] + public async Task RetrieveNewProjectMetaDataAsync_UsesFallback_WhenDeserializationFails() + { + // Arrange + _httpClientMock + .Setup(x => x.GetStringAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult("{ invalid json }")); + + var sutWithFallbackMock = new StubNewProjectDataProvider(_httpClientMock.Object, _environmentWrapperMock.Object); + + // Act + var result = await sutWithFallbackMock.RetrieveNewProjectMetaDataAsync(); + + // Assert + result.IsFallback.Should().BeTrue(); + result.Should().BeEquivalentTo(new NewProjectMetaData(sutWithFallbackMock.Fallback, true)); + } + + [Fact] + public async Task DependenciesOf_ReturnsCorrectDependencies_WhenFrameworkExists() + { + // Arrange + var validJson = CreateValidMetadataJson(); + _httpClientMock + .Setup(x => x.GetStringAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(validJson)); + + await _sut.RetrieveNewProjectMetaDataAsync(); + + // Act + var dependencies = _sut.DependenciesOf("nunit").ToArray(); // using tag value + + // Assert + dependencies.Should().NotBeEmpty(); + dependencies.Should().Contain(d => d.name == "NUnit"); + dependencies.Should().Contain(d => d.name == "NUnit3TestAdapter"); + } + + [Fact] + public async Task DependenciesOf_ReturnsEmptyCollection_WhenFrameworkDoesNotExist() + { + // Arrange + var validJson = CreateValidMetadataJson(); + _httpClientMock + .Setup(x => x.GetStringAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(validJson)); + + await _sut.RetrieveNewProjectMetaDataAsync(); + + // Act + var dependencies = _sut.DependenciesOf("NonExistentFramework"); + + // Assert + dependencies.Should().BeEmpty(); + } + + [Fact] + public void CreateFallBackMetaData_ReturnsValidMetaData_WhenResourceExists() + { + // Arrange + var provider = new NewProjectMetaDataProvider(_httpClientMock.Object, _environmentWrapperMock.Object); + + // Act + var result = provider.CreateFallBackMetaDataRecord(); + + // Assert + result.Should().NotBeNull(); + result.TestFrameworks.Should().NotBeEmpty(); + } + + private string CreateValidMetadataJson() + { + return @"{ + ""testFrameworks"": [ + { + ""tag"": ""nunit"", + ""label"": ""NUnit"", + ""description"": ""NUnit test framework"", + ""url"": ""https://nunit.org"", + ""dependencies"": [ + { + ""name"": ""NUnit"", + ""version"": ""3.13.2"" + }, + { + ""name"": ""NUnit3TestAdapter"", + ""version"": ""4.0.0"" + } + ] + }, + { + ""tag"": ""xunit"", + ""label"": ""xUnit"", + ""description"": ""xUnit test framework"", + ""url"": ""https://xunit.net"", + ""dependencies"": [ + { + ""name"": ""xunit"", + ""version"": ""2.4.1"" + }, + { + ""name"": ""xunit.runner.visualstudio"", + ""version"": ""2.4.3"" + } + ] + }, + { + ""tag"": ""unique"", + ""label"": ""Unique"", + ""description"": ""Dummy Test Framework Unique To This Test"", + ""url"": ""https://xunit.net"", + ""dependencies"": [ + { + ""name"": ""xunit"", + ""version"": ""2.4.1"" + }, + { + ""name"": ""xunit.runner.visualstudio"", + ""version"": ""2.4.3"" + } + ] + } + ], + ""dotNetFrameworks"": [ + { + ""label"": "".NET 6.0"", + ""tag"": ""net6.0"", + ""default"": ""true"" + } + ] + }"; + } + } +} \ No newline at end of file diff --git a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml index e8cede97..d9f24ff7 100644 --- a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml +++ b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml @@ -14,6 +14,7 @@ + diff --git a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs index 520dbf0f..7b8030fb 100644 --- a/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs +++ b/Tests/Reqnroll.VisualStudio.UI.Tester/UiTesterWindow.xaml.cs @@ -5,6 +5,7 @@ using Reqnroll.VisualStudio.ProjectSystem.Actions; using Reqnroll.VisualStudio.UI.Dialogs; using Reqnroll.VisualStudio.UI.ViewModels; +using Reqnroll.VisualStudio.Wizards.Infrastructure; namespace Reqnroll.VisualStudio.UI.Tester; @@ -94,7 +95,20 @@ private void Test_ProjectTemplateWizard(object sender, RoutedEventArgs e) if (result != true) return; string resultMessage = - $"Chosen {viewModel.DotNetFramework} with {viewModel.UnitTestFramework}"; + $"Chosen {viewModel.DotNetFramework.Tag} with {viewModel.UnitTestFramework.Tag}"; + + MessageBox.Show(resultMessage); + } + + private void Test_ProjectTemplateWizardWithMetadataProvider(object sender, RoutedEventArgs e) + { + var viewModel = new AddNewReqnrollProjectViewModel(new NewProjectMetaDataProvider(new HttpClientWrapper(), new EnvironmentWrapper())); + var dialog = new AddNewReqnrollProjectDialog(viewModel); + var result = dialog.ShowDialog(); + if (result != true) return; + + string resultMessage = + $"Chosen {viewModel.DotNetFramework.Tag} with {viewModel.UnitTestFramework.Tag}"; MessageBox.Show(resultMessage); }