diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsEditPageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsEditPageTests.cs index e60e7ff7..6fb2f7f8 100644 --- a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsEditPageTests.cs +++ b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantsEditPageTests.cs @@ -725,6 +725,343 @@ public void MerchantsEdit_DisplaysAssignedContracts_WhenPresent() cut.WaitForAssertion(() => cut.Markup.ShouldContain("Test Contract"), timeout: TimeSpan.FromSeconds(5)); } + [Fact] + public void MerchantsEdit_LoadMerchant_Success_LoadsAllData() + { + // Arrange + var merchantId = Guid.NewGuid(); + SetupSuccessfulDataLoad(merchantId, "Test Merchant", "MERCH001"); + + // Act + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Test Merchant"), timeout: TimeSpan.FromSeconds(5)); + cut.Markup.ShouldContain("MERCH001"); + this.MerchantUIService.Verify(m => m.GetMerchant(It.IsAny(), It.IsAny(), merchantId), Times.Once); + this.MerchantUIService.Verify(m => m.GetMerchantOperators(It.IsAny(), It.IsAny(), merchantId), Times.Once); + this.MerchantUIService.Verify(m => m.GetMerchantContracts(It.IsAny(), It.IsAny(), merchantId), Times.Once); + this.MerchantUIService.Verify(m => m.GetMerchantDevices(It.IsAny(), It.IsAny(), merchantId), Times.Once); + } + + [Fact] + public void MerchantsEdit_AddDeviceToMerchant_Success_ShowsSuccessMessage() + { + // Arrange + var merchantId = Guid.NewGuid(); + SetupSuccessfulDataLoad(merchantId); + + this.MerchantUIService.Setup(m => m.AddMerchantDevice(It.IsAny(), It.IsAny(), merchantId, It.IsAny())) + .ReturnsAsync(Result.Success); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to devices tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? devicesButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Assigned Devices")); + devicesButton?.Click(); + + // Click Add Device button + IElement addDeviceButton = cut.Find("#addDeviceButton"); + addDeviceButton.Click(); + + // Act - Enter device identifier and add + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + IElement deviceInput = cut.Find("input[placeholder*='device']"); + deviceInput.Change("DEV123"); + + IElement addButton = cut.FindAll("button") + .First(b => b.TextContent.Trim() == "Add" && (b.GetAttribute("id") ?? "") != "addDeviceButton"); + addButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Device added successfully"), timeout: TimeSpan.FromSeconds(10)); + this.MerchantUIService.Verify(m => m.AddMerchantDevice(It.IsAny(), It.IsAny(), merchantId, "DEV123"), Times.Once); + } + + [Fact] + public void MerchantsEdit_AddDeviceToMerchant_Failure_ShowsErrorMessage() + { + // Arrange + var merchantId = Guid.NewGuid(); + SetupSuccessfulDataLoad(merchantId); + + this.MerchantUIService.Setup(m => m.AddMerchantDevice(It.IsAny(), It.IsAny(), merchantId, It.IsAny())) + .ReturnsAsync(Result.Failure()); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to devices tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? devicesButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Assigned Devices")); + devicesButton?.Click(); + + // Click Add Device button + IElement addDeviceButton = cut.Find("#addDeviceButton"); + addDeviceButton.Click(); + + // Act - Enter device identifier and add + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + IElement deviceInput = cut.Find("input[placeholder*='device']"); + deviceInput.Change("DEV123"); + + IElement addButton = cut.FindAll("button") + .First(b => b.TextContent.Trim() == "Add" && (b.GetAttribute("id") ?? "") != "addDeviceButton"); + addButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Failed to add device"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void MerchantsEdit_SaveAllChanges_Success_ShowsSuccessMessageAndNavigates() + { + // Arrange + var merchantId = Guid.NewGuid(); + SetupSuccessfulDataLoad(merchantId); + + this.MerchantUIService.Setup(m => m.UpdateMerchant(It.IsAny(), It.IsAny(), merchantId, It.IsAny())) + .ReturnsAsync(Result.Success); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Act - Find and click save button (button text is "Save" not "Save Changes") + IRefreshableElementCollection buttons = cut.FindAll("button[type='submit']"); + IElement? saveButton = buttons.FirstOrDefault(); + saveButton.ShouldNotBeNull(); + saveButton.Click(); + + // Assert - Component has WaitOnUIRefresh() with 2.5s delay before navigation + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Merchant details updated successfully"), timeout: TimeSpan.FromSeconds(10)); + cut.WaitForState(() => _fakeNavigationManager.Uri.Contains("/merchants"), TimeSpan.FromSeconds(10)); + _fakeNavigationManager.Uri.ShouldContain("/merchants"); + this.MerchantUIService.Verify(m => m.UpdateMerchant(It.IsAny(), It.IsAny(), merchantId, It.IsAny()), Times.Once); + } + + [Fact] + public void MerchantsEdit_SaveAllChanges_Failure_ShowsErrorMessage() + { + // Arrange + var merchantId = Guid.NewGuid(); + SetupSuccessfulDataLoad(merchantId); + + this.MerchantUIService.Setup(m => m.UpdateMerchant(It.IsAny(), It.IsAny(), merchantId, It.IsAny())) + .ReturnsAsync(Result.Failure()); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Act - Find and click save button (button text is "Save" not "Save Changes") + IRefreshableElementCollection buttons = cut.FindAll("button[type='submit']"); + IElement? saveButton = buttons.FirstOrDefault(); + saveButton.ShouldNotBeNull(); + saveButton.Click(); + + // Assert - Use longer timeout to account for async operations + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Failed to update merchant"), timeout: TimeSpan.FromSeconds(10)); + this.MerchantUIService.Verify(m => m.UpdateMerchant(It.IsAny(), It.IsAny(), merchantId, It.IsAny()), Times.Once); + } + + [Fact] + public void MerchantsEdit_SwapDeviceConfirm_Success_ShowsSuccessMessage() + { + // Arrange + var merchantId = Guid.NewGuid(); + var existingDevice = new MerchantModels.MerchantDeviceModel { DeviceIdentifier = "DEV123" }; + SetupSuccessfulDataLoad(merchantId, assignedDevices: new List { existingDevice }); + + this.MerchantUIService.Setup(m => m.SwapMerchantDevice(It.IsAny(), It.IsAny(), merchantId, "DEV123", "DEV456")) + .ReturnsAsync(Result.Success); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to devices tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? devicesButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Assigned Devices")); + devicesButton?.Click(); + + // Click Swap button for the device + cut.WaitForAssertion(() => cut.FindAll("button").Any(b => b.TextContent.Contains("Swap")).ShouldBeTrue(), timeout: TimeSpan.FromSeconds(5)); + IElement swapButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Swap")); + swapButton.Click(); + + // Act - Enter new device identifier and confirm swap + cut.WaitForAssertion(() => cut.Markup.ShouldContain("New Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + IElement swapInput = cut.FindAll("input").Last(); + swapInput.Change("DEV456"); + + IElement confirmButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Confirm")); + confirmButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Device DEV123 swapped for DEV456"), timeout: TimeSpan.FromSeconds(5)); + this.MerchantUIService.Verify(m => m.SwapMerchantDevice(It.IsAny(), It.IsAny(), merchantId, "DEV123", "DEV456"), Times.Once); + } + + [Fact] + public void MerchantsEdit_SwapDeviceConfirm_EmptyIdentifier_ShowsError() + { + // Arrange + var merchantId = Guid.NewGuid(); + var existingDevice = new MerchantModels.MerchantDeviceModel { DeviceIdentifier = "DEV123" }; + SetupSuccessfulDataLoad(merchantId, assignedDevices: new List { existingDevice }); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to devices tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? devicesButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Assigned Devices")); + devicesButton?.Click(); + + // Click Swap button for the device + cut.WaitForAssertion(() => cut.FindAll("button").Any(b => b.TextContent.Contains("Swap")).ShouldBeTrue(), timeout: TimeSpan.FromSeconds(5)); + IElement swapButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Swap")); + swapButton.Click(); + + // Act - Try to confirm swap without entering identifier + cut.WaitForAssertion(() => cut.Markup.ShouldContain("New Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + IElement confirmButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Confirm")); + confirmButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("New device identifier is required"), timeout: TimeSpan.FromSeconds(5)); + this.MerchantUIService.Verify(m => m.SwapMerchantDevice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void MerchantsEdit_SwapDeviceConfirm_SameIdentifier_ShowsError() + { + // Arrange + var merchantId = Guid.NewGuid(); + var existingDevice = new MerchantModels.MerchantDeviceModel { DeviceIdentifier = "DEV123" }; + SetupSuccessfulDataLoad(merchantId, assignedDevices: new List { existingDevice }); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to devices tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? devicesButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Assigned Devices")); + devicesButton?.Click(); + + // Click Swap button for the device + cut.WaitForAssertion(() => cut.FindAll("button").Any(b => b.TextContent.Contains("Swap")).ShouldBeTrue(), timeout: TimeSpan.FromSeconds(5)); + IElement swapButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Swap")); + swapButton.Click(); + + // Act - Enter same device identifier and try to confirm swap + cut.WaitForAssertion(() => cut.Markup.ShouldContain("New Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + IElement swapInput = cut.FindAll("input").Last(); + swapInput.Change("DEV123"); + + IElement confirmButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Confirm")); + confirmButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("New device identifier cannot be the same as the current device"), timeout: TimeSpan.FromSeconds(5)); + this.MerchantUIService.Verify(m => m.SwapMerchantDevice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void MerchantsEdit_SwapDeviceConfirm_DuplicateIdentifier_ShowsError() + { + // Arrange + var merchantId = Guid.NewGuid(); + var device1 = new MerchantModels.MerchantDeviceModel { DeviceIdentifier = "DEV123" }; + var device2 = new MerchantModels.MerchantDeviceModel { DeviceIdentifier = "DEV456" }; + SetupSuccessfulDataLoad(merchantId, assignedDevices: new List { device1, device2 }); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to devices tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? devicesButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Assigned Devices")); + devicesButton?.Click(); + + // Click Swap button for the first device + cut.WaitForAssertion(() => cut.FindAll("button").Any(b => b.TextContent.Contains("Swap")).ShouldBeTrue(), timeout: TimeSpan.FromSeconds(5)); + IElement swapButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Swap")); + swapButton.Click(); + + // Act - Try to swap to an already assigned device identifier + cut.WaitForAssertion(() => cut.Markup.ShouldContain("New Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + IElement swapInput = cut.FindAll("input").Last(); + swapInput.Change("DEV456"); + + IElement confirmButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Confirm")); + confirmButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("The specified device identifier is already assigned"), timeout: TimeSpan.FromSeconds(5)); + this.MerchantUIService.Verify(m => m.SwapMerchantDevice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void MerchantsEdit_SwapDeviceConfirm_Failure_ClearsSwapUI() + { + // Arrange + var merchantId = Guid.NewGuid(); + var existingDevice = new MerchantModels.MerchantDeviceModel { DeviceIdentifier = "DEV123" }; + SetupSuccessfulDataLoad(merchantId, assignedDevices: new List { existingDevice }); + + this.MerchantUIService.Setup(m => m.SwapMerchantDevice(It.IsAny(), It.IsAny(), merchantId, "DEV123", "DEV456")) + .ReturnsAsync(Result.Failure()); + + IRenderedComponent cut = RenderComponent(parameters => parameters + .Add(p => p.MerchantId, merchantId)); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to devices tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? devicesButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Assigned Devices")); + devicesButton?.Click(); + + // Click Swap button for the device + cut.WaitForAssertion(() => cut.FindAll("button").Any(b => b.TextContent.Contains("Swap")).ShouldBeTrue(), timeout: TimeSpan.FromSeconds(5)); + IElement swapButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Swap")); + swapButton.Click(); + + // Verify swap UI is shown + cut.WaitForAssertion(() => cut.Markup.ShouldContain("New Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + + // Act - Enter new device identifier and confirm swap + IElement swapInput = cut.FindAll("input").Last(); + swapInput.Change("DEV456"); + + IElement confirmButton = cut.FindAll("button") + .First(b => b.TextContent.Contains("Confirm")); + confirmButton.Click(); + + // Assert - Component calls CancelSwapDevice() which clears the swap UI even on failure + // This means "New Device Identifier" should no longer be in markup after the operation + cut.WaitForAssertion(() => cut.Markup.ShouldNotContain("New Device Identifier"), timeout: TimeSpan.FromSeconds(5)); + this.MerchantUIService.Verify(m => m.SwapMerchantDevice(It.IsAny(), It.IsAny(), merchantId, "DEV123", "DEV456"), Times.Once); + } + // Helper methods private void SetupSuccessfulDataLoad(Guid merchantId, string merchantName = "Test Merchant",