diff --git a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs index c883bc4..8474069 100644 --- a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs +++ b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Globalization; using Microsoft.Playwright; using Shouldly; using System.Text.Json; @@ -470,6 +471,245 @@ await operatorRow.WaitForAsync(new LocatorWaitForOptions }, nameof(AssertAssignedOperatorNotVisibleAsync)); } + public async Task OpenContractManagementScreenAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var contractsLink = _page.Locator("#contractsLink"); + if (await contractsLink.CountAsync() > 0 && await contractsLink.First.IsVisibleAsync()) + { + await contractsLink.First.ClickAsync(new LocatorClickOptions { NoWaitAfter = true }); + } + else + { + await _page.GotoAsync(ResolveBaseUrl() + "/contracts"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenContractManagementScreenAsync)); + } + + public async Task AssertContractManagementHeadingVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = "Contract Management" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractManagementHeadingVisibleAsync)); + } + + public async Task CreateContractAsync(string contractDescription, string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("#newContractButton").ClickAsync(new LocatorClickOptions { NoWaitAfter = true }); + await _page.WaitForURLAsync(new Regex(@".*/contracts/new.*", RegexOptions.IgnoreCase)); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + await _page.Locator("input[placeholder='Enter contract description']").FillAsync(contractDescription); + + var operatorOption = _page.Locator("select option").Filter(new() { HasText = operatorName }); + await operatorOption.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Attached, + Timeout = 10000 + }); + + var operatorValue = await operatorOption.First.GetAttributeAsync("value"); + operatorValue.ShouldNotBeNull(); + + await _page.Locator("select").SelectOptionAsync(new[] { operatorValue! }); + await _page.Locator("#createContractButton").ClickAsync(); + await _page.WaitForURLAsync(new Regex(@".*/contracts.*", RegexOptions.IgnoreCase)); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(CreateContractAsync)); + } + + public async Task AssertContractListContainsAsync(string contractDescription) + { + await RunWithFailureArtifactsAsync(async () => + { + var contractCard = GetContractCard(contractDescription); + await contractCard.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await contractCard.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractListContainsAsync)); + } + + public async Task OpenContractViewAsync(string contractDescription) + { + await RunWithFailureArtifactsAsync(async () => + { + var contractCard = GetContractCard(contractDescription); + await contractCard.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await contractCard.GetByRole(AriaRole.Button, new() { Name = "View" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenContractViewAsync)); + } + + public async Task AssertContractViewVisibleAsync(string contractDescription) + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = $"View Contract: {contractDescription}" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Heading, new() { Name = "Contract Details" }).IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Back to List" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractViewVisibleAsync)); + } + + public async Task OpenContractEditAsync(string contractDescription) + { + await RunWithFailureArtifactsAsync(async () => + { + var currentContractId = ExtractContractIdFromUrl(_page.Url); + if (currentContractId == Guid.Empty) + { + await _page.GotoAsync(ResolveBaseUrl() + "/contracts"); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var contractCard = GetContractCard(contractDescription); + await contractCard.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await contractCard.GetByRole(AriaRole.Button, new() { Name = "Edit" }).ClickAsync(); + } + else + { + await _page.GotoAsync($"{ResolveBaseUrl()}/contracts/{currentContractId}/edit"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenContractEditAsync)); + } + + public async Task AssertContractEditVisibleAsync(string contractDescription) + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = $"Edit Contract: {contractDescription}" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Add Product" }).First.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractEditVisibleAsync)); + } + + public async Task AddProductToContractAsync(string productName, string displayText, decimal value) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Add Product" }).First.ClickAsync(); + + var modal = _page.Locator("div.fixed.inset-0").Filter(new() { HasText = "Add New Product" }); + await modal.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await modal.Locator("input[placeholder='Enter product name']").FillAsync(productName); + await modal.Locator("input[placeholder='Enter display text']").FillAsync(displayText); + await modal.Locator("input[placeholder='Enter value']").FillAsync(value.ToString(CultureInfo.InvariantCulture)); + await modal.GetByRole(AriaRole.Button, new() { Name = "Add Product" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var productCard = GetContractProductCard(productName); + await productCard.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + }, nameof(AddProductToContractAsync)); + } + + public async Task AssertContractProductVisibleAsync(string productName) + { + await RunWithFailureArtifactsAsync(async () => + { + var productCard = GetContractProductCard(productName); + await productCard.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await productCard.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractProductVisibleAsync)); + } + + public async Task AddFeeToContractAsync(string feeDescription, decimal feeValue) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Add Fee" }).First.ClickAsync(); + + var modal = _page.Locator("div.fixed.inset-0").Filter(new() { HasText = "Add Transaction Fee" }); + await modal.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await modal.Locator("input[placeholder='Enter fee description']").FillAsync(feeDescription); + await modal.Locator("select").Nth(0).SelectOptionAsync("0"); + await modal.Locator("select").Nth(1).SelectOptionAsync("0"); + await modal.Locator("input[placeholder='Enter fee value']").FillAsync(feeValue.ToString(CultureInfo.InvariantCulture)); + await modal.GetByRole(AriaRole.Button, new() { Name = "Add Fee" }).ClickAsync(); + + await _page.GetByText(feeDescription).WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + }, nameof(AddFeeToContractAsync)); + } + + public async Task AssertContractFeeVisibleAsync(string feeDescription) + { + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByText(feeDescription).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractFeeVisibleAsync)); + } + + public async Task BackToContractListAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Back to List" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(BackToContractListAsync)); + } + public async Task AssertHomePageVisibleAsync() { await RunWithFailureArtifactsAsync(async () => @@ -687,6 +927,30 @@ private ILocator GetOperatorRow(string operatorName) return _page.Locator("tbody tr").Filter(new() { HasText = operatorName }); } + private ILocator GetContractCard(string contractDescription) + { + return _page.Locator("div.bg-white.rounded-lg.shadow-md.p-6").Filter(new() + { + Has = _page.GetByRole(AriaRole.Heading, new() { Name = contractDescription }) + }); + } + + private ILocator GetContractProductCard(string productName) + { + return _page.Locator("div.border.border-gray-200.rounded-lg.p-4").Filter(new() + { + Has = _page.GetByRole(AriaRole.Heading, new() { Name = productName }) + }); + } + + private static Guid ExtractContractIdFromUrl(string url) + { + var match = Regex.Match(url, @"/contracts/(?[0-9a-fA-F-]+)(?:/edit)?(?:\?.*)?$", RegexOptions.IgnoreCase); + return match.Success && Guid.TryParse(match.Groups["id"].Value, out var contractId) + ? contractId + : Guid.Empty; + } + private async Task IsAnyVisibleAsync(params string[] selectors) { foreach (var selector in selectors) diff --git a/EstateManagementUI.IntegrationTests/Features/ContractManagement.feature b/EstateManagementUI.IntegrationTests/Features/ContractManagement.feature new file mode 100644 index 0000000..ba1ead5 --- /dev/null +++ b/EstateManagementUI.IntegrationTests/Features/ContractManagement.feature @@ -0,0 +1,79 @@ +@base @background @dashboard @estate +Feature: Contract Management + As an authenticated estate user + I want to move through the contract screens + So that I can create, inspect, and edit a contract 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 contract 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 contract management screen + Then I should see the contract management heading + When I create a contract + Then I should see the contract in the list + When I view the contract + Then I should see the contract details page + When I edit the contract + Then I should see the contract edit page + When I add a product to the contract + Then I should see the product in the contract edit view + When I add a fee to the contract + Then I should see the fee in the contract edit view + When I return to the contract list + Then I should see the contract management heading diff --git a/EstateManagementUI.IntegrationTests/Steps/ContractManagementSteps.cs b/EstateManagementUI.IntegrationTests/Steps/ContractManagementSteps.cs new file mode 100644 index 0000000..170465a --- /dev/null +++ b/EstateManagementUI.IntegrationTests/Steps/ContractManagementSteps.cs @@ -0,0 +1,65 @@ +using EstateManagementUI.IntegrationTests.Common; +using Microsoft.Playwright; +using Reqnroll; + +namespace EstateManagementUI.IntegrationTests.Steps; + +[Binding] +[Scope(Tag = "estate")] +public sealed class ContractManagementSteps +{ + private const string ContractDescription = "Integration Contract"; + private const string ProductName = "Integration Product"; + private const string ProductDisplayText = "Integration Product Display"; + private const string FeeDescription = "Integration Fee"; + + private readonly IPage _page; + private readonly TestingContext _testingContext; + + public ContractManagementSteps(IPage page, TestingContext testingContext) + { + _page = page; + _testingContext = testingContext; + } + + [When("I open the contract management screen")] + public Task WhenIOpenTheContractManagementScreen() => GetHelper().OpenContractManagementScreenAsync(); + + [Then("I should see the contract management heading")] + public Task ThenIShouldSeeTheContractManagementHeading() => GetHelper().AssertContractManagementHeadingVisibleAsync(); + + [When("I create a contract")] + public Task WhenICreateAContract() => GetHelper().CreateContractAsync(ContractDescription, "Test Operator"); + + [Then("I should see the contract in the list")] + public Task ThenIShouldSeeTheContractInTheList() => GetHelper().AssertContractListContainsAsync(ContractDescription); + + [When("I view the contract")] + public Task WhenIViewTheContract() => GetHelper().OpenContractViewAsync(ContractDescription); + + [Then("I should see the contract details page")] + public Task ThenIShouldSeeTheContractDetailsPage() => GetHelper().AssertContractViewVisibleAsync(ContractDescription); + + [When("I edit the contract")] + public Task WhenIEditTheContract() => GetHelper().OpenContractEditAsync(ContractDescription); + + [Then("I should see the contract edit page")] + public Task ThenIShouldSeeTheContractEditPage() => GetHelper().AssertContractEditVisibleAsync(ContractDescription); + + [When("I add a product to the contract")] + public Task WhenIAddAProductToTheContract() => GetHelper().AddProductToContractAsync(ProductName, ProductDisplayText, 10m); + + [Then("I should see the product in the contract edit view")] + public Task ThenIShouldSeeTheProductInTheContractEditView() => GetHelper().AssertContractProductVisibleAsync(ProductName); + + [When("I add a fee to the contract")] + public Task WhenIAddAFeeToTheContract() => GetHelper().AddFeeToContractAsync(FeeDescription, 1.50m); + + [Then("I should see the fee in the contract edit view")] + public Task ThenIShouldSeeTheFeeInTheContractEditView() => GetHelper().AssertContractFeeVisibleAsync(FeeDescription); + + [When("I return to the contract list")] + public Task WhenIReturnToTheContractList() => GetHelper().BackToContractListAsync(); + + private DashboardPageHelper GetHelper() => new(_page, _testingContext); +} diff --git a/docs/superpowers/plans/2026-06-22-contract-screen-integration-test.md b/docs/superpowers/plans/2026-06-22-contract-screen-integration-test.md new file mode 100644 index 0000000..1f4fdbc --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-contract-screen-integration-test.md @@ -0,0 +1,349 @@ +# Contract Screen Integration Test 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 one ReqNroll integration scenario that covers the contract list, create, view, edit, product, and fee flows end-to-end. + +**Architecture:** Reuse the existing Docker-backed integration harness and the same estate-user background used by the operator scenario. Add one contract feature file, one scoped step-definition class, and a small set of helper methods on the shared `DashboardPageHelper` so the scenario reads like a single user journey while all browser logic stays centralized. + +**Tech Stack:** ReqNroll, NUnit, Playwright, Docker-backed integration containers, shared integration test helpers. + +--- + +### Task 1: Add the contract feature scenario + +**Files:** +- Create: `EstateManagementUI.IntegrationTests/Features/ContractManagement.feature` + +- [ ] **Step 1: Write the failing feature** + +```gherkin +@base @background @estate +Feature: Contract Management + As an authenticated estate user + I want to move through the contract screens + So that I can create, inspect, and edit a contract from one journey + + Scenario: Estate users can complete the full contract 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 estate info page + When I open the contract management screen + Then I should see the contract management heading + When I create a contract + Then I should see the contract in the list + When I view the contract + Then I should see the contract details page + When I edit the contract + Then I should see the contract edit page + When I add a product to the contract + Then I should see the product in the contract edit view + When I add a fee to the contract + Then I should see the fee in the contract edit view + When I return to the contract list + Then I should see the contract management heading +``` + +- [ ] **Step 2: Run the feature to confirm the new scenario does not bind yet** + +Run: +`dotnet test EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj --filter FullyQualifiedName~EstateManagementUI.IntegrationTests.Features.ContractManagement` + +Expected: fail because the new contract step definitions do not exist yet. + +- [ ] **Step 3: Keep the scenario self-contained** + +Use one generated contract name during the run so the later list/view/edit steps all target the same record without relying on pre-seeded contract data. + +### Task 2: Expand the shared Playwright helper for contract screens + +**Files:** +- Modify: `EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs` + +- [ ] **Step 1: Add contract navigation and assertion helpers** + +```csharp +public async Task OpenContractManagementScreenAsync() +{ + await RunWithFailureArtifactsAsync(async () => + { + var contractLink = _page.Locator("#contractsLink"); + if (await contractLink.CountAsync() > 0 && await contractLink.First.IsVisibleAsync()) + { + await contractLink.First.ClickAsync(new LocatorClickOptions { NoWaitAfter = true }); + } + else + { + await _page.GotoAsync(ResolveBaseUrl() + "/contracts"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenContractManagementScreenAsync)); +} + +public async Task AssertContractManagementHeadingVisibleAsync() +{ + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByRole(AriaRole.Heading, new() { Name = "Contract Management" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractManagementHeadingVisibleAsync)); +} + +public async Task CreateContractAsync(string contractDescription, string operatorName) +{ + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("#newContractButton").ClickAsync(); + await _page.WaitForURLAsync(new Regex(@".*/contracts/new.*", RegexOptions.IgnoreCase)); + await _page.Locator("input[placeholder='Enter contract description']").FillAsync(contractDescription); + + var operatorOption = _page.Locator("select option").Filter(new() { HasText = operatorName }); + await operatorOption.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Attached, + Timeout = 10000 + }); + + var operatorValue = await operatorOption.First.GetAttributeAsync("value"); + operatorValue.ShouldNotBeNull(); + + await _page.Locator("select").SelectOptionAsync(new[] { operatorValue! }); + await _page.Locator("#createContractButton").ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(CreateContractAsync)); +} + +public async Task AssertContractListContainsAsync(string contractDescription) +{ + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByText(contractDescription).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractListContainsAsync)); +} + +public async Task OpenContractViewAsync(string contractDescription) +{ + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByText(contractDescription).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenContractViewAsync)); +} + +public async Task AssertContractViewVisibleAsync(string contractDescription) +{ + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByRole(AriaRole.Heading, new() { Name = $"View Contract: {contractDescription}" }).IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Back to List" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractViewVisibleAsync)); +} +``` + +- [ ] **Step 2: Add contract edit helpers for the product and fee flows** + +```csharp +public async Task OpenContractEditAsync(string contractDescription) +{ + await RunWithFailureArtifactsAsync(async () => + { + var editButton = _page.GetByRole(AriaRole.Button, new() { Name = "Edit" }).First; + await editButton.ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenContractEditAsync)); +} + +public async Task AssertContractEditVisibleAsync(string contractDescription) +{ + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByRole(AriaRole.Heading, new() { Name = $"Edit Contract: {contractDescription}" }).IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Add Product" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractEditVisibleAsync)); +} + +public async Task AddProductToContractAsync(string productName, string displayText, decimal value) +{ + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Add Product" }).ClickAsync(); + await _page.GetByRole(AriaRole.Heading, new() { Name = "Add New Product" }).WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await _page.Locator("input[placeholder='Enter product name']").FillAsync(productName); + await _page.Locator("input[placeholder='Enter display text']").FillAsync(displayText); + await _page.Locator("input[placeholder='Enter value']").FillAsync(value.ToString("0.##", CultureInfo.InvariantCulture)); + await _page.GetByRole(AriaRole.Button, new() { Name = "Add Product" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(AddProductToContractAsync)); +} + +public async Task AssertContractProductVisibleAsync(string productName) +{ + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByText(productName).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractProductVisibleAsync)); +} + +public async Task AddFeeToContractAsync(string feeDescription, decimal feeValue) +{ + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Add Fee" }).First.ClickAsync(); + await _page.GetByRole(AriaRole.Heading, new() { Name = "Add Transaction Fee" }).WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await _page.Locator("input[placeholder='Enter fee description']").FillAsync(feeDescription); + await _page.Locator("select").Nth(0).SelectOptionAsync("0"); + await _page.Locator("select").Nth(1).SelectOptionAsync("0"); + await _page.Locator("input[placeholder='Enter fee value']").FillAsync(feeValue.ToString("0.##", CultureInfo.InvariantCulture)); + await _page.GetByRole(AriaRole.Button, new() { Name = "Add Fee" }).First.ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(AddFeeToContractAsync)); +} + +public async Task AssertContractFeeVisibleAsync(string feeDescription) +{ + await RunWithFailureArtifactsAsync(async () => + { + (await _page.GetByText(feeDescription).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertContractFeeVisibleAsync)); +} + +public async Task BackToContractListAsync() +{ + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Back to List" }).First.ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(BackToContractListAsync)); +} +``` + +- [ ] **Step 3: Run the existing helper-backed integration suite to catch selector mismatches early** + +Run: +`dotnet test EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj --filter FullyQualifiedName~EstateManagementUI.IntegrationTests.Features.OperatorManagement` + +Expected: operator coverage still passes after the shared helper grows. + +### Task 3: Add the contract step definitions + +**Files:** +- Create: `EstateManagementUI.IntegrationTests/Steps/ContractManagementSteps.cs` + +- [ ] **Step 1: Add the contract step file** + +```csharp +using EstateManagementUI.IntegrationTests.Common; +using Microsoft.Playwright; +using Reqnroll; + +namespace EstateManagementUI.IntegrationTests.Steps; + +[Binding] +[Scope(Tag = "estate")] +public sealed class ContractManagementSteps +{ + private const string ContractDescription = "Integration Contract"; + private const string OperatorName = "Test Operator"; + private const string ProductName = "Integration Product"; + private const string ProductDisplayText = "Integration Product Display"; + private const string FeeDescription = "Integration Fee"; + + private readonly IPage _page; + private readonly TestingContext _testingContext; + + public ContractManagementSteps(IPage page, TestingContext testingContext) + { + _page = page; + _testingContext = testingContext; + } + + [When("I open the contract management screen")] + public Task WhenIOpenTheContractManagementScreen() => GetHelper().OpenContractManagementScreenAsync(); + + [Then("I should see the contract management heading")] + public Task ThenIShouldSeeTheContractManagementHeading() => GetHelper().AssertContractManagementHeadingVisibleAsync(); + + [When("I create a contract")] + public Task WhenICreateAContract() => GetHelper().CreateContractAsync(ContractDescription, OperatorName); + + [Then("I should see the contract in the list")] + public Task ThenIShouldSeeTheContractInTheList() => GetHelper().AssertContractListContainsAsync(ContractDescription); + + [When("I view the contract")] + public Task WhenIViewTheContract() => GetHelper().OpenContractViewAsync(ContractDescription); + + [Then("I should see the contract details page")] + public Task ThenIShouldSeeTheContractDetailsPage() => GetHelper().AssertContractViewVisibleAsync(ContractDescription); + + [When("I edit the contract")] + public Task WhenIEditTheContract() => GetHelper().OpenContractEditAsync(ContractDescription); + + [Then("I should see the contract edit page")] + public Task ThenIShouldSeeTheContractEditPage() => GetHelper().AssertContractEditVisibleAsync(ContractDescription); + + [When("I add a product to the contract")] + public Task WhenIAddAProductToTheContract() => GetHelper().AddProductToContractAsync(ProductName, ProductDisplayText, 10m); + + [Then("I should see the product in the contract edit view")] + public Task ThenIShouldSeeTheProductInTheContractEditView() => GetHelper().AssertContractProductVisibleAsync(ProductName); + + [When("I add a fee to the contract")] + public Task WhenIAddAFeeToTheContract() => GetHelper().AddFeeToContractAsync(FeeDescription, 1.50m); + + [Then("I should see the fee in the contract edit view")] + public Task ThenIShouldSeeTheFeeInTheContractEditView() => GetHelper().AssertContractFeeVisibleAsync(FeeDescription); + + [When("I return to the contract list")] + public Task WhenIReturnToTheContractList() => GetHelper().BackToContractListAsync(); + + private DashboardPageHelper GetHelper() => new(_page, _testingContext); +} +``` + +- [ ] **Step 2: Run the contract feature again** + +Run: +`dotnet test EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj --filter FullyQualifiedName~EstateManagementUI.IntegrationTests.Features.ContractManagement` + +Expected: the scenario binds and reaches the browser interactions. + +- [ ] **Step 3: Tighten selectors only if the browser assertions show the UI uses a different accessible target** + +Prefer changing the helper to match the current UI instead of broadening the Gherkin or duplicating navigation logic in the step file. + +### Task 4: Verify the full contract flow and commit it + +**Files:** +- Modify: `EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs` +- Create: `EstateManagementUI.IntegrationTests/Steps/ContractManagementSteps.cs` +- Create: `EstateManagementUI.IntegrationTests/Features/ContractManagement.feature` + +- [ ] **Step 1: Run the single contract integration scenario** + +Run: +`dotnet test EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj --filter FullyQualifiedName~EstateManagementUI.IntegrationTests.Features.ContractManagement` + +Expected: one passing scenario that creates a contract, opens the view page, opens the edit page, adds a product, adds a fee, and returns to the list. + +- [ ] **Step 2: Inspect traces and screenshots if the scenario fails** + +Use the existing `TestResults/Diagnostics`, `TestResults/Screenshots`, and `TestResults/Traces` outputs generated by `BrowserHooks.cs` to identify whether the failure is a selector issue, a timing issue, or a backend data issue. + +- [ ] **Step 3: Commit the integration test changes** + +```bash +git add EstateManagementUI.IntegrationTests/Features/ContractManagement.feature EstateManagementUI.IntegrationTests/Steps/ContractManagementSteps.cs EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs docs/superpowers/plans/2026-06-22-contract-screen-integration-test.md +git commit -m "test: add contract screen integration scenario" +``` diff --git a/docs/superpowers/specs/2026-06-22-contract-screen-integration-test-design.md b/docs/superpowers/specs/2026-06-22-contract-screen-integration-test-design.md new file mode 100644 index 0000000..c493b62 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-contract-screen-integration-test-design.md @@ -0,0 +1,150 @@ +# Contract Screen Integration Test Design + +> **For agentic workers:** this design is the contract-screen counterpart to the existing estate/operator integration coverage. Implement it as one ReqNroll scenario first, then add the minimum helper and step surface needed to make the flow readable. + +**Goal:** Add a single ReqNroll integration scenario that walks through the contract screens end-to-end: contract list, create, view, edit, and the edit-screen product and fee subflows. + +**Why this shape:** The contract UI is already split into a few distinct screens, and a single user journey is the clearest way to prove those pages still connect correctly without multiplying test maintenance. The scenario should exercise the main navigation and the important edit capabilities in one pass. + +**Architecture:** Reuse the existing Docker-backed integration harness and the same Playwright/ReqNroll pattern used by the dashboard and operator tests. Add one contract-focused feature file, a small step-definition class, and contract-specific helper methods in the shared page helper. Keep the scenario self-contained by creating its own contract through the UI instead of depending on pre-seeded contract data. + +**Tech Stack:** ReqNroll, NUnit, Playwright, Docker-backed integration containers, shared integration test helpers. + +--- + +### Scenario Shape + +The test should follow one continuous journey: + +1. Open the app and sign in with the existing estate test user. +2. Navigate to the contract list page and confirm the list screen renders. +3. Start contract creation, enter a contract description, choose an operator, and create it. +4. Confirm the new contract appears in the list. +5. Open the contract view screen and confirm the detail page renders. +6. Open the edit screen and confirm the edit page renders. +7. Add one product from the edit screen and confirm it appears in the edit view. +8. Add one fee from the edit screen and confirm it appears in the edit view. +9. Return to the list screen to prove the navigation loop closes cleanly. + +The scenario should avoid relying on contract-description persistence if the current backend does not support updating that field cleanly. The important thing is that the edit screen loads and its product/fee flows work. + +--- + +### Files To Add Or Update + +- Create: `EstateManagementUI.IntegrationTests/Features/ContractManagement.feature` +- Create: `EstateManagementUI.IntegrationTests/Steps/ContractManagementSteps.cs` +- Modify: `EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs` + +--- + +### Feature Design + +Use one scenario, tagged the same way as the other integration journeys so it runs inside the existing seeded estate context. + +```gherkin +@base @background @estate +Feature: Contract Management + As an authenticated estate user + I want to move through the contract screens + So that I can create, inspect, and edit a contract from one journey + + Scenario: Estate users can complete the full contract 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 estate info page + When I open the contract management screen + Then I should see the contract management heading + When I create a contract + Then I should see the contract in the list + When I view the contract + Then I should see the contract details page + When I edit the contract + Then I should see the contract edit page + When I add a product to the contract + Then I should see the product in the contract edit view + When I add a fee to the contract + Then I should see the fee in the contract edit view + When I return to the contract list + Then I should see the contract management heading +``` + +The scenario should use a single contract identifier generated during the run, so later steps can target the same record without extra state plumbing. + +--- + +### Helper Design + +Add helper methods for the following behaviors in `DashboardPageHelper.cs`: + +- open the contract management list screen +- assert the list screen heading and initial content +- create a contract from the new-contract form +- verify the created contract appears in the list +- open the contract view screen for the created contract +- assert the contract detail heading and details content +- open the contract edit screen for the created contract +- assert the edit page renders +- add a product using the edit-screen modal or inline form +- assert the new product is visible in the edit view +- add a fee using the edit-screen modal or inline form +- assert the new fee is visible in the edit view +- navigate back to the list screen + +Prefer existing stable selectors such as explicit buttons, role-based headings, and the current contract page ids/names. Only fall back to CSS selectors where the current UI does not expose a stable accessible target. + +Suggested data for the run: + +- Contract description: `Integration Contract` +- Product name: `Integration Product` +- Product display text: `Integration Product Display` +- Fee description: `Integration Fee` +- Fee value: `1.50` + +If the edit screen requires a product value or calculation type, use the smallest valid values that let the add flow succeed. + +--- + +### Step Design + +Create one contract-specific step class scoped to `@estate` that delegates into the helper. Keep the step text close to the user journey so the feature remains readable. + +The step file should cover: + +- open contract management +- create contract +- view contract +- edit contract +- add product +- add fee +- return to the list + +The step class should not duplicate page logic. It should only translate readable Gherkin into helper calls. + +--- + +### Verification Plan + +1. First run should fail with missing step definitions or missing helper methods. +2. After adding the step file and helper methods, the scenario should compile and reach browser assertions. +3. If selectors are off, tighten the helper locators rather than broadening the feature. +4. Final state should be one passing scenario in the contract feature file. + +Recommended verification command: + +```powershell +dotnet test EstateManagementUI.IntegrationTests/EstateManagementUI.IntegrationTests.csproj --filter FullyQualifiedName~EstateManagementUI.IntegrationTests.Features.ContractManagement +``` + +--- + +### Acceptance Criteria + +- One ReqNroll scenario covers the full contract journey. +- The scenario starts from login and ends back on the contract list. +- Contract creation is verified through the list page. +- Contract view and edit pages are both exercised. +- Product and fee creation flows are both exercised on the edit page. +- The implementation stays inside the existing integration test harness and does not require production code changes unless a selector proves unstable.