diff --git a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs index e7126a0..c883bc4 100644 --- a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs +++ b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs @@ -54,6 +54,7 @@ public async Task OpenEstateInfoFromEntryAsync() await RunWithFailureArtifactsAsync(async () => { await _page.Locator("a[href='/estate-info']").ClickAsync(new LocatorClickOptions { NoWaitAfter = true }); + await _page.WaitForURLAsync(new Regex(@".*/estate-info.*", RegexOptions.IgnoreCase)); await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); }, nameof(OpenEstateInfoFromEntryAsync)); } @@ -67,6 +68,11 @@ await RunWithFailureArtifactsAsync(async () => State = WaitForSelectorState.Visible, Timeout = 10000 }); + await _page.GetByText("Comprehensive estate management and configuration").WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); await _page.Locator("#loginButton").WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, @@ -79,6 +85,171 @@ await _page.Locator("#loginButton").WaitForAsync(new LocatorWaitForOptions }, nameof(AssertEstateInfoPageVisibleAsync)); } + public async Task OpenOperatorManagementScreenAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var operatorLink = _page.Locator("#operatorsLink"); + if (await operatorLink.CountAsync() > 0 && await operatorLink.First.IsVisibleAsync()) + { + await operatorLink.First.ClickAsync(new LocatorClickOptions { NoWaitAfter = true }); + } + else + { + await _page.GotoAsync(ResolveEstateManagementBaseUrl() + "/operators"); + } + + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenOperatorManagementScreenAsync)); + } + + public async Task AssertOperatorManagementHeadingVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = "Operator Management" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertOperatorManagementHeadingVisibleAsync)); + } + + public async Task AssertOperatorListContainsAsync(string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + var operatorRow = GetOperatorRow(operatorName); + await operatorRow.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await operatorRow.IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertOperatorListContainsAsync)); + } + + public async Task OpenOperatorViewAsync(string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + var operatorRow = GetOperatorRow(operatorName); + await operatorRow.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await operatorRow.ClickAsync(); + }, nameof(OpenOperatorViewAsync)); + } + + public async Task AssertOperatorViewVisibleAsync(string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = $"View Operator: {operatorName}" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Heading, new() { Name = "Operator Details" }).IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Back to List" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertOperatorViewVisibleAsync)); + } + + public async Task BackToOperatorListFromViewAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Back to List" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(BackToOperatorListFromViewAsync)); + } + + public async Task OpenOperatorEditAsync(string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + var editButton = GetOperatorRow(operatorName).GetByRole(AriaRole.Button, new() { Name = "Edit" }); + await editButton.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + await editButton.ClickAsync(); + }, nameof(OpenOperatorEditAsync)); + } + + public async Task AssertOperatorEditVisibleAsync(string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = $"Edit Operator: {operatorName}" }); + await heading.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 10000 + }); + + (await heading.IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("input[placeholder='Enter operator name']").IsVisibleAsync()).ShouldBeTrue(); + (await _page.GetByRole(AriaRole.Button, new() { Name = "Update Operator" }).IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertOperatorEditVisibleAsync)); + } + + public async Task CancelOperatorEditAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.GetByRole(AriaRole.Button, new() { Name = "Cancel" }).ClickAsync(); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(CancelOperatorEditAsync)); + } + + public async Task OpenNewOperatorScreenAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("#newOperatorButton").ClickAsync(); + await _page.WaitForURLAsync(new Regex(@".*/operators/new.*", RegexOptions.IgnoreCase)); + await _page.WaitForLoadStateAsync(LoadState.NetworkIdle); + }, nameof(OpenNewOperatorScreenAsync)); + } + + public async Task AssertNewOperatorScreenVisibleAsync() + { + await RunWithFailureArtifactsAsync(async () => + { + (await WaitForAnyVisibleAsync( + "h1:has-text('Create New Operator')", + "input[placeholder='Enter operator name']", + "#createOperatorButton")).ShouldBeTrue(); + + var heading = _page.GetByRole(AriaRole.Heading, new() { Name = "Create New Operator" }); + (await heading.IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("input[placeholder='Enter operator name']").IsVisibleAsync()).ShouldBeTrue(); + (await _page.Locator("#createOperatorButton").IsVisibleAsync()).ShouldBeTrue(); + }, nameof(AssertNewOperatorScreenVisibleAsync)); + } + + public async Task CreateOperatorAsync(string operatorName) + { + await RunWithFailureArtifactsAsync(async () => + { + await _page.Locator("input[placeholder='Enter operator name']").FillAsync(operatorName); + await _page.Locator("#createOperatorButton").ClickAsync(); + }, nameof(CreateOperatorAsync)); + } + public async Task ClickSignInButtonAsync() { await RunWithFailureArtifactsAsync(async () => @@ -492,12 +663,30 @@ await _page.Locator(".animate-spin").WaitForAsync(new LocatorWaitForOptions }); } + private async Task WaitForOperatorManagementAsync() + { + var spinner = _page.Locator(".animate-spin"); + if (await spinner.CountAsync() > 0) + { + await spinner.First.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Hidden, + Timeout = 10000 + }); + } + } + private ILocator GetAssignedOperatorRow(string operatorName) { return _page.Locator("div.flex.items-center.justify-between.p-3.bg-gray-50.rounded-lg") .Filter(new() { HasText = operatorName }); } + private ILocator GetOperatorRow(string operatorName) + { + return _page.Locator("tbody tr").Filter(new() { HasText = operatorName }); + } + private async Task IsAnyVisibleAsync(params string[] selectors) { foreach (var selector in selectors) diff --git a/EstateManagementUI.IntegrationTests/Features/OperatorManagement.feature b/EstateManagementUI.IntegrationTests/Features/OperatorManagement.feature new file mode 100644 index 0000000..66dbcfa --- /dev/null +++ b/EstateManagementUI.IntegrationTests/Features/OperatorManagement.feature @@ -0,0 +1,86 @@ +@base @background @estate +Feature: Operator Management + As an authenticated estate user + I want to move through the operator management screens + So that I can manage operators from one end-to-end 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 | + | Test Estate | Spare Operator | False | False | + + 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 | + + Given the user navigates to the entry screen + Then I should see the entry screen + When I open the estate information page + Then I should see the estate info page + 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 + + Scenario: Estate users can navigate the operator screens and manage operators + When I open the operator management screen + Then I should see the operator management heading + And I should see the operator 'Test Operator' in the operator list + When I open the operator view for 'Test Operator' + Then I should see the operator view page for 'Test Operator' + When I go back to the operator list from the view page + Then I should see the operator management heading + When I open the operator edit page for 'Test Operator' + Then I should see the operator edit page for 'Test Operator' + When I cancel operator editing + Then I should see the operator management heading + When I open the new operator screen + Then I should see the new operator screen + When I create the operator 'Integration Operator' + Then I should see the operator management heading + And I should see the operator 'Integration Operator' in the operator list diff --git a/EstateManagementUI.IntegrationTests/Steps/OperatorManagementSteps.cs b/EstateManagementUI.IntegrationTests/Steps/OperatorManagementSteps.cs new file mode 100644 index 0000000..208d03d --- /dev/null +++ b/EstateManagementUI.IntegrationTests/Steps/OperatorManagementSteps.cs @@ -0,0 +1,57 @@ +using EstateManagementUI.IntegrationTests.Common; +using Microsoft.Playwright; +using Reqnroll; + +namespace EstateManagementUI.IntegrationTests.Steps; + +[Binding] +[Scope(Tag = "estate")] +public sealed class OperatorManagementSteps +{ + private readonly IPage _page; + private readonly TestingContext _testingContext; + + public OperatorManagementSteps(IPage page, TestingContext testingContext) + { + _page = page; + _testingContext = testingContext; + } + + [When("I open the operator management screen")] + public Task WhenIOpenTheOperatorManagementScreen() => GetHelper().OpenOperatorManagementScreenAsync(); + + [Then("I should see the operator management heading")] + public Task ThenIShouldSeeTheOperatorManagementHeading() => GetHelper().AssertOperatorManagementHeadingVisibleAsync(); + + [Then("I should see the operator {string} in the operator list")] + public Task ThenIShouldSeeTheOperatorInTheOperatorList(string operatorName) => GetHelper().AssertOperatorListContainsAsync(operatorName); + + [When("I open the operator view for {string}")] + public Task WhenIOpenTheOperatorViewFor(string operatorName) => GetHelper().OpenOperatorViewAsync(operatorName); + + [Then("I should see the operator view page for {string}")] + public Task ThenIShouldSeeTheOperatorViewPageFor(string operatorName) => GetHelper().AssertOperatorViewVisibleAsync(operatorName); + + [When("I go back to the operator list from the view page")] + public Task WhenIGoBackToTheOperatorListFromTheViewPage() => GetHelper().BackToOperatorListFromViewAsync(); + + [When("I open the operator edit page for {string}")] + public Task WhenIOpenTheOperatorEditPageFor(string operatorName) => GetHelper().OpenOperatorEditAsync(operatorName); + + [Then("I should see the operator edit page for {string}")] + public Task ThenIShouldSeeTheOperatorEditPageFor(string operatorName) => GetHelper().AssertOperatorEditVisibleAsync(operatorName); + + [When("I cancel operator editing")] + public Task WhenICancelOperatorEditing() => GetHelper().CancelOperatorEditAsync(); + + [When("I open the new operator screen")] + public Task WhenIOpenTheNewOperatorScreen() => GetHelper().OpenNewOperatorScreenAsync(); + + [Then("I should see the new operator screen")] + public Task ThenIShouldSeeTheNewOperatorScreen() => GetHelper().AssertNewOperatorScreenVisibleAsync(); + + [When("I create the operator {string}")] + public Task WhenICreateTheOperator(string operatorName) => GetHelper().CreateOperatorAsync(operatorName); + + private DashboardPageHelper GetHelper() => new(_page, _testingContext); +}