diff --git a/.github/workflows/nightlybuild.yml b/.github/workflows/nightlybuild.yml
index a662cd54..9494099b 100644
--- a/.github/workflows/nightlybuild.yml
+++ b/.github/workflows/nightlybuild.yml
@@ -26,8 +26,9 @@ jobs:
- name: Run Unit Tests
run: |
- dotnet test "EstateManagementUI.BusinessLogic.Tests\EstateManagementUI.BusinessLogic.Tests.csproj" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov1.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
- dotnet test "EstateManagementUI.UITests\EstateManagementUI.UITests.csproj" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov2.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
+ dotnet test "EstateManagementUI.BusinessLogic.Tests\EstateManagementUI.BusinessLogic.Tests.csproj" --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov1.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
+ dotnet test "EstateManagementUI.UITests\EstateManagementUI.UITests.csproj" --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov2.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
+ dotnet test "EstateManagementUI.BlazorServer.Tests\EstateManagementUI.BlazorServer.Tests.csproj" --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov3.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
- name: Install LCOV merger
run: npm install -g lcov-result-merger
@@ -224,10 +225,16 @@ jobs:
- name: Run Unit Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "EstateManagementUI.BusinessLogic.Tests\EstateManagementUI.BusinessLogic.Tests.csproj" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov1.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
+ dotnet test "EstateManagementUI.BusinessLogic.Tests\EstateManagementUI.BusinessLogic.Tests.csproj" --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov1.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
+ dotnet test "EstateManagementUI.BlazorServer.Tests\EstateManagementUI.BlazorServer.Tests.csproj" --settings .runsettings /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov2.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
+
+ - name: Merge LCOV reports
+ run: |
+ sudo npm install -g lcov-result-merger
+ lcov-result-merger "*.info" > merged-lcov.info
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
- files: ./lcov1.info
+ files: ./merged-lcov.info
diff --git a/.runsettings b/.runsettings
new file mode 100644
index 00000000..53c758b2
--- /dev/null
+++ b/.runsettings
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+ .*Tests\.dll$
+
+ .*bunit\.core\.dll$
+ .*bunit\.web\.dll$
+
+
+
+
+
+
+ .*BuildRenderTree.*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsNewPageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsNewPageTests.cs
index bb3c6efe..b07d2e06 100644
--- a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsNewPageTests.cs
+++ b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsNewPageTests.cs
@@ -11,6 +11,28 @@ namespace EstateManagementUI.BlazorServer.Tests.Pages.Merchants;
public class MerchantsNewPageTests : BaseTest
{
+ private System.Reflection.MethodInfo GetHandleSubmitMethod()
+ {
+ return typeof(MerchantsNew).GetMethod("HandleSubmit",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ }
+
+ private System.Reflection.FieldInfo GetErrorMessageField(object instance)
+ {
+ // Search through the type hierarchy to find the errorMessage field
+ Type currentType = instance.GetType();
+ System.Reflection.FieldInfo field = null;
+
+ while (currentType != null && field == null)
+ {
+ field = currentType.GetField("errorMessage",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ currentType = currentType.BaseType;
+ }
+
+ return field;
+ }
+
[Fact]
public void MerchantsNew_RendersCorrectly()
{
@@ -61,8 +83,8 @@ public void MerchantsNew_CreateMerchantButton_Exists()
createButton.TextContent.ShouldContain("Create Merchant");
}
- [Fact(Skip = "Form submission tests require CountrySelector interaction - tracked in separate issue")]
- public void MerchantsNew_SuccessfulCreation_ShowsSuccessMessage()
+ [Fact]
+ public async Task HandleSubmit_SuccessfulCreation_SetsSuccessMessage()
{
// Arrange
this.MerchantUIService.Setup(m => m.CreateMerchant(
@@ -74,45 +96,26 @@ public void MerchantsNew_SuccessfulCreation_ShowsSuccessMessage()
var cut = RenderComponent();
cut.Instance.SetDelayOverride(0);
- cut.Render(); // required to trigger re-render
-
- // Act - Fill in form and submit
- var merchantNameInput = cut.Find("input[name='MerchantName']");
- merchantNameInput.Change("Test Merchant");
- var settlementScheduleSelect = cut.Find("select[name='SettlementSchedule']");
- settlementScheduleSelect.Change("Weekly");
-
- var addressLine1Input = cut.Find("input[name='AddressLine1']");
- addressLine1Input.Change("123 Test Street");
-
- var townInput = cut.Find("input[name='Town']");
- townInput.Change("Test Town");
-
- var regionInput = cut.Find("input[name='Region']");
- regionInput.Change("Test Region");
-
- var postCodeInput = cut.Find("input[name='PostCode']");
- postCodeInput.Change("12345");
-
- var contactNameInput = cut.Find("input[name='ContactName']");
- contactNameInput.Change("John Doe");
-
- var emailInput = cut.Find("input[name='EmailAddress']");
- emailInput.Change("john@example.com");
+ // Act - Use reflection to call the private HandleSubmit method on the Dispatcher
+ var handleSubmitMethod = GetHandleSubmitMethod();
+ handleSubmitMethod.ShouldNotBeNull();
+
+ await cut.InvokeAsync(async () =>
+ {
+ var task = (Task)handleSubmitMethod.Invoke(cut.Instance, null);
+ await task;
+ });
- var phoneInput = cut.Find("input[name='PhoneNumber']");
- phoneInput.Change("1234567890");
+ // Trigger a render to ensure state changes are reflected
+ cut.Render();
- var createButton = cut.Find("#createMerchantButton");
- createButton.Click();
-
- // Assert
+ // Assert - Verify success message was set
cut.WaitForAssertion(() => cut.Markup.ShouldContain("Merchant created successfully"), timeout: TimeSpan.FromSeconds(5));
}
- [Fact(Skip = "Form submission tests require CountrySelector interaction - tracked in separate issue")]
- public void MerchantsNew_SuccessfulCreation_NavigatesToMerchantsList()
+ [Fact]
+ public async Task HandleSubmit_SuccessfulCreation_NavigatesToMerchantsList()
{
// Arrange
this.MerchantUIService.Setup(m => m.CreateMerchant(
@@ -124,45 +127,23 @@ public void MerchantsNew_SuccessfulCreation_NavigatesToMerchantsList()
var cut = RenderComponent();
cut.Instance.SetDelayOverride(0);
- cut.Render(); // required to trigger re-render
-
- // Act - Fill in form and submit
- var merchantNameInput = cut.Find("input[name='MerchantName']");
- merchantNameInput.Change("Test Merchant");
-
- var settlementScheduleSelect = cut.Find("select[name='SettlementSchedule']");
- settlementScheduleSelect.Change("Weekly");
-
- var addressLine1Input = cut.Find("input[name='AddressLine1']");
- addressLine1Input.Change("123 Test Street");
-
- var townInput = cut.Find("input[name='Town']");
- townInput.Change("Test Town");
-
- var regionInput = cut.Find("input[name='Region']");
- regionInput.Change("Test Region");
- var postCodeInput = cut.Find("input[name='PostCode']");
- postCodeInput.Change("12345");
-
- var contactNameInput = cut.Find("input[name='ContactName']");
- contactNameInput.Change("John Doe");
-
- var emailInput = cut.Find("input[name='EmailAddress']");
- emailInput.Change("john@example.com");
-
- var phoneInput = cut.Find("input[name='PhoneNumber']");
- phoneInput.Change("1234567890");
-
- var createButton = cut.Find("#createMerchantButton");
- createButton.Click();
+ // Act - Use reflection to call the private HandleSubmit method on the Dispatcher
+ var handleSubmitMethod = GetHandleSubmitMethod();
+ handleSubmitMethod.ShouldNotBeNull();
+
+ await cut.InvokeAsync(async () =>
+ {
+ var task = (Task)handleSubmitMethod.Invoke(cut.Instance, null);
+ await task;
+ });
- // Assert - Wait for navigation
- cut.WaitForAssertion(() => _fakeNavigationManager.Uri.ShouldContain("/merchants"), timeout: TimeSpan.FromSeconds(5));
+ // Assert - Should navigate to /merchants
+ _fakeNavigationManager.Uri.ShouldContain("/merchants");
}
- [Fact(Skip = "Form submission tests require CountrySelector interaction - tracked in separate issue")]
- public void MerchantsNew_FailedCreation_ShowsErrorMessage()
+ [Fact]
+ public async Task HandleSubmit_FailedCreation_SetsErrorMessage()
{
// Arrange
this.MerchantUIService.Setup(m => m.CreateMerchant(
@@ -174,43 +155,25 @@ public void MerchantsNew_FailedCreation_ShowsErrorMessage()
var cut = RenderComponent();
- // Act - Fill in form and submit
- var merchantNameInput = cut.Find("input[name='MerchantName']");
- merchantNameInput.Change("Test Merchant");
-
- var settlementScheduleSelect = cut.Find("select[name='SettlementSchedule']");
- settlementScheduleSelect.Change("Weekly");
-
- var addressLine1Input = cut.Find("input[name='AddressLine1']");
- addressLine1Input.Change("123 Test Street");
-
- var townInput = cut.Find("input[name='Town']");
- townInput.Change("Test Town");
-
- var regionInput = cut.Find("input[name='Region']");
- regionInput.Change("Test Region");
-
- var postCodeInput = cut.Find("input[name='PostCode']");
- postCodeInput.Change("12345");
-
- var contactNameInput = cut.Find("input[name='ContactName']");
- contactNameInput.Change("John Doe");
-
- var emailInput = cut.Find("input[name='EmailAddress']");
- emailInput.Change("john@example.com");
-
- var phoneInput = cut.Find("input[name='PhoneNumber']");
- phoneInput.Change("1234567890");
+ // Act - Use reflection to call the private HandleSubmit method on the Dispatcher
+ var handleSubmitMethod = GetHandleSubmitMethod();
+ handleSubmitMethod.ShouldNotBeNull();
+
+ await cut.InvokeAsync(async () =>
+ {
+ var task = (Task)handleSubmitMethod.Invoke(cut.Instance, null);
+ await task;
+ });
- var createButton = cut.Find("#createMerchantButton");
- createButton.Click();
+ // Trigger a render to ensure state changes are reflected
+ cut.Render();
- // Assert
+ // Assert - Verify error message was set
cut.WaitForAssertion(() => cut.Markup.ShouldContain("Failed to create merchant"), timeout: TimeSpan.FromSeconds(5));
}
- [Fact(Skip = "Form submission tests require CountrySelector interaction - tracked in separate issue")]
- public void MerchantsNew_FailedCreation_DoesNotNavigate()
+ [Fact]
+ public async Task HandleSubmit_FailedCreation_DoesNotNavigate()
{
// Arrange
this.MerchantUIService.Setup(m => m.CreateMerchant(
@@ -221,103 +184,109 @@ public void MerchantsNew_FailedCreation_DoesNotNavigate()
.ReturnsAsync(Result.Failure);
var cut = RenderComponent();
+ var initialUri = _fakeNavigationManager.Uri;
- // Act - Fill in form and submit
- var merchantNameInput = cut.Find("input[name='MerchantName']");
- merchantNameInput.Change("Test Merchant");
-
- var settlementScheduleSelect = cut.Find("select[name='SettlementSchedule']");
- settlementScheduleSelect.Change("Weekly");
-
- var addressLine1Input = cut.Find("input[name='AddressLine1']");
- addressLine1Input.Change("123 Test Street");
-
- var townInput = cut.Find("input[name='Town']");
- townInput.Change("Test Town");
-
- var regionInput = cut.Find("input[name='Region']");
- regionInput.Change("Test Region");
+ // Act - Use reflection to call the private HandleSubmit method on the Dispatcher
+ var handleSubmitMethod = GetHandleSubmitMethod();
+ handleSubmitMethod.ShouldNotBeNull();
+
+ await cut.InvokeAsync(async () =>
+ {
+ var task = (Task)handleSubmitMethod.Invoke(cut.Instance, null);
+ await task;
+ });
- var postCodeInput = cut.Find("input[name='PostCode']");
- postCodeInput.Change("12345");
+ // Assert - Should not navigate (should remain on the same page, not go to /merchants list)
+ _fakeNavigationManager.Uri.ShouldBe(initialUri);
+ }
- var contactNameInput = cut.Find("input[name='ContactName']");
- contactNameInput.Change("John Doe");
+ [Fact]
+ public async Task HandleSubmit_CallsCreateMerchantWithCorrectParameters()
+ {
+ // Arrange
+ Guid capturedEstateId = default;
+ Guid capturedMerchantId = default;
+ CorrelationId capturedCorrelationId = default;
+ MerchantModels.CreateMerchantModel capturedModel = default;
- var emailInput = cut.Find("input[name='EmailAddress']");
- emailInput.Change("john@example.com");
+ this.MerchantUIService.Setup(m => m.CreateMerchant(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback(
+ (correlationId, estateId, merchantId, model) => {
+ capturedCorrelationId = correlationId;
+ capturedEstateId = estateId;
+ capturedMerchantId = merchantId;
+ capturedModel = model;
+ })
+ .ReturnsAsync(Result.Success);
- var phoneInput = cut.Find("input[name='PhoneNumber']");
- phoneInput.Change("1234567890");
+ var cut = RenderComponent();
+ cut.Instance.SetDelayOverride(0);
- var createButton = cut.Find("#createMerchantButton");
- createButton.Click();
+ // Act - Use reflection to call the private HandleSubmit method on the Dispatcher
+ var handleSubmitMethod = GetHandleSubmitMethod();
+ handleSubmitMethod.ShouldNotBeNull();
+
+ await cut.InvokeAsync(async () =>
+ {
+ var task = (Task)handleSubmitMethod.Invoke(cut.Instance, null);
+ await task;
+ });
- // Assert - Should not navigate to /merchants
- cut.WaitForAssertion(() => cut.Markup.ShouldContain("Failed to create merchant"), timeout: TimeSpan.FromSeconds(5));
- _fakeNavigationManager.Uri.ShouldNotContain("/merchants");
+ // Assert - Verify CreateMerchant was called with proper parameters
+ this.MerchantUIService.Verify(m => m.CreateMerchant(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()), Times.Once);
+
+ capturedCorrelationId.ShouldNotBeNull();
+ capturedEstateId.ShouldNotBe(Guid.Empty);
+ capturedMerchantId.ShouldNotBe(Guid.Empty);
+ capturedModel.ShouldNotBeNull();
}
- [Fact(Skip = "Form submission tests require CountrySelector interaction - tracked in separate issue")]
- public void MerchantsNew_SavingState_ShowsLoadingIndicator()
+ [Fact]
+ public async Task HandleSubmit_ClearsErrorMessageBeforeSubmit()
{
// Arrange
- var tcs = new TaskCompletionSource();
this.MerchantUIService.Setup(m => m.CreateMerchant(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
- .Returns(tcs.Task);
+ .ReturnsAsync(Result.Success);
var cut = RenderComponent();
+ cut.Instance.SetDelayOverride(0);
- // Act - Fill in form and submit
- var merchantNameInput = cut.Find("input[name='MerchantName']");
- merchantNameInput.Change("Test Merchant");
-
- var settlementScheduleSelect = cut.Find("select[name='SettlementSchedule']");
- settlementScheduleSelect.Change("Weekly");
-
- var addressLine1Input = cut.Find("input[name='AddressLine1']");
- addressLine1Input.Change("123 Test Street");
-
- var townInput = cut.Find("input[name='Town']");
- townInput.Change("Test Town");
-
- var regionInput = cut.Find("input[name='Region']");
- regionInput.Change("Test Region");
+ // Set an error message first using reflection
+ var errorMessageField = GetErrorMessageField(cut.Instance);
+ errorMessageField.ShouldNotBeNull();
+ errorMessageField.SetValue(cut.Instance, "Previous error");
+ cut.Render();
- var postCodeInput = cut.Find("input[name='PostCode']");
- postCodeInput.Change("12345");
+ // Verify error is displayed
+ cut.Markup.ShouldContain("Previous error");
- // Interact with CountrySelector - find the button that opens the dropdown
- var countryButtons = cut.FindAll("button[aria-label='Select country']");
- if (countryButtons.Any())
+ // Act - Use reflection to call the private HandleSubmit method on the Dispatcher
+ var handleSubmitMethod = GetHandleSubmitMethod();
+ handleSubmitMethod.ShouldNotBeNull();
+
+ await cut.InvokeAsync(async () =>
{
- countryButtons.First().Click();
- // Find and click a country option (e.g., United Kingdom)
- var countryOptions = cut.FindAll("button");
- var ukButton = countryOptions.FirstOrDefault(b => b.TextContent.Contains("United Kingdom"));
- ukButton?.Click();
- }
+ var task = (Task)handleSubmitMethod.Invoke(cut.Instance, null);
+ await task;
+ });
- var contactNameInput = cut.Find("input[name='ContactName']");
- contactNameInput.Change("John Doe");
+ // Trigger a render to ensure state changes are reflected
+ cut.Render();
- var emailInput = cut.Find("input[name='EmailAddress']");
- emailInput.Change("john@example.com");
-
- var phoneInput = cut.Find("input[name='PhoneNumber']");
- phoneInput.Change("1234567890");
-
- var createButton = cut.Find("#createMerchantButton");
- createButton.Click();
-
- // Assert - Should show "Saving..." text
- cut.WaitForAssertion(() => cut.Markup.ShouldContain("Saving..."), timeout: TimeSpan.FromSeconds(5));
-
- // Complete the task
- tcs.SetResult(Result.Success());
+ // Assert - Error message should be cleared and success message shown
+ cut.Markup.ShouldNotContain("Previous error");
+ cut.Markup.ShouldContain("Merchant created successfully");
}
}