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