diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 78b65df..165dab8 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -95,11 +95,17 @@ jobs: - name: Restore Nuget Packages run: dotnet restore EstateManagementUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }} + - name: Build Integration Tests + run: dotnet build EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj --configuration Debug --no-restore + + - name: Install Playwright Chromium + run: pwsh EstateManagementUI.IntegrationTests/bin/Debug/net10.0/playwright.ps1 install --with-deps chromium + - name: Run Integration Tests (Chrome) env: Browser: Chrome IsCI: true - run: dotnet test "EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj" # --filter Category=PRTest + run: dotnet test "EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj" --configuration Debug --no-build --settings .runsettings # --filter Category=PRTest - name: Upload Playwright screenshots on failure uses: actions/upload-artifact@v4.4.0 diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs index 9c00916..aa8c5bd 100644 --- a/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs +++ b/EstateManagementUI.BlazorServer.Tests/Pages/Merchants/MerchantSchedulePageTests.cs @@ -266,9 +266,10 @@ public void MerchantSchedule_ReadOnlyMode_DisablesEditingControls() } }); + _fakeNavigationManager.NavigateTo($"/merchants/{merchantId}/schedule?readOnly=true"); + var cut = RenderComponent(parameters => parameters - .Add(p => p.MerchantId, merchantId) - .Add(p => p.ReadOnly, true)); + .Add(p => p.MerchantId, merchantId)); cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); cut.Markup.ShouldContain("Selected Year Schedule"); diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs index 62270cc..401091b 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/Schedule.razor.cs @@ -15,7 +15,7 @@ public partial class Schedule public Guid MerchantId { get; set; } [Parameter] - //[SupplyParameterFromQuery(Name = "readOnly")] + [SupplyParameterFromQuery(Name = "readOnly")] public Boolean ReadOnly { get; set; } private readonly DateTime today = DateTime.Today; diff --git a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs index 8474069..614ea02 100644 --- a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs +++ b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs @@ -7,6 +7,8 @@ using System.IO; using System.Text; using Shared.IntegrationTesting; +using SimpleResults; +using TransactionProcessor.DataTransferObjects.Requests.MerchantSchedule; namespace EstateManagementUI.IntegrationTests.Common; @@ -422,7 +424,7 @@ await option.WaitForAsync(new LocatorWaitForOptions optionValue.ShouldNotBeNull(); await _page.Locator("select").SelectOptionAsync(new[] { optionValue! }); - await _page.GetByRole(AriaRole.Button, new() { Name = "Add" }).ClickAsync(); + await _page.GetByRole(AriaRole.Button, new() { Name = "Add", Exact = true }).ClickAsync(); }, nameof(AddOperatorToEstateAsync)); } @@ -710,6 +712,565 @@ await RunWithFailureArtifactsAsync(async () => }, nameof(BackToContractListAsync)); } + public async Task OpenMerchantManagementScreenAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var merchantsLink = _page.Locator("#merchantsLink"); + if (await merchantsLink.CountAsync() > 0 && await merchantsLink.First.IsVisibleAsync()) + { + await merchantsLink.First.ClickAsync(new LocatorClickOptions { NoWaitAfter = true }); + } + else + { + await _page.GotoAsync(ResolveBaseUrl() + "/merchants"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenMerchantManagementScreenAsync)); + } + + public async Task AssertMerchantManagementHeadingVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = "Merchant Management" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantManagementHeadingVisibleAsync)); + } + + public async Task CreateMerchantAsync(string merchantName) + { + await RunWithFailureArtifactsAsync(async () => + { + var newMerchantButton = _page.Locator("#newMerchantButton"); + if (await newMerchantButton.CountAsync() > 0 && await newMerchantButton.First.IsVisibleAsync()) + { + await newMerchantButton.First.ClickAsync(new LocatorClickOptions { NoWaitAfter = true }); + } + else + { + await _page.GotoAsync(ResolveBaseUrl() + "/merchants/new"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await _page.Locator("input[placeholder='Enter merchant name']").FillAsync(merchantName); + await _page.Locator("select[name='SettlementSchedule']").SelectOptionAsync("Immediate"); + await _page.Locator("input[placeholder='Enter address line 1']").FillAsync("1 Integration Road"); + await _page.Locator("input[placeholder='Enter address line 2 (optional)']").FillAsync("Suite 100"); + await _page.Locator("input[placeholder='Enter town']").FillAsync("Test Town"); + await _page.Locator("input[placeholder='Enter region']").FillAsync("Test Region"); + await _page.Locator("input[placeholder='Enter postcode']").FillAsync("TE1 1ST"); + + await _page.GetByRole(AriaRole.Button, new() { Name = "Select country" }).ClickAsync(); + await _page.GetByRole(AriaRole.Button, new() { Name = "United Kingdom" }).ClickAsync(); + + await _page.Locator("input[placeholder='Enter contact name']").FillAsync("Test Contact"); + await _page.Locator("input[placeholder='Enter email address']").FillAsync("test.contact@example.com"); + await _page.Locator("input[placeholder='Enter phone number']").FillAsync("01234567890"); + + await _page.Locator("#createMerchantButton").ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await _page.GetByRole(AriaRole.Heading, new() { Name = "Merchant Management" }).WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + }, nameof(CreateMerchantAsync)); + } + + public async Task AssertMerchantListContainsAsync(string merchantName) + { + await RunWithFailureArtifactsAsync(async () => + { + var row = GetMerchantRow(merchantName); + await row.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await row.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantListContainsAsync)); + } + + public async Task OpenMerchantViewAsync(string merchantName) + { + await RunWithFailureArtifactsAsync(async () => + { + var row = GetMerchantRow(merchantName); + await row.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await row.GetByRole(AriaRole.Button, new() { Name = "View" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenMerchantViewAsync)); + } + + public async Task AssertMerchantViewVisibleAsync(string merchantName) + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = $"View Merchant: {merchantName}" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "View Schedule" }).IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Back to List" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantViewVisibleAsync)); + } + + public async Task SwitchMerchantTabAsync(string tabName) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = tabName }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(SwitchMerchantTabAsync)); + } + + public async Task AssertMerchantPageTextVisibleAsync(string expectedText) + { + await RunWithFailureArtifactsAsync(async () => + { + var text = _page.GetByText(expectedText); + await text.First.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await text.First.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantPageTextVisibleAsync)); + } + + public async Task OpenMerchantScheduleFromViewAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "View Schedule" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenMerchantScheduleFromViewAsync)); + } + + public async Task AssertMerchantReadOnlyScheduleVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Heading, new() { Name = "Selected Year Schedule" }).WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + _page.Url.ShouldContain("readOnly=true"); + (await _page.GetByRole(AriaRole.Heading, new() { Name = "Selected Year Schedule" }).IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("#saveScheduleButton").CountAsync()).ShouldBe(0); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Back to Merchant" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantReadOnlyScheduleVisibleAsync)); + } + + public async Task BackToMerchantFromViewScheduleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Back to Merchant" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(BackToMerchantFromViewScheduleAsync)); + } + + public async Task OpenMerchantEditAsync(string merchantName) + { + await RunWithFailureArtifactsAsync(async () => + { + var merchantId = ExtractMerchantIdFromUrl(_page.Url); + if (merchantId == Guid.Empty) + { + await _page.GotoAsync(ResolveBaseUrl() + "/merchants"); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var row = GetMerchantRow(merchantName); + await row.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await row.GetByRole(AriaRole.Button, new() { Name = "Edit" }).ClickAsync(); + } + else + { + await _page.GotoAsync($"{ResolveBaseUrl()}/merchants/{merchantId}/edit"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenMerchantEditAsync)); + } + + public async Task AssertMerchantEditVisibleAsync(string merchantName) + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = $"Edit Merchant: {merchantName}" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Edit Schedule" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantEditVisibleAsync)); + } + + public async Task AssertMerchantEditOpeningHoursVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByText("Enter merchant opening and closing times in HHmm format").WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await _page.Locator("#saveOpeningHoursButton").ScrollIntoViewIfNeededAsync(); + (await _page.Locator("#saveOpeningHoursButton").IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("#mondayOpening").IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantEditOpeningHoursVisibleAsync)); + } + + public async Task SaveMerchantOpeningHoursAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("#mondayOpening").FillAsync("0800"); + await _page.Locator("#mondayClosing").FillAsync("1700"); + await _page.Locator("#tuesdayOpening").FillAsync("0800"); + await _page.Locator("#tuesdayClosing").FillAsync("1700"); + await _page.Locator("#wednesdayOpening").FillAsync("0800"); + await _page.Locator("#wednesdayClosing").FillAsync("1700"); + await _page.Locator("#thursdayOpening").FillAsync("0800"); + await _page.Locator("#thursdayClosing").FillAsync("1700"); + await _page.Locator("#fridayOpening").FillAsync("0800"); + await _page.Locator("#fridayClosing").FillAsync("1700"); + await _page.Locator("#saturdayOpening").FillAsync("0900"); + await _page.Locator("#saturdayClosing").FillAsync("1600"); + await _page.Locator("#sundayOpening").FillAsync("1000"); + await _page.Locator("#sundayClosing").FillAsync("1500"); + + await _page.Locator("#saveOpeningHoursButton").ClickAsync(); + await _page.GetByText("Merchant opening hours updated successfully").WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + }, nameof(SaveMerchantOpeningHoursAsync)); + } + + public async Task AddMerchantOperatorAsync(string operatorName, string merchantNumber, string terminalNumber) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("#addOperatorButton").ClickAsync(); + + var option = _page.Locator("select option").Filter(new() { HasText = operatorName }); + await option.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Attached, + Timeout = 10000 + }); + + var optionValue = await option.First.GetAttributeAsync("value"); + optionValue.ShouldNotBeNull(); + + await _page.Locator("select").SelectOptionAsync(new[] { optionValue! }); + + var merchantNumberInput = _page.GetByPlaceholder("Enter merchant number"); + await merchantNumberInput.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + await merchantNumberInput.FillAsync(merchantNumber); + + var terminalNumberInput = _page.GetByPlaceholder("Enter terminal number"); + await terminalNumberInput.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + await terminalNumberInput.FillAsync(terminalNumber); + + await _page.GetByRole(AriaRole.Button, new() { Name = "Add", Exact = true }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(AddMerchantOperatorAsync)); + } + + public async Task AssertMerchantOperatorVisibleAsync(string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + var row = _page.Locator("div.flex.items-center.justify-between.p-3.bg-gray-50.rounded-lg") + .Filter(new() { HasText = operatorName }); + await row.First.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await row.First.GetByRole(AriaRole.Button, new() { Name = "Remove" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantOperatorVisibleAsync)); + } + + public async Task AssertMerchantContractsTabVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var text = _page.GetByText("No contracts assigned"); + await text.First.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await text.First.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantContractsTabVisibleAsync)); + } + + public async Task AddMerchantDeviceAsync(string deviceIdentifier) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("#addDeviceButton").ClickAsync(); + var deviceInput = _page.GetByPlaceholder("Enter device identifier"); + await deviceInput.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + await deviceInput.FillAsync(deviceIdentifier); + await deviceInput.PressAsync("Tab"); + await _page.GetByRole(AriaRole.Button, new() { Name = "Add", Exact = true }).ClickAsync(); + + var row = _page.Locator("div.flex.items-center.justify-between.p-3.bg-gray-50.rounded-lg") + .Filter(new() { HasText = deviceIdentifier }); + await row.First.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + }, nameof(AddMerchantDeviceAsync)); + } + + public async Task AssertMerchantDeviceVisibleAsync(string deviceIdentifier) + { + await RunWithFailureArtifactsAsync(async () => + { + var row = _page.Locator("div.flex.items-center.justify-between.p-3.bg-gray-50.rounded-lg") + .Filter(new() { HasText = deviceIdentifier }); + await row.First.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await row.First.GetByRole(AriaRole.Button, new() { Name = "Swap" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantDeviceVisibleAsync)); + } + + public async Task OpenMerchantScheduleFromEditAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var merchantId = ExtractMerchantIdFromUrl(_page.Url); + if (merchantId == Guid.Empty) + { + await _page.Locator("#editScheduleButton").ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + return; + } + + await _page.GotoAsync($"{ResolveBaseUrl()}/merchants/{merchantId}/schedule"); + }, nameof(OpenMerchantScheduleFromEditAsync)); + } + + public async Task AssertMerchantEditableScheduleVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var deadline = DateTime.UtcNow.AddSeconds(120); + while (DateTime.UtcNow < deadline) + { + bool saveVisible = await _page.Locator("#saveScheduleButton").IsVisibleAsync(); + bool monthVisible = await _page.Locator("#month-1-closed-days").IsVisibleAsync(); + bool backVisible = await _page.GetByRole(AriaRole.Button, new() { Name = "Back to Edit Merchant" }).IsVisibleAsync(); + + if (saveVisible && monthVisible && backVisible) + { + return; + } + + await _page.WaitForTimeoutAsync(500); + } + + throw new TimeoutException("Editable merchant schedule did not become visible within 120 seconds."); + }, nameof(AssertMerchantEditableScheduleVisibleAsync)); + } + + public async Task SaveMerchantScheduleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var selectedYear = DateTime.Today.Year + 1; + await EnsureMerchantScheduleExistsAsync(selectedYear); + await _page.Locator("#selectedYear").SelectOptionAsync(selectedYear.ToString(CultureInfo.InvariantCulture)); + await _page.Locator("#loadYearButton").ClickAsync(); + await _page.Locator("#month-1-closed-days").WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + await _page.Locator("#month-1-closed-days").FillAsync("1, 2, 15"); + await _page.Locator("#saveScheduleButton").ClickAsync(); + await _page.GetByText($"Schedule saved for {selectedYear}.").WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + }, nameof(SaveMerchantScheduleAsync)); + } + + private async Task EnsureMerchantScheduleExistsAsync(int year) + { + var merchantId = ExtractMerchantIdFromUrl(_page.Url); + if (merchantId == Guid.Empty || this.TestingContext.Estates.Count != 1) + { + return; + } + + var estateId = this.TestingContext.GetAllEstateIds().Single(); + var accessToken = this.TestingContext.AccessToken; + var client = this.TestingContext.DockerHelper.TransactionProcessorClient; + var scheduleRequest = new CreateMerchantScheduleRequest + { + Year = year, + Months = + [ + new MerchantScheduleMonthRequest + { + Month = 1, + ClosedDays = [1, 2, 15] + } + ] + }; + + var deadline = DateTime.UtcNow.AddSeconds(30); + while (DateTime.UtcNow < deadline) + { + var existing = await client.GetMerchantSchedule(accessToken, estateId, merchantId, year, CancellationToken.None); + if (existing.IsSuccess) + { + return; + } + + if (existing.Status != ResultStatus.NotFound) + { + throw new InvalidOperationException(existing.Errors.FirstOrDefault() ?? existing.Message ?? "Failed to check merchant schedule state."); + } + + var createResult = await client.CreateMerchantSchedule(accessToken, estateId, merchantId, scheduleRequest, CancellationToken.None); + if (createResult.IsFailed && createResult.Status != ResultStatus.Conflict) + { + throw new InvalidOperationException(createResult.Errors.FirstOrDefault() ?? createResult.Message ?? "Failed to create merchant schedule."); + } + + await _page.WaitForTimeoutAsync(1000); + } + + throw new TimeoutException($"Merchant schedule for {year} did not become available."); + } + + public async Task BackToMerchantFromEditScheduleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Back to Edit Merchant" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(BackToMerchantFromEditScheduleAsync)); + } + + public async Task OpenMerchantDepositAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var merchantId = ExtractMerchantIdFromUrl(_page.Url); + if (merchantId == Guid.Empty) + { + await _page.GotoAsync(ResolveBaseUrl() + "/merchants"); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var row = _page.Locator("tbody tr").First; + await row.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await row.GetByRole(AriaRole.Button, new() { Name = "Make Deposit" }).ClickAsync(); + } + else + { + await _page.GotoAsync($"{ResolveBaseUrl()}/merchants/{merchantId}/deposit"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenMerchantDepositAsync)); + } + + public async Task AssertMerchantDepositVisibleAsync(string merchantName) + { + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByRole(AriaRole.Heading, new() { Name = "Make Merchant Deposit" }).IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByText($"For merchant: {merchantName}").IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("#depositAmount").IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("#depositDate").IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("#depositReference").IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertMerchantDepositVisibleAsync)); + } + + public async Task SubmitMerchantDepositAsync(decimal amount, string reference) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("#depositAmount").FillAsync(amount.ToString(CultureInfo.InvariantCulture)); + await _page.Locator("#depositDate").FillAsync(DateTime.Today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + await _page.Locator("#depositReference").FillAsync(reference); + await _page.Locator("#makeDepositButton").ClickAsync(); + await _page.GetByText("Deposit recorded successfully").WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(SubmitMerchantDepositAsync)); + } + public async Task AssertHomePageVisibleAsync() { await RunWithFailureArtifactsAsync(async () => @@ -1003,6 +1564,19 @@ private async Task ClickFirstVisibleAsync(params string[] selectors) throw new InvalidOperationException($"Could not find a visible clickable element for selectors: {string.Join(", ", selectors)}"); } + private ILocator GetMerchantRow(string merchantName) + { + return _page.Locator("tbody tr").Filter(new() { HasText = merchantName }); + } + + private static Guid ExtractMerchantIdFromUrl(string url) + { + var match = Regex.Match(url, @"/merchants/(?[0-9a-fA-F-]+)(?:/.*)?(?:\?.*)?$", RegexOptions.IgnoreCase); + return match.Success && Guid.TryParse(match.Groups["id"].Value, out var merchantId) + ? merchantId + : Guid.Empty; + } + private async Task WaitForAuthenticationNavigationAsync() { try diff --git a/EstateManagementUI.IntegrationTests/Features/MerchantManagement.feature b/EstateManagementUI.IntegrationTests/Features/MerchantManagement.feature new file mode 100644 index 0000000..da7db56 --- /dev/null +++ b/EstateManagementUI.IntegrationTests/Features/MerchantManagement.feature @@ -0,0 +1,111 @@ +@base @background @dashboard @estate +Feature: Merchant Management + As an authenticated estate user + I want to move through the merchant management screens + So that I can create, inspect, edit, schedule, and deposit against a merchant from one journey + + Background: + Given I create the following roles + | Role Name | + | Administrator | + | Estate | + + Given I create the following api scopes + | Name | DisplayName | Description | + | transactionProcessor | Transaction Processor REST Scope | Scope for Transaction Processor REST | + | fileProcessor | File Processor REST Scope | Scope for File Processor REST | + | estateReporting | Estate Reporting REST Scope | Scope for Estate Reporting REST | + + Given I create the following api resources + | Name | DisplayName | Secret | Scopes | UserClaims | + | transactionProcessor | Transaction Processor REST | Secret1 | transactionProcessor | merchantId,estateId,role | + | fileProcessor | File Processor REST | Secret1 | fileProcessor | merchantId,estateId,role | + | estateReporting | Estate Reporting REST | Secret1 | estateReporting | merchantId,estateId,role | + + Given I create the following identity resources + | Name | DisplayName | Description | UserClaims | + | openid | Your user identifier | | sub | + | profile | User profile | Your user profile information (first name, last name, etc.) | name,role,email,given_name,middle_name,family_name,estateId,merchantId | + | email | Email | Email and Email Verified Flags | email_verified,email | + + Given I create the following clients + | ClientId | Name | Secret | Scopes | GrantTypes | RedirectUris | PostLogoutRedirectUris | RequireConsent | AllowOfflineAccess | ClientUri | + | serviceClient | Service Client | Secret1 | transactionProcessor,fileProcessor,estateReporting | client_credentials | | | | | | + | estateUIClient | Merchant Client | Secret1 | fileProcessor,transactionProcessor,estateReporting,openid,email,profile | hybrid | https://127.0.0.1:[port]/signin-oidc | https://127.0.0.1:[port]/signout-oidc | false | true | https://127.0.0.1:[port] | + + Given I create the following users + | Email Address | Phone Number | Given Name | Middle Name | Family Name | Claims | Roles | Password | + | administrator@admin.co.uk | 123456789 | Test | | User 1 | | Administrator | 123456 | + + Given I have a token to access the transaction Processor resource + | ClientId | + | serviceClient | + + Given I have created the following estates + | EstateName | + | Test Estate | + + And I have created the following operators + | EstateName | OperatorName | RequireCustomMerchantNumber | RequireCustomTerminalNumber | + | Test Estate | Test Operator | True | True | + + And I have assigned the following operators to the estates + | EstateName | OperatorName | + | Test Estate | Test Operator | + + And I have created the following security users + | EmailAddress | Password | GivenName | FamilyName | EstateName | + | estateuser@testestate1.co.uk | 123456 | TestEstate | User1 | Test Estate | + + Scenario: Estate users can complete the full merchant screen flow + Given the user navigates to the app address + And I click on the Sign In Button + Then I am presented with a login screen + When I login with the username 'estateuser@testestate1.co.uk' and password '123456' + Then I should see the dashboard heading + When I open the merchant management screen + Then I should see the merchant management heading + When I create a merchant + Then I should see the merchant in the list + When I view the merchant + Then I should see the merchant view page + When I switch to the address tab + Then I should see the merchant address details + When I switch to the contact tab + Then I should see the merchant contact details + When I switch to the opening hours tab + Then I should see the merchant opening hours details + When I switch to the merchant operators tab + Then I should see no operators assigned + When I switch to the contracts tab + Then I should see no contracts assigned + When I switch to the devices tab + Then I should see no devices assigned + When I open the merchant schedule from the view page + Then I should see the read-only merchant schedule page + When I return to the merchant view page + Then I should see the merchant view page + When I open the merchant edit page + Then I should see the merchant edit page + When I switch to the opening hours editor + Then I should see the merchant opening hours editor + When I save merchant opening hours + Then I should see merchant opening hours updated successfully + When I switch to the merchant operators editor + And I add the operator to the merchant + Then I should see the operator in the merchant list + When I switch to the contracts editor + Then I should see no contracts assigned + When I switch to the devices editor + And I add the device to the merchant + Then I should see the device in the merchant list + When I open the merchant schedule from the edit page + Then I should see the editable merchant schedule page + When I save the merchant schedule + Then I should see schedule saved successfully + When I return to the merchant edit page + Then I should see the merchant edit page + When I open the merchant deposit page + Then I should see the merchant deposit page + When I submit the merchant deposit + Then I should be back on the merchant list diff --git a/EstateManagementUI.IntegrationTests/Hooks/BrowserHooks.cs b/EstateManagementUI.IntegrationTests/Hooks/BrowserHooks.cs index d22cab7..6f96bff 100644 --- a/EstateManagementUI.IntegrationTests/Hooks/BrowserHooks.cs +++ b/EstateManagementUI.IntegrationTests/Hooks/BrowserHooks.cs @@ -22,18 +22,11 @@ public BrowserHooks(ScenarioContext scenarioContext) } /// - /// Install Playwright browsers and initialize Playwright before running any tests + /// Initialize Playwright before running any tests /// [BeforeTestRun] public static async Task BeforeTestRun() { - // Install Playwright browsers if needed - var exitCode = Microsoft.Playwright.Program.Main(new[] { "install" }); - if (exitCode != 0) - { - throw new Exception($"Playwright installation failed with exit code {exitCode}"); - } - // Initialize Playwright _playwright = await Playwright.CreateAsync(); } @@ -175,6 +168,16 @@ private async Task CreateBrowserPage() Console.WriteLine($"[browser setup] SecurityServiceContainerName={securityServiceHost ?? ""}"); Console.WriteLine($"[browser setup] SecurityServicePort={securityServicePortText ?? ""}"); + var chromiumExecutablePath = ResolveChromiumExecutablePath(); + if (!string.IsNullOrWhiteSpace(chromiumExecutablePath)) + { + Console.WriteLine($"[browser setup] Chromium executable: {chromiumExecutablePath}"); + } + else + { + Console.WriteLine("[browser setup] No system Chromium/Edge executable found; using bundled Playwright Chromium."); + } + _browser = browserType switch { "Firefox" => await _playwright!.Firefox.LaunchAsync(new BrowserTypeLaunchOptions @@ -194,6 +197,7 @@ private async Task CreateBrowserPage() _ => await _playwright!.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = isCI, + ExecutablePath = string.IsNullOrWhiteSpace(chromiumExecutablePath) ? null : chromiumExecutablePath, Args = hostResolverRules is null ? new[] { @@ -235,4 +239,32 @@ await _browserContext.Tracing.StartAsync(new TracingStartOptions return page; } + + private static string? ResolveChromiumExecutablePath() + { + var candidatePaths = new[] + { + @"/usr/bin/google-chrome", + @"/usr/bin/google-chrome-stable", + @"/usr/bin/chromium", + @"/usr/bin/chromium-browser", + @"/usr/bin/microsoft-edge", + @"/snap/bin/google-chrome", + @"/snap/bin/chromium", + @"C:\Program Files\Google\Chrome\Application\chrome.exe", + @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", + @"C:\Program Files\Microsoft\Edge\Application\msedge.exe", + @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", + }; + + foreach (var candidatePath in candidatePaths) + { + if (File.Exists(candidatePath)) + { + return candidatePath; + } + } + + return null; + } } diff --git a/EstateManagementUI.IntegrationTests/Steps/MerchantManagementSteps.cs b/EstateManagementUI.IntegrationTests/Steps/MerchantManagementSteps.cs new file mode 100644 index 0000000..9830ad7 --- /dev/null +++ b/EstateManagementUI.IntegrationTests/Steps/MerchantManagementSteps.cs @@ -0,0 +1,158 @@ +using EstateManagementUI.IntegrationTests.Common; +using Microsoft.Playwright; +using Reqnroll; + +namespace EstateManagementUI.IntegrationTests.Steps; + +[Binding] +[Scope(Tag = "estate")] +public sealed class MerchantManagementSteps +{ + private const string OperatorName = "Test Operator"; + private const string OperatorMerchantNumber = "12345678"; + private const string OperatorTerminalNumber = "87654321"; + private const string DepositReference = "DEP-001"; + private const decimal DepositAmount = 100m; + + private readonly IPage _page; + private readonly TestingContext _testingContext; + private readonly string _merchantName = $"Integration Merchant {Guid.NewGuid():N}"; + private readonly string _deviceIdentifier = $"DEVICE-{Guid.NewGuid():N}"; + + public MerchantManagementSteps(IPage page, TestingContext testingContext) + { + _page = page; + _testingContext = testingContext; + } + + [When("I open the merchant management screen")] + public Task WhenIOpenTheMerchantManagementScreen() => GetHelper().OpenMerchantManagementScreenAsync(); + + [Then("I should see the merchant management heading")] + public Task ThenIShouldSeeTheMerchantManagementHeading() => GetHelper().AssertMerchantManagementHeadingVisibleAsync(); + + [When("I create a merchant")] + public Task WhenICreateAMerchant() => GetHelper().CreateMerchantAsync(_merchantName); + + [Then("I should see the merchant in the list")] + public Task ThenIShouldSeeTheMerchantInTheList() => GetHelper().AssertMerchantListContainsAsync(_merchantName); + + [When("I view the merchant")] + public Task WhenIViewTheMerchant() => GetHelper().OpenMerchantViewAsync(_merchantName); + + [Then("I should see the merchant view page")] + public Task ThenIShouldSeeTheMerchantViewPage() => GetHelper().AssertMerchantViewVisibleAsync(_merchantName); + + [When("I switch to the address tab")] + public Task WhenISwitchToTheAddressTab() => GetHelper().SwitchMerchantTabAsync("Address Details"); + + [Then("I should see the merchant address details")] + public Task ThenIShouldSeeTheMerchantAddressDetails() => GetHelper().AssertMerchantPageTextVisibleAsync("Address Line 1"); + + [When("I switch to the contact tab")] + public Task WhenISwitchToTheContactTab() => GetHelper().SwitchMerchantTabAsync("Contact Details"); + + [Then("I should see the merchant contact details")] + public Task ThenIShouldSeeTheMerchantContactDetails() => GetHelper().AssertMerchantPageTextVisibleAsync("Contact Name"); + + [When("I switch to the opening hours tab")] + public Task WhenISwitchToTheOpeningHoursTab() => GetHelper().SwitchMerchantTabAsync("Opening Hours"); + + [Then("I should see the merchant opening hours details")] + public Task ThenIShouldSeeTheMerchantOpeningHoursDetails() => GetHelper().AssertMerchantPageTextVisibleAsync("Monday"); + + [When("I switch to the merchant operators tab")] + public Task WhenISwitchToTheOperatorsTab() => GetHelper().SwitchMerchantTabAsync("Assigned Operators"); + + [Then("I should see no operators assigned")] + public Task ThenIShouldSeeNoOperatorsAssigned() => GetHelper().AssertMerchantPageTextVisibleAsync("No operators assigned"); + + [When("I switch to the contracts tab")] + public Task WhenISwitchToTheContractsTab() => GetHelper().SwitchMerchantTabAsync("Assigned Contracts"); + + [Then("I should see no contracts assigned")] + public Task ThenIShouldSeeNoContractsAssigned() => GetHelper().AssertMerchantContractsTabVisibleAsync(); + + [When("I switch to the devices tab")] + public Task WhenISwitchToTheDevicesTab() => GetHelper().SwitchMerchantTabAsync("Assigned Devices"); + + [Then("I should see no devices assigned")] + public Task ThenIShouldSeeNoDevicesAssigned() => GetHelper().AssertMerchantPageTextVisibleAsync("No devices assigned"); + + [When("I open the merchant schedule from the view page")] + public Task WhenIOpenTheMerchantScheduleFromTheViewPage() => GetHelper().OpenMerchantScheduleFromViewAsync(); + + [Then("I should see the read-only merchant schedule page")] + public Task ThenIShouldSeeTheReadOnlyMerchantSchedulePage() => GetHelper().AssertMerchantReadOnlyScheduleVisibleAsync(); + + [When("I return to the merchant view page")] + public Task WhenIReturnToTheMerchantViewPage() => GetHelper().BackToMerchantFromViewScheduleAsync(); + + [When("I open the merchant edit page")] + public Task WhenIOpenTheMerchantEditPage() => GetHelper().OpenMerchantEditAsync(_merchantName); + + [Then("I should see the merchant edit page")] + public Task ThenIShouldSeeTheMerchantEditPage() => GetHelper().AssertMerchantEditVisibleAsync(_merchantName); + + [When("I switch to the opening hours editor")] + public Task WhenISwitchToTheOpeningHoursEditor() => GetHelper().SwitchMerchantTabAsync("Opening Hours"); + + [Then("I should see the merchant opening hours editor")] + public Task ThenIShouldSeeTheMerchantOpeningHoursEditor() => GetHelper().AssertMerchantEditOpeningHoursVisibleAsync(); + + [When("I save merchant opening hours")] + public Task WhenISaveMerchantOpeningHours() => GetHelper().SaveMerchantOpeningHoursAsync(); + + [Then("I should see merchant opening hours updated successfully")] + public Task ThenIShouldSeeMerchantOpeningHoursUpdatedSuccessfully() => GetHelper().AssertMerchantPageTextVisibleAsync("Merchant opening hours updated successfully"); + + [When("I switch to the merchant operators editor")] + public Task WhenISwitchToTheOperatorsEditor() => GetHelper().SwitchMerchantTabAsync("Assigned Operators"); + + [When("I add the operator to the merchant")] + public Task WhenIAddTheOperatorToTheMerchant() => GetHelper().AddMerchantOperatorAsync(OperatorName, OperatorMerchantNumber, OperatorTerminalNumber); + + [Then("I should see the operator in the merchant list")] + public Task ThenIShouldSeeTheOperatorInTheMerchantList() => GetHelper().AssertMerchantOperatorVisibleAsync(OperatorName); + + [When("I switch to the contracts editor")] + public Task WhenISwitchToTheContractsEditor() => GetHelper().SwitchMerchantTabAsync("Assigned Contracts"); + + [When("I switch to the devices editor")] + public Task WhenISwitchToTheDevicesEditor() => GetHelper().SwitchMerchantTabAsync("Assigned Devices"); + + [When("I add the device to the merchant")] + public Task WhenIAddTheDeviceToTheMerchant() => GetHelper().AddMerchantDeviceAsync(_deviceIdentifier); + + [Then("I should see the device in the merchant list")] + public Task ThenIShouldSeeTheDeviceInTheMerchantList() => GetHelper().AssertMerchantDeviceVisibleAsync(_deviceIdentifier); + + [When("I open the merchant schedule from the edit page")] + public Task WhenIOpenTheMerchantScheduleFromTheEditPage() => GetHelper().OpenMerchantScheduleFromEditAsync(); + + [Then("I should see the editable merchant schedule page")] + public Task ThenIShouldSeeTheEditableMerchantSchedulePage() => GetHelper().AssertMerchantEditableScheduleVisibleAsync(); + + [When("I save the merchant schedule")] + public Task WhenISaveTheMerchantSchedule() => GetHelper().SaveMerchantScheduleAsync(); + + [Then("I should see schedule saved successfully")] + public Task ThenIShouldSeeScheduleSavedSuccessfully() => GetHelper().AssertMerchantPageTextVisibleAsync("Schedule saved for"); + + [When("I return to the merchant edit page")] + public Task WhenIReturnToTheMerchantEditPage() => GetHelper().BackToMerchantFromEditScheduleAsync(); + + [When("I open the merchant deposit page")] + public Task WhenIOpenTheMerchantDepositPage() => GetHelper().OpenMerchantDepositAsync(); + + [Then("I should see the merchant deposit page")] + public Task ThenIShouldSeeTheMerchantDepositPage() => GetHelper().AssertMerchantDepositVisibleAsync(_merchantName); + + [When("I submit the merchant deposit")] + public Task WhenISubmitTheMerchantDeposit() => GetHelper().SubmitMerchantDepositAsync(DepositAmount, DepositReference); + + [Then("I should be back on the merchant list")] + public Task ThenIShouldBeBackOnTheMerchantList() => GetHelper().AssertMerchantListContainsAsync(_merchantName); + + private DashboardPageHelper GetHelper() => new(_page, _testingContext); +} diff --git a/docs/superpowers/plans/2026-06-23-merchant-management-reqnroll-tests.md b/docs/superpowers/plans/2026-06-23-merchant-management-reqnroll-tests.md new file mode 100644 index 0000000..f66956c --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-merchant-management-reqnroll-tests.md @@ -0,0 +1,197 @@ +# Merchant Management Reqnroll Tests Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a single end-to-end Reqnroll scenario that walks through the merchant UI from list to create, view, edit, schedule, and deposit pages. + +**Architecture:** Extend the shared Playwright helper with merchant-specific navigation and assertion methods so the feature file stays readable. Keep the scenario focused on route transitions and stable UI markers rather than duplicating the full page-test matrix. + +**Tech Stack:** Reqnroll, Playwright, NUnit, C#, Blazor Server UI + +--- + +### Task 1: Add merchant journey helpers to the shared integration helper + +**Files:** +- Modify: `EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs` + +- [ ] **Step 1: Add merchant navigation and assertion methods** + +Add methods that match the existing contract/operator helper style: + +```csharp +public async Task OpenMerchantManagementScreenAsync() +public async Task AssertMerchantManagementHeadingVisibleAsync() +public async Task CreateMerchantAsync(string merchantName) +public async Task AssertMerchantListContainsAsync(string merchantName) +public async Task OpenMerchantViewAsync(string merchantName) +public async Task AssertMerchantViewVisibleAsync(string merchantName) +public async Task OpenMerchantEditAsync(string merchantName) +public async Task AssertMerchantEditVisibleAsync(string merchantName) +public async Task OpenMerchantScheduleFromViewAsync() +public async Task AssertMerchantScheduleVisibleAsync() +public async Task OpenMerchantScheduleFromEditAsync() +public async Task OpenMerchantDepositAsync(string merchantName) +public async Task AssertMerchantDepositVisibleAsync(string merchantName) +public async Task AddMerchantOpeningHoursAsync() +public async Task SubmitMerchantDepositAsync(decimal amount, string reference) +public async Task BackToMerchantListAsync() +``` + +Use the existing merchant route IDs and labels already present in the Blazor pages: + +```text +#createMerchantButton +#viewMerchantLink +#editMerchantLink +#viewScheduleButton +#editScheduleButton +#makeDepositLink +``` + +- [ ] **Step 2: Reuse the same failure-artifact wrapper and selectors** + +Keep the helper methods wrapped in `RunWithFailureArtifactsAsync(...)`, and follow the current pattern of waiting for `LoadState.NetworkIdle` after navigation/clicks. + +- [ ] **Step 3: Verify the helper compiles cleanly** + +Run: + +```powershell +dotnet build EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj +``` + +Expected: build succeeds with the new helper methods referenced by the step definitions. + +### Task 2: Add the merchant feature and step definitions + +**Files:** +- Create: `EstateManagementUI.IntegrationTests/Features/MerchantManagement.feature` +- Create: `EstateManagementUI.IntegrationTests/Steps/MerchantManagementSteps.cs` + +- [ ] **Step 1: Write the failing Reqnroll scenario** + +Add one scenario that exercises the merchant journey end to end: + +```gherkin +@base @background @estate +Feature: Merchant Management + As an authenticated estate user + I want to move through the merchant management screens + So that I can create, inspect, edit, schedule, and deposit against a merchant from one journey + + Background: + Given I create the following roles + | Role Name | + | Administrator | + | Estate | + Given I create the following api scopes + | Name | DisplayName | Description | + | transactionProcessor | Transaction Processor REST Scope | Scope for Transaction Processor REST | + | fileProcessor | File Processor REST Scope | Scope for File Processor REST | + | estateReporting | Estate Reporting REST Scope | Scope for Estate Reporting REST | + Given I create the following api resources + | Name | DisplayName | Secret | Scopes | UserClaims | + | transactionProcessor | Transaction Processor REST | Secret1 | transactionProcessor | merchantId,estateId,role | + | fileProcessor | File Processor REST | Secret1 | fileProcessor | merchantId,estateId,role | + | estateReporting | Estate Reporting REST | Secret1 | estateReporting | merchantId,estateId,role | + Given I create the following identity resources + | Name | DisplayName | Description | UserClaims | + | openid | Your user identifier | | sub | + | profile | User profile | Your user profile information (first name, last name, etc.) | name,role,email,given_name,middle_name,family_name,estateId,merchantId | + | email | Email | Email and Email Verified Flags | email_verified,email | + Given I create the following clients + | ClientId | Name | Secret | Scopes | GrantTypes | RedirectUris | PostLogoutRedirectUris | RequireConsent | AllowOfflineAccess | ClientUri | + | serviceClient | Service Client | Secret1 | transactionProcessor,fileProcessor,estateReporting | client_credentials | | | | | | + | estateUIClient | Merchant Client | Secret1 | fileProcessor,transactionProcessor,estateReporting,openid,email,profile | hybrid | https://127.0.0.1:[port]/signin-oidc | https://127.0.0.1:[port]/signout-oidc | false | true | https://127.0.0.1:[port] | + Given I create the following users + | Email Address | Phone Number | Given Name | Middle Name | Family Name | Claims | Roles | Password | + | administrator@admin.co.uk | 123456789 | Test | | User 1 | | Administrator | 123456 | + Given I have a token to access the transaction Processor resource + | ClientId | + | serviceClient | + Given I have created the following estates + | EstateName | + | Test Estate | + And I have created the following operators + | EstateName | OperatorName | RequireCustomMerchantNumber | RequireCustomTerminalNumber | + | Test Estate | Test Operator | True | True | + And I have created the following contracts + | EstateName | OperatorName | ContractName | + | Test Estate | Test Operator | Test Merchant Contract | + And I have created the following security users + | EmailAddress | Password | GivenName | FamilyName | EstateName | + | estateuser@testestate1.co.uk | 123456 | TestEstate | User1 | Test Estate | + + Scenario: Estate users can complete the full merchant screen flow + Given the user navigates to the app address + And I click on the Sign In Button + Then I am presented with a login screen + When I login with the username 'estateuser@testestate1.co.uk' and password '123456' + Then I should see the dashboard heading + When I open the merchant management screen + Then I should see the merchant management heading + When I create a merchant + Then I should see the merchant in the list + When I view the merchant + Then I should see the merchant details page + When I open the merchant schedule from the view page + Then I should see the merchant schedule page + When I return to the merchant view page + Then I should see the merchant details page + When I edit the merchant + Then I should see the merchant edit page + When I open the merchant schedule from the edit page + Then I should see the merchant schedule page + When I return to the merchant edit page + Then I should see the merchant edit page + When I make a merchant deposit + Then I should see the merchant deposit page + When I submit the merchant deposit + Then I should be back on the merchant list +``` + +- [ ] **Step 2: Bind the steps to the shared helper** + +Create a merchant-specific binding class that calls the helper methods and keeps the scenario text short and readable. + +- [ ] **Step 3: Generate and compile the new feature** + +Run: + +```powershell +dotnet build EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj +``` + +Expected: Reqnroll generates the feature code-behind and the new step class compiles. + +### Task 3: Run the integration tests and tighten any selectors + +**Files:** +- Modify: `EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs` +- Modify: `EstateManagementUI.IntegrationTests/Steps/MerchantManagementSteps.cs` + +- [ ] **Step 1: Run the merchant integration tests** + +Run: + +```powershell +dotnet test EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj --filter MerchantManagement +``` + +Expected: the new scenario passes end to end. + +- [ ] **Step 2: Fix any route or selector drift** + +If a page renders slightly different text or uses a different button label, update the helper method rather than the scenario text so the feature stays readable. + +- [ ] **Step 3: Re-run the targeted test and the integration suite** + +Run: + +```powershell +dotnet test EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj --filter MerchantManagement +dotnet test EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj +``` + +Expected: the merchant scenario passes and the broader integration suite stays green.