diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index 3ae1d185..78b65df6 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -5,6 +5,10 @@ on:
branches:
- main
+permissions:
+ contents: read
+ checks: write
+
jobs:
unittests:
name: "Build and Unit Test"
@@ -48,61 +52,67 @@ jobs:
name: test-results
path: '**/TestResults/*.trx'
retention-days: 30
-
-
-# chrometests:
-# name: "Build and Test UI - Chrome"
-# env:
-# ASPNETCORE_ENVIRONMENT: "Production"
-# NODE_VERSION: '14.x' # set this to the node version to use
-
-# runs-on: ubuntu-latest
-
-# steps:
-# - uses: actions/checkout@v2.3.4
-
-# - name: Trust Certificate
-# run: |
-# sudo apt-get install expect
-# certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx"
-# password="password"
-
-# # Use expect to automate the passphrase input
-# expect -c "
-# spawn openssl pkcs12 -in \"$certPath\" -clcerts -nokeys -out certificate.crt -password pass:\"$password\"
-# expect \"Enter Import Password:\" { send \"$password\n\" }
-# expect eof
-# "
-
-# expect -c "
-# spawn openssl pkcs12 -in \"$certPath\" -nocerts -out private.key -password pass:\"$password\"
-# expect \"Enter Import Password:\" { send \"$password\n\" }
-# expect eof
-# "
-
-# sudo cp certificate.crt /usr/local/share/ca-certificates/aspnetapp-root-cert.crt
-# sudo update-ca-certificates
-
-# sudo cp private.key /etc/ssl/private/aspnetapp-root-cert.key
-# sudo chmod 400 /etc/ssl/private/aspnetapp-root-cert.key
+
+ chrometests:
+ name: "Build and Test UI - Chrome"
+ env:
+ ASPNETCORE_ENVIRONMENT: "Production"
+ NODE_VERSION: '14.x' # set this to the node version to use
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2.3.4
+
+ - name: Trust Certificate
+ run: |
+ sudo apt-get install expect
+ certPath="$GITHUB_WORKSPACE/Certificates/aspnetapp-root-cert.pfx"
+ password="password"
+
+ # Use expect to automate the passphrase input
+ expect -c "
+ spawn openssl pkcs12 -in \"$certPath\" -clcerts -nokeys -out certificate.crt -password pass:\"$password\"
+ expect \"Enter Import Password:\" { send \"$password\n\" }
+ expect eof
+ "
+
+ expect -c "
+ spawn openssl pkcs12 -in \"$certPath\" -nocerts -out private.key -password pass:\"$password\"
+ expect \"Enter Import Password:\" { send \"$password\n\" }
+ expect eof
+ "
+
+ sudo cp certificate.crt /usr/local/share/ca-certificates/aspnetapp-root-cert.crt
+ sudo update-ca-certificates
+
+ sudo cp private.key /etc/ssl/private/aspnetapp-root-cert.key
+ sudo chmod 400 /etc/ssl/private/aspnetapp-root-cert.key
-# - name: Build Docker Image
-# run: docker build . --file EstateManagementUI/Dockerfile --tag estatemanagementui:latest
-
-# - name: Restore Nuget Packages
-# run: dotnet restore EstateManagementUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
-
-# - name: Run Integration Tests (Chrome)
-# env:
-# Browser: Chrome
-# IsCI: true
-# run: dotnet test "EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj" --filter Category=PRTest
+ - name: Build Docker Image
+ run: docker build . --file EstateManagementUI.BlazorServer/Dockerfile --tag estatemanagementuiblazorserver:latest
+
+ - name: Restore Nuget Packages
+ run: dotnet restore EstateManagementUI.sln --source ${{ secrets.PUBLICFEEDURL }} --source ${{ secrets.PRIVATEFEED_URL }}
+
+ - name: Run Integration Tests (Chrome)
+ env:
+ Browser: Chrome
+ IsCI: true
+ run: dotnet test "EstateManagementUI.IntegrationTests\EstateManagementUI.IntegrationTests.csproj" # --filter Category=PRTest
+
+ - name: Upload Playwright screenshots on failure
+ uses: actions/upload-artifact@v4.4.0
+ if: ${{ failure() }}
+ with:
+ name: chrometestresults
+ path: '**/TestResults/**'
-# - uses: actions/upload-artifact@v4.4.0
-# if: ${{ failure() }}
-# with:
-# name: chrometracelogs
-# path: /home/txnproc/trace/
+ - uses: actions/upload-artifact@v4.4.0
+ if: ${{ failure() }}
+ with:
+ name: chrometracelogs
+ path: /home/txnproc/trace/
# edgetests:
# name: "Build and Test UI - Edge"
@@ -210,4 +220,4 @@ jobs:
# if: ${{ failure() }}
# with:
# name: firefoxtracelogs
-# path: /home/txnproc/trace/
\ No newline at end of file
+# path: /home/txnproc/trace/
diff --git a/Pages/_Host.cshtml b/Archive/Pages/_Host.cshtml
similarity index 100%
rename from Pages/_Host.cshtml
rename to Archive/Pages/_Host.cshtml
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3e5fab83..24bfe947 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,9 +4,9 @@
-
+
-
+
@@ -14,11 +14,14 @@
+
+
+
@@ -27,12 +30,15 @@
+
-
-
+
+
+
-
+
+
diff --git a/EstateManagementUI.BlazorServer/Common/BoostrapperExtensions.cs b/EstateManagementUI.BlazorServer/Common/BoostrapperExtensions.cs
index a2fb4ede..3e00e835 100644
--- a/EstateManagementUI.BlazorServer/Common/BoostrapperExtensions.cs
+++ b/EstateManagementUI.BlazorServer/Common/BoostrapperExtensions.cs
@@ -206,6 +206,8 @@ public static WebApplicationBuilder ConfigureLiveAuthentication(this WebApplicat
{
OnRedirectToIdentityProvider = context =>
{
+ context.ProtocolMessage.IssuerAddress = $"{issuerAddress}/connect/authorize";
+
// Pass prompt parameter if specified in authentication properties
if (context.Properties.Items.TryGetValue("prompt", out var prompt))
{
@@ -273,4 +275,4 @@ public static WebApplicationBuilder RegisterSerialiser(this WebApplicationBuilde
builder.Services.AddSingleton(serialiserSettings);
return builder;
}
-}
\ No newline at end of file
+}
diff --git a/EstateManagementUI.BlazorServer/Components/Pages/EntryScreen.razor b/EstateManagementUI.BlazorServer/Components/Pages/EntryScreen.razor
index 11bc4f01..f00bc25e 100644
--- a/EstateManagementUI.BlazorServer/Components/Pages/EntryScreen.razor
+++ b/EstateManagementUI.BlazorServer/Components/Pages/EntryScreen.razor
@@ -131,7 +131,7 @@
Sign in to access all features
-
+
diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Home.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Home.razor.cs
index 770920aa..749044b7 100644
--- a/EstateManagementUI.BlazorServer/Components/Pages/Home.razor.cs
+++ b/EstateManagementUI.BlazorServer/Components/Pages/Home.razor.cs
@@ -34,16 +34,22 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
}
// This runs once after the interactive circuit is established.
- await this.LogToConsole("OnAfterRenderAsync FIRST RENDER (interactive) - performing interactive initialization");
+ await this.TraceDashboardAsync("OnAfterRenderAsync FIRST RENDER - starting interactive initialization");
+ await this.TraceDashboardAsync($"Current URI: {NavigationManager.Uri}");
try
{
- await OnAfterRender(PermissionSection.Dashboard, PermissionFunction.View);
+ var permissionResult = await OnAfterRender(PermissionSection.Dashboard, PermissionFunction.View);
+ await this.TraceDashboardAsync($"Dashboard permission check result: {(permissionResult.IsFailed ? "FAILED" : "SUCCESS")}");
+
ClaimsPrincipal user = this.AuthState.User;
+ await this.TraceDashboardAsync($"Authentication state: authenticated={user.Identity?.IsAuthenticated}, name={user.Identity?.Name ?? ""}, authType={user.Identity?.AuthenticationType ?? ""}");
+ await this.TraceDashboardAsync($"Claims: {string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"))}");
// Redirect unauthenticated users to entry screen
if (!user.Identity?.IsAuthenticated ?? true)
{
+ await this.TraceDashboardAsync("User is not authenticated - redirecting to entry page");
NavigationManager.NavigateToEntryPage();
return;
}
@@ -51,29 +57,33 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
// Determine role and admin flag now that we're interactive
var role = await PermissionService.GetUserRoleAsync();
this.isAdministrator = role == "Administrator";
- await this.LogToConsole($"User role: {role}, isAdministrator: {this.isAdministrator}");
-
- CorrelationId correlationId = new CorrelationId(Guid.NewGuid());
- var estateId = await this.GetEstateId();
+ await this.TraceDashboardAsync($"User role resolved to '{role}', isAdministrator={this.isAdministrator}");
// Only load dashboard data for non-admins
if (this.isAdministrator == false)
{
+ CorrelationId correlationId = new CorrelationId(Guid.NewGuid());
+ var estateId = await this.GetEstateId();
+ await this.TraceDashboardAsync($"Resolved estateId for dashboard data load: {estateId}");
+ await this.TraceDashboardAsync("Non-admin user - loading dashboard data");
var result = await this.LoadDashboardData(correlationId, estateId);
if (result.IsFailed)
{
+ await this.TraceDashboardAsync($"Dashboard data load failed: {result.Message ?? ""}");
this.NavigationManager.NavigateToErrorPage();
return;
}
}
else
{
+ await this.TraceDashboardAsync("Administrator detected - skipping dashboard data load and rendering welcome panel");
this.isLoading = false;
this.StateHasChanged();
}
}
catch (Exception ex)
{
+ await this.TraceDashboardAsync($"Initialization exception: {ex}");
this.errorMessage = $"Initialization error: {ex.Message}";
this.isLoading = false;
this.StateHasChanged();
@@ -86,7 +96,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
private async Task LoadDashboardData(CorrelationId correlationId, Guid estateId)
{
- await this.LogToConsole($"LoadDashboardData START - selectedDate: {this._selectedComparisonDate}");
+ await this.TraceDashboardAsync($"LoadDashboardData START - selectedDate: {this._selectedComparisonDate}");
try {
this.isLoading = true;
this.errorMessage = null;
@@ -142,8 +152,10 @@ private async Task LoadDashboardData(CorrelationId correlationId, Guid e
private async Task> LoadComparisonDates(CorrelationId correlationId, Guid estateId) {
if (this.comparisonDates == null || !this.comparisonDates.Any()) {
+ await this.TraceDashboardAsync($"Loading comparison dates for estateId={estateId}");
var comparisonDatesResult = await this.CalendarUiService.GetComparisonDates(correlationId, estateId);
if (comparisonDatesResult.IsFailed) {
+ await this.TraceDashboardAsync($"Comparison dates load failed: {comparisonDatesResult.Message ?? ""}");
return ResultHelpers.CreateFailure(comparisonDatesResult);
}
@@ -165,7 +177,7 @@ private async Task> LoadComparisonDates(CorrelationId correlati
private async Task OnComparisonDateChanged()
{
this.changeEventCounter++;
- await this.LogToConsole($"🔥 OnComparisonDateChanged FIRED! Count: {this.changeEventCounter}, New value: {this._selectedComparisonDate}");
+ await this.TraceDashboardAsync($"OnComparisonDateChanged fired. Count={this.changeEventCounter}, New value={this._selectedComparisonDate}");
// This is called after _selectedComparisonDate is updated by @bind-Value
if (this.isAdministrator == false) {
@@ -173,20 +185,28 @@ private async Task OnComparisonDateChanged()
CorrelationId correlationId = CorrelationIdHelper.New();
Guid estateId= await this.GetEstateId();
- await this.LogToConsole($"Loading dashboard data for date: {this._selectedComparisonDate}");
+ await this.TraceDashboardAsync($"Loading dashboard data for date: {this._selectedComparisonDate}");
var loadResult = await this.LoadDashboardData(correlationId,estateId);
if (loadResult.IsFailed) {
+ await this.TraceDashboardAsync($"Dashboard refresh failed: {loadResult.Message ?? ""}");
this.NavigationManager.NavigateToErrorPage();
return;
}
this.StateHasChanged();
- await this.LogToConsole("Dashboard data reload complete");
+ await this.TraceDashboardAsync("Dashboard data reload complete");
}
else {
- await this.LogToConsole("User is administrator - skipping data reload");
+ await this.TraceDashboardAsync("Administrator user - skipping dashboard data reload");
}
}
+ private async Task TraceDashboardAsync(string message)
+ {
+ var trace = $"[Home.razor {DateTime.Now:HH:mm:ss.fff}] {message}";
+ Console.WriteLine(trace);
+ await this.LogToConsole(message);
+ }
+
private async Task LogToConsole(string message)
{
try
diff --git a/EstateManagementUI.BlazorServer/Components/Permissions/AuthorizedComponentBase.cs b/EstateManagementUI.BlazorServer/Components/Permissions/AuthorizedComponentBase.cs
index de7b2188..78a29254 100644
--- a/EstateManagementUI.BlazorServer/Components/Permissions/AuthorizedComponentBase.cs
+++ b/EstateManagementUI.BlazorServer/Components/Permissions/AuthorizedComponentBase.cs
@@ -25,8 +25,10 @@ protected async Task RequirePermission(PermissionSection permissionSecti
{
// Do a permission check here
Boolean hasPermission = await this.PermissionService.HasPermissionAsync(permissionSection, permissionFunction);
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] RequirePermission section={permissionSection}, function={permissionFunction}, result={hasPermission}");
if (hasPermission == false)
{
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] Permission denied - navigating to access denied page");
this.NavigationManager.NavigateToAccessDeniedPage();
return Result.Unauthorized();
}
@@ -37,8 +39,13 @@ protected async Task RequirePermission(PermissionSection permissionSecti
protected async Task GetEstateId() {
Result estateIdResult = this.AuthState.GetEstateIdFromClaims();
if (estateIdResult.IsFailed) {
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] Failed to resolve estateId from claims");
this.NavigationManager.NavigateToErrorPage();
}
+ else
+ {
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] Resolved estateId={estateIdResult.Data}");
+ }
return estateIdResult.Data;
}
@@ -52,20 +59,29 @@ protected async Task OnAfterRender(PermissionSection section,
{
this.AuthState = await AuthenticationStateTask;
User = this.AuthState.User;
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender authState authenticated={User.Identity?.IsAuthenticated}, name={User.Identity?.Name ?? ""}, authType={User.Identity?.AuthenticationType ?? ""}, claims={string.Join(", ", User.Claims.Select(c => $"{c.Type}={c.Value}"))}");
Result authResult = await RequirePermission(section, function);
if (authResult.IsFailed)
+ {
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender stopped because permission check failed");
return Result.Failure();
+ }
if (loadFunc == null)
+ {
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender completed without loadFunc");
return Result.Success();
+ }
Result result = await loadFunc();
if (result.IsFailed)
{
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender loadFunc failed");
this.NavigationManager.NavigateToErrorPage();
return Result.Failure();
}
+ Console.WriteLine($"[AuthorizedComponentBase {DateTime.Now:HH:mm:ss.fff}] OnAfterRender loadFunc succeeded");
return Result.Success();
}
-}
\ No newline at end of file
+}
diff --git a/EstateManagementUI.BlazorServer/Dockerfile b/EstateManagementUI.BlazorServer/Dockerfile
index 993d3bb2..4b6de0eb 100644
--- a/EstateManagementUI.BlazorServer/Dockerfile
+++ b/EstateManagementUI.BlazorServer/Dockerfile
@@ -13,6 +13,7 @@ RUN curl -fsSL --insecure https://deb.nodesource.com/setup_20.x | bash - && \
COPY ["EstateManagementUI.BlazorServer/NuGet.Config", "."]
COPY ["EstateManagementUI.BlazorServer/Certificates/*.*", "Certificates/"]
COPY ["EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj", "EstateManagementUI.BlazorServer/"]
+COPY ["Directory.Packages.props", "."]
RUN dotnet restore "./EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj"
COPY . .
diff --git a/EstateManagementUI.BlazorServer/Dockerfile.original b/EstateManagementUI.BlazorServer/Dockerfile.original
new file mode 100644
index 00000000..993d3bb2
--- /dev/null
+++ b/EstateManagementUI.BlazorServer/Dockerfile.original
@@ -0,0 +1,38 @@
+FROM stuartferguson/txnprocbase AS base
+WORKDIR /app
+
+# This stage is used to build the service project
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+
+# Install Node.js for Tailwind CSS build - use insecure curl for corporate proxy environments
+RUN curl -fsSL --insecure https://deb.nodesource.com/setup_20.x | bash - && \
+ apt-get install -y nodejs
+
+COPY ["EstateManagementUI.BlazorServer/NuGet.Config", "."]
+COPY ["EstateManagementUI.BlazorServer/Certificates/*.*", "Certificates/"]
+COPY ["EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj", "EstateManagementUI.BlazorServer/"]
+RUN dotnet restore "./EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj"
+
+COPY . .
+WORKDIR "/src/EstateManagementUI.BlazorServer"
+
+# Install npm packages and build Tailwind CSS
+RUN npm install
+RUN npm run css:build
+
+RUN dotnet build "./EstateManagementUI.BlazorServer.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# This stage is used to publish the service project to be copied to the final stage
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./EstateManagementUI.BlazorServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+# Copy certificates to the runtime image
+COPY --from=build /src/EstateManagementUI.BlazorServer/Certificates ./Certificates
+ENTRYPOINT ["dotnet", "EstateManagementUI.BlazorServer.dll"]
diff --git a/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj b/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj
index 15ec8ff7..10e4ef21 100644
--- a/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj
+++ b/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj
@@ -18,6 +18,7 @@
+
diff --git a/EstateManagementUI.BlazorServer/appsettings.Development.json b/EstateManagementUI.BlazorServer/appsettings.Development.json
index d4c5b89f..5a5cda7a 100644
--- a/EstateManagementUI.BlazorServer/appsettings.Development.json
+++ b/EstateManagementUI.BlazorServer/appsettings.Development.json
@@ -11,20 +11,20 @@
"SecurityServicePort": null,
"HttpClientIgnoreCertificateErrors": true,
"TestMode": "Disabled",
- //"TransactionProcessorApi": "http://192.168.1.163:5002",
- //"EstateReportingApi": "http://192.168.1.163:5011",
- //"FileProcessorApi": "http://192.168.1.163:5009",
- //"SecurityService": "https://192.168.1.163:5001",
- "TransactionProcessorApi": "http://127.0.0.1:5002",
- "EstateReportingApi": "http://127.0.0.1:5011",
- "FileProcessorApi": "http://127.0.0.1:5009",
- "SecurityService": "https://127.0.0.1:5001",
+ "TransactionProcessorApi": "http://192.168.1.163:5002",
+ "EstateReportingApi": "http://192.168.1.163:5011",
+ "FileProcessorApi": "http://192.168.1.163:5009",
+ "SecurityService": "https://192.168.1.163:5001",
+ //"TransactionProcessorApi": "http://127.0.0.1:5002",
+ //"EstateReportingApi": "http://127.0.0.1:5011",
+ //"FileProcessorApi": "http://127.0.0.1:5009",
+ //"SecurityService": "https://127.0.0.1:5001",
"ClientId": "serviceClient",
"ClientSecret": "d192cbc46d834d0da90e8a9d50ded543"
},
"Authentication": {
- "Authority": "https://127.0.0.1:5001",
- //"Authority": "https://192.168.1.163:5001",
+ //"Authority": "https://127.0.0.1:5001",
+ "Authority": "https://192.168.1.163:5001",
"ClientId": "managementUIClient",
"ClientSecret": "d192cbc46d834d0da90e8a9d50ded543",
"CallbackPath": "/signin-oidc",
diff --git a/EstateManagementUI.IntegrationTests/Common/ContractManagementPageHelper.cs b/EstateManagementUI.IntegrationTests/Common/ContractManagementPageHelper.cs
deleted file mode 100644
index ec922780..00000000
--- a/EstateManagementUI.IntegrationTests/Common/ContractManagementPageHelper.cs
+++ /dev/null
@@ -1,661 +0,0 @@
-using Microsoft.Playwright;
-using Shouldly;
-
-namespace EstateManagementUI.IntegrationTests.Common;
-
-///
-/// Helper class for interacting with the Contract Management pages using Playwright
-///
-public class ContractManagementPageHelper
-{
- private readonly IPage _page;
- private readonly string _baseUrl;
-
- public ContractManagementPageHelper(IPage page, string baseUrl)
- {
- _page = page;
- _baseUrl = baseUrl;
- }
-
- #region Navigation
-
- ///
- /// Navigate to the Contract Management page (index)
- ///
- public async Task NavigateToContractManagement()
- {
- await _page.GotoAsync($"{_baseUrl}/contracts");
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- }
-
- ///
- /// Navigate to home page to check menu visibility
- ///
- public async Task NavigateToHome()
- {
- await _page.GotoAsync(_baseUrl);
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- }
-
- ///
- /// Navigate to the Create New Contract page
- ///
- public async Task NavigateToCreateNewContract()
- {
- await _page.GotoAsync($"{_baseUrl}/contracts/new");
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- }
-
- ///
- /// Navigate to the Edit Contract page for a specific contract
- ///
- public async Task NavigateToEditContract(string contractId)
- {
- await _page.GotoAsync($"{_baseUrl}/contracts/{contractId}/edit");
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- }
-
- ///
- /// Navigate to the Edit Contract page for the first contract (using hardcoded test data ID)
- ///
- public async Task NavigateToEditFirstContract()
- {
- // Using the hardcoded test data contract ID from StubbedMediatorService
- await NavigateToEditContract("44444444-4444-4444-4444-444444444444");
- }
-
- #endregion
-
- #region Menu Visibility Verification
-
- ///
- /// Verify that Contract Management menu is not visible (Administrator role)
- ///
- public async Task VerifyContractManagementMenuIsNotVisible()
- {
- // Wait for page to be in stable state
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- var menuLinkCount = await _page.Locator("text=Contract Management").CountAsync();
- menuLinkCount.ShouldBe(0, "Contract Management menu should not be visible for Administrator role");
- }
-
- ///
- /// Verify that Contract Management menu is visible
- ///
- public async Task VerifyContractManagementMenuIsVisible()
- {
- await _page.Locator("text=Contract Management").WaitForAsync();
- var menuLinkCount = await _page.Locator("text=Contract Management").CountAsync();
- menuLinkCount.ShouldBeGreaterThan(0, "Contract Management menu should be visible");
- }
-
- #endregion
-
- #region Page Verification
-
- ///
- /// Verify the Contract Management page is displayed
- ///
- public async Task VerifyContractManagementPageIsDisplayed()
- {
- await _page.Locator("h1:has-text('Contract Management')").WaitForAsync();
- var heading = await _page.Locator("h1:has-text('Contract Management')").TextContentAsync();
- heading.ShouldNotBeNull();
- heading.ShouldContain("Contract Management");
- }
-
- ///
- /// Verify the page title
- ///
- public async Task VerifyPageTitle(string expectedTitle)
- {
- var title = await _page.TitleAsync();
- title.ShouldBe(expectedTitle);
- }
-
- ///
- /// Verify the View Contract page is displayed
- ///
- public async Task VerifyViewContractPageIsDisplayed()
- {
- await _page.Locator("h1:has-text('View Contract')").WaitForAsync();
- var heading = await _page.Locator("h1").TextContentAsync();
- heading.ShouldNotBeNull();
- heading.ShouldContain("View Contract");
- }
-
- ///
- /// Verify the Create New Contract page is displayed
- ///
- public async Task VerifyCreateNewContractPageIsDisplayed()
- {
- await _page.Locator("h1:has-text('Create New Contract')").WaitForAsync();
- var heading = await _page.Locator("h1:has-text('Create New Contract')").TextContentAsync();
- heading.ShouldNotBeNull();
- heading.ShouldContain("Create New Contract");
- }
-
- ///
- /// Verify the Edit Contract page is displayed
- ///
- public async Task VerifyEditContractPageIsDisplayed()
- {
- await _page.Locator("h1:has-text('Edit Contract')").WaitForAsync();
- var heading = await _page.Locator("h1").TextContentAsync();
- heading.ShouldNotBeNull();
- heading.ShouldContain("Edit Contract");
- }
-
- ///
- /// Verify access denied message is displayed
- ///
- public async Task VerifyAccessDeniedMessageIsDisplayed()
- {
- await _page.Locator("h3:has-text('Access Denied')").WaitForAsync();
- var message = await _page.Locator("h3:has-text('Access Denied')").TextContentAsync();
- message.ShouldNotBeNull();
- message.ShouldContain("Access Denied");
- }
-
- ///
- /// Verify access denied message for contract creation
- ///
- public async Task VerifyAccessDeniedForContractCreation()
- {
- await _page.Locator("text=You don't have permission to create contracts").WaitForAsync();
- }
-
- ///
- /// Verify access denied message for contract editing
- ///
- public async Task VerifyAccessDeniedForContractEditing()
- {
- await _page.Locator("text=You don't have permission to edit contracts").WaitForAsync();
- }
-
- #endregion
-
- #region Contract List Verification
-
- ///
- /// Verify the number of contracts displayed in the list
- ///
- public async Task VerifyContractCount(int expectedCount)
- {
- // Wait for contracts grid to be present
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- var contractCards = _page.Locator(".bg-white.rounded-lg.shadow-md.p-6.hover\\:shadow-lg");
- var count = await contractCards.CountAsync();
- count.ShouldBe(expectedCount, $"Expected {expectedCount} contracts but found {count}");
- }
-
- ///
- /// Verify the "Add New Contract" button is visible
- ///
- public async Task VerifyAddNewContractButtonIsVisible()
- {
- await _page.Locator("#newContractButton").WaitForAsync();
- var button = await _page.Locator("#newContractButton").IsVisibleAsync();
- button.ShouldBeTrue("Add New Contract button should be visible");
- }
-
- ///
- /// Verify the "Add New Contract" button is not visible
- ///
- public async Task VerifyAddNewContractButtonIsNotVisible()
- {
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- var buttonCount = await _page.Locator("#newContractButton").CountAsync();
- buttonCount.ShouldBe(0, "Add New Contract button should not be visible");
- }
-
- ///
- /// Verify Edit button is not visible for contracts (Viewer role)
- ///
- public async Task VerifyEditButtonIsNotVisibleForContracts()
- {
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- var editButtonCount = await _page.Locator("button:has-text('Edit')").CountAsync();
- editButtonCount.ShouldBe(0, "Edit button should not be visible for contracts");
- }
-
- #endregion
-
- #region Contract Details Verification
-
- ///
- /// Verify contract description on view page
- ///
- public async Task VerifyContractDescription(string expectedDescription)
- {
- var descriptionElement = _page.Locator("p.text-gray-900.font-medium").First;
- var description = await descriptionElement.TextContentAsync();
- description.ShouldNotBeNull();
- description.Trim().ShouldBe(expectedDescription);
- }
-
- ///
- /// Verify contract operator on view page
- ///
- public async Task VerifyContractOperator(string expectedOperator)
- {
- var operatorElements = _page.Locator("p.text-gray-900.font-medium");
- var operatorText = await operatorElements.Nth(1).TextContentAsync();
- operatorText.ShouldNotBeNull();
- operatorText.Trim().ShouldBe(expectedOperator);
- }
-
- ///
- /// Verify the number of products in a contract
- ///
- public async Task VerifyContractProductCount(int expectedCount)
- {
- var productCards = _page.Locator(".border.border-gray-200.rounded-lg.p-4");
- var count = await productCards.CountAsync();
- count.ShouldBe(expectedCount, $"Expected {expectedCount} products but found {count}");
- }
-
- ///
- /// Verify the first product name
- ///
- public async Task VerifyFirstProductName(string expectedName)
- {
- var productNameElement = _page.Locator("h4.text-md.font-semibold.text-gray-900").First;
- var productName = await productNameElement.TextContentAsync();
- productName.ShouldNotBeNull();
- productName.Trim().ShouldBe(expectedName);
- }
-
- ///
- /// Verify the number of transaction fees for the first product
- ///
- public async Task VerifyFirstProductTransactionFeeCount(int expectedCount)
- {
- var feeCards = _page.Locator(".bg-gray-50.rounded.p-3");
- var count = await feeCards.CountAsync();
- count.ShouldBe(expectedCount, $"Expected {expectedCount} transaction fees but found {count}");
- }
-
- ///
- /// Verify a specific transaction fee exists with expected value
- ///
- public async Task VerifyProductTransactionFee(string productName, string feeDescription, string expectedValue)
- {
- var feeCard = _page.Locator($".bg-gray-50.rounded.p-3:has-text('{feeDescription}')");
- await feeCard.WaitForAsync();
-
- var feeText = await feeCard.TextContentAsync();
- feeText.ShouldNotBeNull();
- feeText.ShouldContain(feeDescription);
- feeText.ShouldContain(expectedValue);
- }
-
- #endregion
-
- #region Form Verification
-
- ///
- /// Verify the contract form is displayed
- ///
- public async Task VerifyContractFormIsDisplayed()
- {
- await _page.Locator("form").WaitForAsync();
- var form = await _page.Locator("form").IsVisibleAsync();
- form.ShouldBeTrue("Contract form should be displayed");
- }
-
- ///
- /// Verify a specific field is visible
- ///
- public async Task VerifyFieldIsVisible(string fieldLabel)
- {
- var field = _page.Locator($"label:has-text('{fieldLabel}')");
- await field.WaitForAsync();
- var isVisible = await field.IsVisibleAsync();
- isVisible.ShouldBeTrue($"{fieldLabel} field should be visible");
- }
-
- ///
- /// Verify description field contains specific value on edit page
- ///
- public async Task VerifyDescriptionFieldContains(string expectedValue)
- {
- var descriptionInput = _page.Locator("input[type='text']").First;
- var value = await descriptionInput.InputValueAsync();
- value.ShouldBe(expectedValue);
- }
-
- ///
- /// Verify operator name is displayed on edit page
- ///
- public async Task VerifyOperatorNameDisplayed(string expectedOperator)
- {
- var operatorText = await _page.Locator("p.text-gray-900.font-medium").First.TextContentAsync();
- operatorText.ShouldNotBeNull();
- operatorText.Trim().ShouldBe(expectedOperator);
- }
-
- ///
- /// Verify products section is displayed on edit page
- ///
- public async Task VerifyProductsSectionIsDisplayed()
- {
- await _page.Locator("h3:has-text('Products')").WaitForAsync();
- }
-
- ///
- /// Verify the number of products listed on edit page
- ///
- public async Task VerifyProductCountOnEditPage(int expectedCount)
- {
- var productCards = _page.Locator(".border.border-gray-200.rounded-lg.p-4");
- var count = await productCards.CountAsync();
- count.ShouldBe(expectedCount, $"Expected {expectedCount} products but found {count}");
- }
-
- ///
- /// Verify Add Product button is visible
- ///
- public async Task VerifyAddProductButtonIsVisible()
- {
- var button = _page.Locator("button:has-text('Add Product')");
- await button.WaitForAsync();
- var isVisible = await button.IsVisibleAsync();
- isVisible.ShouldBeTrue("Add Product button should be visible");
- }
-
- ///
- /// Verify validation errors are displayed
- ///
- public async Task VerifyValidationErrorsAreDisplayed()
- {
- var errors = _page.Locator(".text-red-600.text-sm");
- var count = await errors.CountAsync();
- count.ShouldBeGreaterThan(0, "Validation errors should be displayed");
- }
-
- ///
- /// Verify specific validation error is shown
- ///
- public async Task VerifyValidationErrorMessage(string errorMessage)
- {
- await _page.Locator($"text={errorMessage}").WaitForAsync();
- }
-
- #endregion
-
- #region Interaction Methods
-
- ///
- /// Click on View button for the first contract
- ///
- public async Task ClickViewButtonForFirstContract()
- {
- var viewButton = _page.Locator("button:has-text('View')").First;
- await viewButton.ClickAsync();
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- }
-
- ///
- /// Click on Edit button for the first contract
- ///
- public async Task ClickEditButtonForFirstContract()
- {
- var editButton = _page.Locator("button:has-text('Edit')").First;
- await editButton.ClickAsync();
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- }
-
- ///
- /// Click on Add New Contract button
- ///
- public async Task ClickAddNewContractButton()
- {
- await _page.Locator("#newContractButton").ClickAsync();
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- }
-
- ///
- /// Enter text in a form field
- ///
- public async Task EnterTextInField(string fieldLabel, string text)
- {
- // Find the input associated with the label
- var input = _page.Locator($"label:has-text('{fieldLabel}')").Locator("..").Locator("input");
- await input.FillAsync(text);
- }
-
- ///
- /// Select an option from a dropdown
- ///
- public async Task SelectFromDropdown(string fieldLabel, string option)
- {
- var select = _page.Locator($"label:has-text('{fieldLabel}')").Locator("..").Locator("select");
- await select.SelectOptionAsync(new[] { option });
- }
-
- ///
- /// Click on a button by its text or ID
- ///
- public async Task ClickButton(string buttonText)
- {
- var button = _page.Locator($"button:has-text('{buttonText}')").First;
- await button.ClickAsync();
- await Task.Delay(500); // Wait for any state changes
- }
-
- ///
- /// Click on Create Contract button (with ID)
- ///
- public async Task ClickCreateContractButton()
- {
- await _page.Locator("#createContractButton").ClickAsync();
- await Task.Delay(500);
- }
-
- ///
- /// Verify user is redirected to Contract Management page
- ///
- public async Task VerifyRedirectedToContractManagementPage()
- {
- await _page.WaitForURLAsync("**/contracts");
- var url = _page.Url;
- url.ShouldEndWith("/contracts");
- }
-
- ///
- /// Verify contract is created successfully
- ///
- public async Task VerifyContractCreatedSuccessfully()
- {
- // After creation, we should be on the contracts list page
- await VerifyRedirectedToContractManagementPage();
- }
-
- ///
- /// Click Add Product button
- ///
- public async Task ClickAddProductButton()
- {
- var button = _page.Locator("button:has-text('Add Product')").First;
- await button.ClickAsync();
- await Task.Delay(300);
- }
-
- ///
- /// Verify Add Product modal is displayed
- ///
- public async Task VerifyAddProductModalIsDisplayed()
- {
- await _page.Locator("h3:has-text('Add New Product')").WaitForAsync();
- }
-
- ///
- /// Enter text in product name field
- ///
- public async Task EnterProductName(string productName)
- {
- var input = _page.Locator("label:has-text('Product Name')").Locator("..").Locator("input");
- await input.FillAsync(productName);
- }
-
- ///
- /// Enter text in display text field
- ///
- public async Task EnterDisplayText(string displayText)
- {
- var input = _page.Locator("label:has-text('Display Text')").Locator("..").Locator("input");
- await input.FillAsync(displayText);
- }
-
- ///
- /// Enter value in the value field
- ///
- public async Task EnterValue(string value)
- {
- var input = _page.Locator("label:has-text('Value')").Locator("..").Locator("input");
- await input.FillAsync(value);
- }
-
- ///
- /// Check the Variable Value checkbox
- ///
- public async Task CheckVariableValueCheckbox()
- {
- var checkbox = _page.Locator("input[type='checkbox']");
- await checkbox.CheckAsync();
- }
-
- ///
- /// Click Add Product button in modal
- ///
- public async Task ClickAddProductButtonInModal()
- {
- var button = _page.Locator(".bg-white.rounded-lg.p-6").Locator("button:has-text('Add Product')");
- await button.ClickAsync();
- await Task.Delay(500);
- }
-
- ///
- /// Verify product is added successfully
- ///
- public async Task VerifyProductAddedSuccessfully()
- {
- // Wait for modal to close
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- var modalCount = await _page.Locator("h3:has-text('Add New Product')").CountAsync();
- modalCount.ShouldBe(0, "Add Product modal should be closed after successful addition");
- }
-
- ///
- /// Verify Add Product modal is closed
- ///
- public async Task VerifyAddProductModalIsClosed()
- {
- var modalCount = await _page.Locator("h3:has-text('Add New Product')").CountAsync();
- modalCount.ShouldBe(0, "Add Product modal should be closed");
- }
-
- ///
- /// Click Add Fee button for the first product
- ///
- public async Task ClickAddFeeButtonForFirstProduct()
- {
- var button = _page.Locator("button:has-text('Add Fee')").First;
- await button.ClickAsync();
- await Task.Delay(300);
- }
-
- ///
- /// Verify Add Transaction Fee modal is displayed
- ///
- public async Task VerifyAddTransactionFeeModalIsDisplayed()
- {
- await _page.Locator("h3:has-text('Add Transaction Fee')").WaitForAsync();
- }
-
- ///
- /// Enter text in fee description field
- ///
- public async Task EnterFeeDescription(string description)
- {
- var input = _page.Locator("label:has-text('Description')").Locator("..").Locator("input");
- await input.FillAsync(description);
- }
-
- ///
- /// Select calculation type from dropdown
- ///
- public async Task SelectCalculationType(string calculationType)
- {
- var select = _page.Locator("label:has-text('Calculation Type')").Locator("..").Locator("select");
-
- // Map text to value with proper error handling
- var value = calculationType.ToLower() switch
- {
- "fixed" => "0",
- "percentage" => "1",
- _ => throw new ArgumentException($"Unknown calculation type: {calculationType}. Expected 'Fixed' or 'Percentage'.")
- };
- await select.SelectOptionAsync(new[] { value });
- }
-
- ///
- /// Select fee type from dropdown
- ///
- public async Task SelectFeeType(string feeType)
- {
- var select = _page.Locator("label:has-text('Fee Type')").Locator("..").Locator("select");
-
- // Map text to value with proper error handling
- var value = feeType.ToLower() switch
- {
- "merchant" => "0",
- "service provider" => "1",
- _ => throw new ArgumentException($"Unknown fee type: {feeType}. Expected 'Merchant' or 'Service Provider'.")
- };
- await select.SelectOptionAsync(new[] { value });
- }
-
- ///
- /// Enter fee value
- ///
- public async Task EnterFeeValue(string value)
- {
- var input = _page.Locator("label:has-text('Fee Value')").Locator("..").Locator("input");
- await input.FillAsync(value);
- }
-
- ///
- /// Click Add Fee button in modal
- ///
- public async Task ClickAddFeeButtonInModal()
- {
- var button = _page.Locator(".bg-white.rounded-lg.p-6").Locator("button:has-text('Add Fee')");
- await button.ClickAsync();
- await Task.Delay(500);
- }
-
- ///
- /// Verify transaction fee is added successfully
- ///
- public async Task VerifyTransactionFeeAddedSuccessfully()
- {
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
- var modalCount = await _page.Locator("h3:has-text('Add Transaction Fee')").CountAsync();
- modalCount.ShouldBe(0, "Add Transaction Fee modal should be closed after successful addition");
- }
-
- ///
- /// Verify Add Transaction Fee modal is closed
- ///
- public async Task VerifyAddTransactionFeeModalIsClosed()
- {
- var modalCount = await _page.Locator("h3:has-text('Add Transaction Fee')").CountAsync();
- modalCount.ShouldBe(0, "Add Transaction Fee modal should be closed");
- }
-
- #endregion
-}
diff --git a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs
index 4e413d68..d8c24910 100644
--- a/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs
+++ b/EstateManagementUI.IntegrationTests/Common/DashboardPageHelper.cs
@@ -1,242 +1,451 @@
+using System.Runtime.CompilerServices;
using Microsoft.Playwright;
using Shouldly;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.IO;
+using System.Text;
+using Shared.IntegrationTesting;
namespace EstateManagementUI.IntegrationTests.Common;
-///
-/// Helper class for interacting with the Dashboard page using Playwright
-///
-public class DashboardPageHelper
+public sealed class DashboardPageHelper
{
private readonly IPage _page;
- private readonly string _baseUrl;
+ private readonly TestingContext TestingContext;
- public DashboardPageHelper(IPage page, string baseUrl)
- {
+ public DashboardPageHelper(IPage page, TestingContext testingContext) {
_page = page;
- _baseUrl = baseUrl;
+ this.TestingContext = testingContext;
}
- #region Navigation
+ public async Task NavigateToAppAddressAsync()
+ {
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ var baseUrl = ResolveBaseUrl();
+ await _page.GotoAsync(baseUrl);
+ await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+ }, nameof(NavigateToAppAddressAsync));
+ }
- ///
- /// Navigate to the home/dashboard page
- ///
- public async Task NavigateToDashboard()
+ public async Task ClickSignInButtonAsync()
{
- await _page.GotoAsync(_baseUrl);
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ var signInButton = _page.Locator("#loginButton");
+
+ (await signInButton.IsVisibleAsync()).ShouldBeTrue();
+ Console.WriteLine($"Sign in before click: {_page.Url}");
+
+ await signInButton.ClickAsync(new LocatorClickOptions { NoWaitAfter = true });
+ await WaitForAuthenticationNavigationAsync();
+ Console.WriteLine($"Sign in after click: {_page.Url}");
+ Console.WriteLine($"Sign in title after click: {await _page.TitleAsync()}");
+ Console.WriteLine($"Sign in body after click: {await _page.Locator("body").InnerTextAsync()}");
+ }, nameof(ClickSignInButtonAsync));
}
- #endregion
+ public async Task AssertLoginScreenVisibleAsync()
+ {
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await WaitForAuthenticationNavigationAsync();
+
+ (await WaitForAnyVisibleAsync(
+ "#Input_Username",
+ "input[name='Input.Username']",
+ "#Input_UserName",
+ "input[name='Input.UserName']",
+ "#Username",
+ "input[name='Username']",
+ "input[name='username']",
+ "input[name='UserName']",
+ "input[name='Email']",
+ "input[type='email']",
+ "input[type='text']",
+ "input[autocomplete='username']")).ShouldBeTrue();
- #region Verification Methods
+ (await WaitForAnyVisibleAsync(
+ "#Input_Password",
+ "input[name='Input.Password']",
+ "#Input_PasswordInput",
+ "#Password",
+ "input[name='Password']",
+ "input[name='password']",
+ "input[name='current-password']",
+ "input[type='password']",
+ "input[autocomplete='current-password']")).ShouldBeTrue();
+ }, nameof(AssertLoginScreenVisibleAsync));
+ }
- ///
- /// Verify the page title is "Dashboard"
- ///
- public async Task VerifyDashboardPageTitle()
+ public async Task LoginAsync(string username, string password)
{
- var title = await _page.TitleAsync();
- title.ShouldBe("Dashboard");
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await FillFirstVisibleAsync(
+ username,
+ "#Input_Username",
+ "input[name='Input.Username']",
+ "#Username",
+ "input[name='Username']",
+ "input[name='username']",
+ "input[type='email']",
+ "input[type='text']",
+ "input[autocomplete='username']");
+
+ await FillFirstVisibleAsync(
+ password,
+ "#Input_Password",
+ "input[name='Input.Password']",
+ "#Password",
+ "input[name='Password']",
+ "input[name='password']",
+ "input[type='password']",
+ "input[autocomplete='current-password']");
+
+ await ClickFirstVisibleAsync(
+ "button[type='submit']",
+ "input[type='submit']",
+ "button:has-text('Sign In')",
+ "button:has-text('Login')");
+
+ await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+ }, nameof(LoginAsync));
}
- ///
- /// Verify the Administrator welcome message is displayed
- ///
- public async Task VerifyAdministratorWelcomeMessage()
+ public async Task AssertDashboardShellVisibleAsync()
{
- var heading = await _page.Locator("h2:has-text('Welcome, Administrator')").TextContentAsync();
- heading.ShouldNotBeNull();
- heading.ShouldContain("Welcome, Administrator");
-
- var description = await _page.Locator("p:has-text('administrative access')").TextContentAsync();
- description.ShouldNotBeNull();
- description.ShouldContain("administrative access to manage system permissions");
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ (await _page.GetByRole(AriaRole.Heading, new() { Name = "Dashboard" }).IsVisibleAsync()).ShouldBeTrue();
+ (await _page.GetByText("Welcome to Estate Management System").IsVisibleAsync()).ShouldBeTrue();
+ (await _page.Locator("#dashboardLink").IsVisibleAsync()).ShouldBeTrue();
+ }, nameof(AssertDashboardShellVisibleAsync));
}
- ///
- /// Verify that KPI cards are visible on the dashboard
- ///
- public async Task VerifyKpiCardsAreVisible()
+ public async Task AssertHomePageVisibleAsync()
{
- await _page.Locator("text=Merchants with Sales (Last Hour)").WaitForAsync();
- await _page.Locator("text=Merchants with No Sales Today").WaitForAsync();
- await _page.Locator("text=Merchants with No Sales (7 Days)").WaitForAsync();
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ (await _page.TitleAsync()).ShouldBe("Welcome - Estate Management");
+ (await _page.Locator("#loginButton").IsVisibleAsync()).ShouldBeTrue();
+ }, nameof(AssertHomePageVisibleAsync));
}
- ///
- /// Verify that KPI cards are NOT visible (Administrator role)
- ///
- public async Task VerifyKpiCardsAreNotVisible()
+ public async Task AssertDashboardWelcomeMessageVisibleAsync()
{
- var salesLastHourCount = await _page.Locator("text=Merchants with Sales (Last Hour)").CountAsync();
- salesLastHourCount.ShouldBe(0, "KPI cards should not be visible for Administrator role");
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ (await _page.GetByText("Welcome to Estate Management System").IsVisibleAsync()).ShouldBeTrue();
+ }, nameof(AssertDashboardWelcomeMessageVisibleAsync));
}
- ///
- /// Verify Merchant KPI values match expected hardcoded test data
- ///
- public async Task VerifyMerchantKpiValues(int salesLastHour, int noSalesToday, int noSales7Days)
+ public async Task AssertEstateDashboardVisibleAsync()
{
- // Wait for KPI cards to load
- await _page.Locator("text=Merchants with Sales (Last Hour)").WaitForAsync();
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await AssertDashboardShellVisibleAsync();
+ await AssertComparisonDateSelectorVisibleAsync();
+ await AssertMerchantKpiSummaryCardsVisibleAsync();
+ await AssertSalesComparisonCardsVisibleAsync();
+ await AssertRecentMerchantsSectionVisibleAsync();
+ }, nameof(AssertEstateDashboardVisibleAsync));
+ }
- // Verify Merchants with Sales in Last Hour
- var salesLastHourCard = _page.Locator(".info-box").Filter(new LocatorFilterOptions
- {
- HasText = "Merchants with Sales (Last Hour)"
- });
- var salesLastHourValue = await salesLastHourCard.Locator(".info-box-number").TextContentAsync();
- salesLastHourValue.ShouldNotBeNull();
- int.Parse(salesLastHourValue.Trim()).ShouldBe(salesLastHour);
-
- // Verify Merchants with No Sales Today
- var noSalesTodayCard = _page.Locator(".info-box").Filter(new LocatorFilterOptions
- {
- HasText = "Merchants with No Sales Today"
- });
- var noSalesTodayValue = await noSalesTodayCard.Locator(".info-box-number").TextContentAsync();
- noSalesTodayValue.ShouldNotBeNull();
- int.Parse(noSalesTodayValue.Trim()).ShouldBe(noSalesToday);
-
- // Verify Merchants with No Sales in Last 7 Days
- var noSales7DaysCard = _page.Locator(".info-box").Filter(new LocatorFilterOptions
- {
- HasText = "Merchants with No Sales (7 Days)"
- });
- var noSales7DaysValue = await noSales7DaysCard.Locator(".info-box-number").TextContentAsync();
- noSales7DaysValue.ShouldNotBeNull();
- int.Parse(noSales7DaysValue.Trim()).ShouldBe(noSales7Days);
+ public async Task AssertAdministratorDashboardVisibleAsync()
+ {
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await AssertDashboardShellVisibleAsync();
+ (await _page.GetByRole(AriaRole.Heading, new() { Name = "Welcome, Administrator" }).IsVisibleAsync()).ShouldBeTrue();
+ }, nameof(AssertAdministratorDashboardVisibleAsync));
}
- ///
- /// Verify Today's Sales card is displayed
- ///
- public async Task VerifyTodaysSalesCardIsDisplayed()
+ public async Task AssertComparisonDateSelectorVisibleAsync()
{
- await _page.Locator("h3:has-text(\"Today's Sales\")").WaitForAsync();
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ var selector = _page.Locator("#comparisonDateSelector");
+ var deadline = DateTime.UtcNow.AddSeconds(10);
+
+ while (DateTime.UtcNow < deadline)
+ {
+ if (await selector.IsVisibleAsync())
+ {
+ return;
+ }
+
+ await _page.WaitForTimeoutAsync(250);
+ }
+
+ (await _page.Locator("#comparisonDateSelector").IsVisibleAsync()).ShouldBeTrue();
+ }, nameof(AssertComparisonDateSelectorVisibleAsync));
}
- ///
- /// Verify Today's Sales values
- ///
- public async Task VerifyTodaysSalesValues(int todayCount, decimal todayValue)
+ public async Task AssertMerchantKpiSummaryCardsVisibleAsync()
{
- var salesCard = _page.Locator(".card").Filter(new LocatorFilterOptions
- {
- HasText = "Today's Sales"
- });
-
- // Wait for the card to be visible
- await salesCard.WaitForAsync();
+ //await AssertInfoBoxVisibleAsync("Merchants with Sales (Last Hour)", "45");
+ //await AssertInfoBoxVisibleAsync("Merchants with No Sales Today", "12");
+ //await AssertInfoBoxVisibleAsync("Merchants with No Sales (7 Days)", "5");
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await AssertInfoBoxVisibleAsync("Merchants with Sales (Last Hour)", "0");
+ await AssertInfoBoxVisibleAsync("Merchants with No Sales Today", "0");
+ await AssertInfoBoxVisibleAsync("Merchants with No Sales (7 Days)", "0");
+ }, nameof(AssertMerchantKpiSummaryCardsVisibleAsync));
+ }
- // Verify today's sales count
- var todayTransactions = await salesCard.Locator("p:has-text('transactions')").First.TextContentAsync();
- todayTransactions.ShouldNotBeNull();
- todayTransactions.ShouldContain($"{todayCount} transactions");
+ public async Task AssertSalesComparisonCardsVisibleAsync()
+ {
+ //await AssertCardVisibleAsync("Today's Sales", "523 transactions", new Regex(@"[£$]145,000\.00"));
+ //await AssertCardVisibleAsync("Failed Sales (Low Credit)", "15 transactions", new Regex(@"[£$]850\.00"));
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await AssertCardVisibleAsync("Today's Sales", "0 transactions", new Regex(@"[£$¤]\s?0(?:,000)?\.00"));
+ await AssertCardVisibleAsync("Failed Sales (Low Credit)", "0 transactions", new Regex(@"[£$¤]\s?0(?:,000)?\.00"));
+ }, nameof(AssertSalesComparisonCardsVisibleAsync));
+ }
- // Verify today's sales value is displayed (currency format)
- var todayValueText = await salesCard.Locator(".text-2xl.font-bold").First.TextContentAsync();
- todayValueText.ShouldNotBeNull();
- // Just verify value is present and formatted as currency
- todayValueText.ShouldContain("$");
+ public async Task AssertRecentMerchantsSectionVisibleAsync()
+ {
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ (await _page.GetByRole(AriaRole.Heading, new() { Name = "Recently Created Merchants" }).IsVisibleAsync()).ShouldBeTrue();
+ }, nameof(AssertRecentMerchantsSectionVisibleAsync));
}
- ///
- /// Verify Failed Sales card is displayed
- ///
- public async Task VerifyFailedSalesCardIsDisplayed()
+ public async Task AssertMerchantKpiSummaryCardsNotVisibleAsync()
{
- await _page.Locator("h3:has-text('Failed Sales (Low Credit)')").WaitForAsync();
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await AssertInfoBoxAbsentAsync("Merchants with Sales (Last Hour)");
+ await AssertInfoBoxAbsentAsync("Merchants with No Sales Today");
+ await AssertInfoBoxAbsentAsync("Merchants with No Sales (7 Days)");
+ }, nameof(AssertMerchantKpiSummaryCardsNotVisibleAsync));
}
- ///
- /// Verify Failed Sales values
- ///
- public async Task VerifyFailedSalesValues(int todayCount)
+ public async Task AssertSalesComparisonCardsNotVisibleAsync()
{
- var failedSalesCard = _page.Locator(".card").Filter(new LocatorFilterOptions
- {
- HasText = "Failed Sales (Low Credit)"
- });
-
- // Wait for the card to be visible
- await failedSalesCard.WaitForAsync();
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ await AssertCardAbsentAsync("Today's Sales");
+ await AssertCardAbsentAsync("Failed Sales (Low Credit)");
+ }, nameof(AssertSalesComparisonCardsNotVisibleAsync));
+ }
- // Verify today's failed sales count
- var todayTransactions = await failedSalesCard.Locator("p:has-text('transactions')").First.TextContentAsync();
- todayTransactions.ShouldNotBeNull();
- todayTransactions.ShouldContain($"{todayCount} transactions");
+ public async Task AssertRecentMerchantsSectionNotVisibleAsync()
+ {
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ (await _page.GetByRole(AriaRole.Heading, new() { Name = "Recently Created Merchants" }).CountAsync()).ShouldBe(0);
+ }, nameof(AssertRecentMerchantsSectionNotVisibleAsync));
}
- ///
- /// Verify comparison date selector is visible
- ///
- public async Task VerifyComparisonDateSelectorIsVisible()
+ public async Task AssertDashboardNavigationLinkVisibleAsync()
{
- await _page.Locator("label:has-text('Compare to:')").WaitForAsync();
- await _page.Locator("#comparisonDateSelector").WaitForAsync();
+ await RunWithFailureArtifactsAsync(async () =>
+ {
+ (await _page.Locator("#dashboardLink").IsVisibleAsync()).ShouldBeTrue();
+ }, nameof(AssertDashboardNavigationLinkVisibleAsync));
}
- ///
- /// Verify comparison date selector is NOT visible (Administrator role)
- ///
- public async Task VerifyComparisonDateSelectorIsNotVisible()
+ private async Task AssertInfoBoxVisibleAsync(string label, string expectedValue)
{
- var selectorCount = await _page.Locator("#comparisonDateSelector").CountAsync();
- selectorCount.ShouldBe(0, "Comparison date selector should not be visible for Administrator role");
+ var card = _page.Locator(".info-box").Filter(new() { HasText = label });
+
+ (await card.IsVisibleAsync()).ShouldBeTrue();
+ (await card.Locator(".info-box-number").InnerTextAsync()).ShouldBe(expectedValue);
}
- ///
- /// Verify Recently Created Merchants section is visible
- ///
- public async Task VerifyRecentlyCreatedMerchantsIsVisible()
+ private async Task AssertInfoBoxAbsentAsync(string label)
{
- await _page.Locator("h3:has-text('Recently Created Merchants')").WaitForAsync();
+ var card = _page.Locator(".info-box").Filter(new() { HasText = label });
+ (await card.CountAsync()).ShouldBe(0);
}
- ///
- /// Verify Recently Created Merchants section is NOT visible (Administrator role)
- ///
- public async Task VerifyRecentlyCreatedMerchantsIsNotVisible()
+ private async Task AssertCardVisibleAsync(string heading, params object[] expectedTexts)
{
- var merchantsCount = await _page.Locator("h3:has-text('Recently Created Merchants')").CountAsync();
- merchantsCount.ShouldBe(0, "Recently Created Merchants should not be visible for Administrator role");
+ var card = _page.Locator("div.card").Filter(new()
+ {
+ Has = _page.GetByRole(AriaRole.Heading, new() { Name = heading })
+ });
+
+ (await card.IsVisibleAsync()).ShouldBeTrue();
+
+ foreach (var expectedText in expectedTexts)
+ {
+ var locator = expectedText is Regex regex
+ ? card.GetByText(regex).First
+ : card.GetByText(expectedText.ToString()!).First;
+ (await locator.IsVisibleAsync()).ShouldBeTrue();
+ }
}
- ///
- /// Verify that at least one merchant is displayed in the Recently Created Merchants section
- ///
- public async Task VerifyRecentlyCreatedMerchantsHasData()
+ private async Task AssertCardAbsentAsync(string heading)
{
- var merchantsCard = _page.Locator(".card").Filter(new LocatorFilterOptions
- {
- HasText = "Recently Created Merchants"
+ var card = _page.Locator("div.card").Filter(new()
+ {
+ Has = _page.GetByRole(AriaRole.Heading, new() { Name = heading })
});
-
- await merchantsCard.WaitForAsync();
-
- // Check that at least one merchant is displayed
- var merchantItems = merchantsCard.Locator(".flex.items-center.justify-between");
- var count = await merchantItems.CountAsync();
- count.ShouldBeGreaterThan(0, "At least one merchant should be displayed");
+ (await card.CountAsync()).ShouldBe(0);
+ }
+
+ private async Task WaitForAnyVisibleAsync(params string[] selectors)
+ {
+ var deadline = DateTime.UtcNow.AddSeconds(30);
+
+ while (DateTime.UtcNow < deadline)
+ {
+ if (await IsAnyVisibleAsync(selectors))
+ {
+ return true;
+ }
+
+ await _page.WaitForTimeoutAsync(250);
+ }
+
+ return false;
+ }
+
+ private async Task IsAnyVisibleAsync(params string[] selectors)
+ {
+ foreach (var selector in selectors)
+ {
+ var locator = _page.Locator(selector);
+ if (await locator.CountAsync() > 0 && await locator.First.IsVisibleAsync())
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private async Task FillFirstVisibleAsync(string value, params string[] selectors)
+ {
+ foreach (var selector in selectors)
+ {
+ var locator = _page.Locator(selector);
+ if (await locator.CountAsync() > 0)
+ {
+ var first = locator.First;
+ if (await first.IsVisibleAsync())
+ {
+ await first.FillAsync(value);
+ return;
+ }
+ }
+ }
+
+ throw new InvalidOperationException($"Could not find a visible input for selectors: {string.Join(", ", selectors)}");
}
- #endregion
+ private async Task ClickFirstVisibleAsync(params string[] selectors)
+ {
+ foreach (var selector in selectors)
+ {
+ var locator = _page.Locator(selector);
+ if (await locator.CountAsync() > 0)
+ {
+ var first = locator.First;
+ if (await first.IsVisibleAsync())
+ {
+ await first.ClickAsync(new LocatorClickOptions { NoWaitAfter = true });
+ return;
+ }
+ }
+ }
+
+ throw new InvalidOperationException($"Could not find a visible clickable element for selectors: {string.Join(", ", selectors)}");
+ }
+
+ private async Task WaitForAuthenticationNavigationAsync()
+ {
+ try
+ {
+ await _page.WaitForURLAsync(
+ new Regex(@".*/(login|connect/authorize).*", RegexOptions.IgnoreCase),
+ new PageWaitForURLOptions
+ {
+ Timeout = 60000
+ });
+ }
+ catch
+ {
+ // If the URL is already where we need it, keep going and let the selector wait decide.
+ }
+ }
- #region Interaction Methods
+ private string ResolveBaseUrl()
+ {
+ var hostPort = this.TestingContext.DockerHelper.GetHostPort(ContainerType.EstateManagementUI);
+ return $"https://127.0.0.1:{hostPort}";
+ }
- ///
- /// Select a comparison date from the dropdown
- ///
- public async Task SelectComparisonDate(string dateDescription)
+ private async Task RunWithFailureArtifactsAsync(Func action, string context)
{
- await _page.Locator("#comparisonDateSelector").SelectOptionAsync(new[] { dateDescription });
- // Wait for dashboard to reload
- await Task.Delay(500); // Small delay for state update
- await _page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+ try
+ {
+ await action();
+ }
+ catch (Exception ex)
+ {
+ await CaptureDebugArtifactsAsync(context, ex);
+ throw;
+ }
}
- #endregion
+ private async Task CaptureDebugArtifactsAsync(string context, Exception exception)
+ {
+ try
+ {
+ var outputDirectory = Path.Combine(Environment.CurrentDirectory, "TestResults", "Diagnostics");
+ Directory.CreateDirectory(outputDirectory);
+
+ var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
+ var safeContext = context.Replace(" ", "_");
+ var artifactPath = Path.Combine(outputDirectory, $"failure-{safeContext}-{timestamp}.txt");
+
+ var bodyText = string.Empty;
+ try
+ {
+ bodyText = await _page.Locator("body").InnerTextAsync();
+ }
+ catch
+ {
+ bodyText = "";
+ }
+
+ var html = string.Empty;
+ try
+ {
+ html = await _page.ContentAsync();
+ }
+ catch
+ {
+ html = "";
+ }
+
+ var content = new StringBuilder();
+ content.AppendLine($"Context: {context}");
+ content.AppendLine($"Exception: {exception.GetType().FullName}");
+ content.AppendLine($"Message: {exception.Message}");
+ content.AppendLine($"Url: {_page.Url}");
+ content.AppendLine($"Title: {await _page.TitleAsync()}");
+ content.AppendLine();
+ content.AppendLine("Body:");
+ content.AppendLine(bodyText);
+ content.AppendLine();
+ content.AppendLine("Html:");
+ content.AppendLine(html);
+
+ await File.WriteAllTextAsync(artifactPath, content.ToString());
+ Console.WriteLine($"Failure diagnostics saved to: {artifactPath}");
+ }
+ catch (Exception captureException)
+ {
+ Console.WriteLine($"Failed to capture debug artifacts: {captureException.Message}");
+ }
+ }
}
diff --git a/EstateManagementUI.IntegrationTests/Common/DockerHelper.cs b/EstateManagementUI.IntegrationTests/Common/DockerHelper.cs
new file mode 100644
index 00000000..3df9b31f
--- /dev/null
+++ b/EstateManagementUI.IntegrationTests/Common/DockerHelper.cs
@@ -0,0 +1,691 @@
+using Ductus.FluentDocker.Common;
+using Ductus.FluentDocker.Services;
+using EventStore.Client;
+using NLog;
+using Reqnroll;
+using SecurityService.Client;
+using SecurityService.DataTransferObjects;
+using Shared.IntegrationTesting;
+using Shared.IntegrationTesting.TestContainers;
+using Shared.Logger;
+using Shared.Serialisation;
+using Shouldly;
+using System.Diagnostics;
+using TransactionProcessor.Client;
+using TransactionProcessor.IntegrationTesting.Helpers;
+using ContainerBuilder = DotNet.Testcontainers.Builders.ContainerBuilder;
+using ReqnrollTableHelper = Shared.IntegrationTesting.ReqnrollTableHelper;
+using String = System.String;
+
+namespace EstateManagementUI.IntegrationTests.Common
+{
+ public class DockerHelper : global::Shared.IntegrationTesting.TestContainers.DockerHelper
+ {
+ #region Fields
+
+ public ITransactionProcessorClient TransactionProcessorClient;
+
+ public HttpClient HttpClient;
+
+ public ISecurityServiceClient SecurityServiceClient;
+
+ public EventStoreProjectionManagementClient ProjectionManagementClient;
+
+ public HttpClient TestHostHttpClient;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ public DockerHelper()
+ {
+ this.TestingContext = new TestingContext();
+ StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions()));
+ }
+
+ String Serialise(Object arg)
+ {
+ return StringSerialiser.Serialise