Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Globalization;
using Microsoft.Playwright;
using Shouldly;
using System.Text.Json;
Expand Down Expand Up @@ -53,7 +54,7 @@
{
await RunWithFailureArtifactsAsync(async () =>
{
await _page.Locator("a[href='/estate-info']").ClickAsync(new LocatorClickOptions { NoWaitAfter = true });

Check warning on line 57 in EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs

View workflow job for this annotation

GitHub Actions / Build and Unit Test

'LocatorClickOptions.NoWaitAfter' is obsolete
await _page.WaitForURLAsync(new Regex(@".*/estate-info.*", RegexOptions.IgnoreCase));
await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
}, nameof(OpenEstateInfoFromEntryAsync));
Expand Down Expand Up @@ -92,7 +93,7 @@
var operatorLink = _page.Locator("#operatorsLink");
if (await operatorLink.CountAsync() > 0 && await operatorLink.First.IsVisibleAsync())
{
await operatorLink.First.ClickAsync(new LocatorClickOptions { NoWaitAfter = true });

Check warning on line 96 in EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs

View workflow job for this annotation

GitHub Actions / Build and Unit Test

'LocatorClickOptions.NoWaitAfter' is obsolete
}
else
{
Expand Down Expand Up @@ -470,6 +471,245 @@
}, 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 () =>
Expand Down Expand Up @@ -687,6 +927,30 @@
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/(?<id>[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<bool> IsAnyVisibleAsync(params string[] selectors)
{
foreach (var selector in selectors)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading