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