From 74580291a7d315a065aa46a82ed9074f0c1ffb17 Mon Sep 17 00:00:00 2001 From: StylusFrost Date: Thu, 23 Oct 2025 14:52:24 +0200 Subject: [PATCH] test(ui): refactor provider deletion logic and improve error handling - Updated the ProvidersPage class to include a new method for deleting a provider if it exists, enhancing the test setup by ensuring a clean state. - Improved error handling during the test connection process to provide clearer feedback on failures. - Refactored existing tests to utilize the new deletion method, streamlining the test code and improving maintainability. --- ui/tests/helpers.ts | 7 - ui/tests/providers/providers-page.ts | 223 ++++++++++++++++----------- ui/tests/providers/providers.spec.ts | 51 ++++-- ui/tests/scans/scans-page.ts | 28 ++++ 4 files changed, 199 insertions(+), 110 deletions(-) create mode 100644 ui/tests/scans/scans-page.ts diff --git a/ui/tests/helpers.ts b/ui/tests/helpers.ts index fbe151d17d..2bc9aa4672 100644 --- a/ui/tests/helpers.ts +++ b/ui/tests/helpers.ts @@ -188,10 +188,3 @@ export async function verifySessionValid(page: Page) { expect(session.refreshToken).toBeTruthy(); return session; } - -export async function deleteProviderIfExists(page: Page, accountId: string) { - const providersPage = new ProvidersPage(page); - if (await providersPage.verifyProviderExists(accountId)) { - await providersPage.actionDeleteProvider(accountId); - } -} diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index 66b77668e7..096ed35e03 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -129,10 +129,11 @@ export class ProvidersPage extends BasePage { this.accessKeyIdInput = page.getByRole("textbox", { name: "Access Key ID" }); this.secretAccessKeyInput = page.getByRole("textbox", { name: "Secret Access Key" }); - // Delete button - this.deleteProviderConfirmationButton = page.locator( - 'button[aria-label="Delete"]', - ); + // Delete button in confirmation modal + this.deleteProviderConfirmationButton = page.getByRole("button", { + name: "Delete", + exact: true, + }); } async goto(): Promise { @@ -150,7 +151,6 @@ export class ProvidersPage extends BasePage { async selectAWSProvider(): Promise { // Prefer label-based click for radios, force if overlay intercepts - await this.awsProviderRadio.click({ force: true }); await this.waitForPageLoad(); } @@ -204,6 +204,43 @@ export class ProvidersPage extends BasePage { await buttonByText.click(); await this.waitForPageLoad(); + + // Wait for either success (redirect to scans) or error message to appear + // The error container has multiple p.text-danger elements, we want the first one with the technical error + const errorMessage = this.page.locator("p.text-danger").first(); + + try { + // Wait up to 15 seconds for either the error message or redirect + await Promise.race([ + // Wait for error message to appear + errorMessage.waitFor({ state: "visible", timeout: 15000 }), + // Wait for redirect to scans page (success case) + this.page.waitForURL(/\/scans/, { timeout: 15000 }), + ]); + + // If we're still on test-connection page, check for error + if (/\/providers\/test-connection/.test(this.page.url())) { + const isErrorVisible = await errorMessage.isVisible().catch(() => false); + if (isErrorVisible) { + const errorText = await errorMessage.textContent(); + throw new Error( + `Test connection failed with error: ${errorText?.trim() || "Unknown error"}`, + ); + } + } + } catch (error) { + // If timeout or other error, check if error message is present + const isErrorVisible = await errorMessage.isVisible().catch(() => false); + if (isErrorVisible) { + const errorText = await errorMessage.textContent(); + throw new Error( + `Test connection failed with error: ${errorText?.trim() || "Unknown error"}`, + ); + } + // Re-throw original error if no error message found + throw error; + } + return; } @@ -316,20 +353,10 @@ export class ProvidersPage extends BasePage { await expect(launchScanButton).toBeVisible(); } - async verifyProviderExists(providerUID: string): Promise { - // Filter search by providerUID - - await this.filterProvidersByProviderUID(providerUID); - // Verify if table has 1 row - if (!(await this.verifySingleRowForProviderUID(providerUID))) { - return false; - } - return true; - } - async verifyLoadProviderPageAfterNewProvider(): Promise { // Verify the provider page is loaded + await this.waitForPageLoad(); await expect(this.page).toHaveTitle(/Prowler/); await expect(this.providersTable).toBeVisible(); } @@ -350,95 +377,119 @@ export class ProvidersPage extends BasePage { return true; } - async filterProvidersByProviderUID(providerUID: string): Promise { - // Filter providers by providerUID + async deleteProviderIfExists(providerUID: string): Promise { + // Delete the provider if it exists - // Go to the providers page + // Navigate to providers page await this.goto(); + await expect(this.providersTable).toBeVisible({ timeout: 10000 }); - // Verify the providers table is visible - await expect(this.providersTable).toBeVisible(); - - // Get the search input + // Find and use the search input to filter the table const searchInput = this.page.getByPlaceholder(/search|filter/i); - if (!searchInput) { - throw new Error("No search input available"); - } - - // Clear existing content and type the providerUID - await searchInput.fill(""); - await searchInput.type(providerUID, { delay: 20 }); - - // Try to submit the filter if the input acts on Enter - try { - await searchInput.press("Enter"); - } catch (error) { - // Enter press might not be needed for all UIs - } - - // Wait for table to update + await expect(searchInput).toBeVisible({ timeout: 5000 }); + + // Clear and search for the specific provider + await searchInput.clear(); + await searchInput.fill(providerUID); + await searchInput.press("Enter"); + + // Wait for the table to finish loading/filtering await this.waitForPageLoad(); - } + + // Additional wait for React table to re-render with the server-filtered data + // The filtering happens on the server, but the table component needs time + // to process the response and update the DOM after network idle + await this.page.waitForTimeout(1500); - async actionDeleteProvider(providerUID: string): Promise { - // Delete the provider + // Get all rows from the table + const allRows = this.providersTable.locator("tbody tr"); + + // Helper function to check if a row is the "No results" row + const isNoResultsRow = async (row: Locator): Promise => { + const text = await row.textContent(); + return text?.includes("No results") || text?.includes("No data") || false; + }; - // Filter search by providerUID - await this.filterProvidersByProviderUID(providerUID); + // Helper function to find the row with the specific UID + const findProviderRow = async (): Promise => { + const count = await allRows.count(); + + for (let i = 0; i < count; i++) { + const row = allRows.nth(i); + + // Skip "No results" rows + if (await isNoResultsRow(row)) { + continue; + } + + // Check if this row contains the UID in the UID column (column 3) + const uidCell = row.locator("td").nth(3); + const uidText = await uidCell.textContent(); + + if (uidText?.includes(providerUID)) { + return row; + } + } + + return null; + }; - // Verify the providers table is visible - await expect(this.providersTable).toBeVisible(); + // Wait for filtering to complete (max 0 or 1 data rows) + await expect(async () => { + const targetRow = await findProviderRow(); + const count = await allRows.count(); + + // Count only real data rows (not "No results") + let dataRowCount = 0; + for (let i = 0; i < count; i++) { + if (!(await isNoResultsRow(allRows.nth(i)))) { + dataRowCount++; + } + } + + // Should have 0 or 1 data row + expect(dataRowCount).toBeLessThanOrEqual(1); + }).toPass({ timeout: 20000 }); - // Find the first row in the filtered results - const firstRow = this.providersTable.locator("tbody tr").first(); + // Find the provider row + const targetRow = await findProviderRow(); + + if (!targetRow) { + // Provider not found, nothing to delete + // Navigate back to providers page to ensure clean state + await this.goto(); + await expect(this.providersTable).toBeVisible({ timeout: 10000 }); + return; + } - // Verify the first row is visible - await expect(firstRow).toBeVisible(); - - // Get button in the last cell - const lastCell = firstRow.locator("td").last(); - const actionButton = lastCell.locator("button").first(); - - // Verify the action button is visible + // Find and click the action button (last cell = actions column) + const actionButton = targetRow.locator("td").last().locator("button").first(); + await expect(actionButton).toBeVisible({ timeout: 5000 }); await actionButton.click(); - // Wait for the dropdown menu to appear - await this.page.waitForSelector('[role="menu"], [role="menuitem"]', { - timeout: 5000, - }); - - // Find the delete menu item + // Wait for dropdown menu to appear and find delete option const deleteMenuItem = this.page.getByRole("menuitem", { name: /delete.*provider/i, }); + await expect(deleteMenuItem).toBeVisible({ timeout: 5000 }); + await deleteMenuItem.click(); - // Verify the delete menu item is visible - await expect(deleteMenuItem).toBeVisible(); + // Wait for confirmation modal to appear + const modal = this.page.locator('[role="dialog"], .modal, [data-testid*="modal"]').first(); + await expect(modal).toBeVisible({ timeout: 10000 }); - // Click the delete menu item with force to handle unstable elements - await deleteMenuItem.click({ force: true }); + // Find and click the delete confirmation button + await expect(this.deleteProviderConfirmationButton).toBeVisible({ timeout: 5000 }); + await this.deleteProviderConfirmationButton.click(); - // Wait for the delete confirmation modal to appear - await this.page.waitForSelector( - '[role="dialog"], .modal, [data-testid*="modal"]', - { timeout: 10000 }, - ); + // Wait for modal to close (this indicates deletion was initiated) + await expect(modal).not.toBeVisible({ timeout: 10000 }); - // Find the delete confirmation button with multiple approaches - let deleteButton = this.deleteProviderConfirmationButton; - - await expect(deleteButton).toBeVisible(); - - // Click the delete button with force to handle unstable elements - await deleteButton.click({ force: true }); - - // Wait for the modal to disappear and the page to update - const modalToCheck = this.page - .locator('[role="dialog"], .modal, [data-testid*="modal"]') - .first(); - - // Verify the modal is not visible - await expect(modalToCheck).not.toBeVisible(); + // Wait for page to reload await this.waitForPageLoad(); + + // Navigate back to providers page to ensure clean state + await this.goto(); + await expect(this.providersTable).toBeVisible({ timeout: 10000 }); } } diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index 8aefcb14fe..98c0b14577 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -1,5 +1,5 @@ import { test } from "@playwright/test"; -import * as helpers from "../helpers"; +import { ScansPage } from "../scans/scans-page"; import { ProvidersPage, AWSProviderData, @@ -9,10 +9,9 @@ import { test.describe.serial("Add AWS Provider", () => { - // Providers page object let providersPage: ProvidersPage; - + let scansPage: ScansPage; // Test data from environment variables const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID; const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY; @@ -30,20 +29,25 @@ test.describe.serial("Add AWS Provider", () => { test.beforeEach(async ({ page }) => { providersPage = new ProvidersPage(page); // Clean up existing provider to ensure clean test state - await helpers.deleteProviderIfExists(page, accountId); + await providersPage.deleteProviderIfExists(accountId); }); // Use admin authentication for provider management test.use({ storageState: "playwright/.auth/admin_user.json" }); - test( "should add a new AWS provider with static credentials", { - tag: ["@critical", "@e2e", "@providers", "@aws", "@serial", "@PROVIDER-E2E-001"], + tag: [ + "@critical", + "@e2e", + "@providers", + "@aws", + "@serial", + "@PROVIDER-E2E-001", + ], }, async ({ page }) => { - // Validate required environment variables if (!accountId || !accessKey || !secretKey) { throw new Error( @@ -80,7 +84,9 @@ test.describe.serial("Add AWS Provider", () => { await providersPage.clickNext(); // Select static credentials type - await providersPage.selectCredentialsType(AWS_CREDENTIAL_OPTIONS.AWS_CREDENTIALS); + await providersPage.selectCredentialsType( + AWS_CREDENTIAL_OPTIONS.AWS_CREDENTIALS, + ); await providersPage.verifyCredentialsPageLoaded(); // Fill static credentials @@ -92,17 +98,24 @@ test.describe.serial("Add AWS Provider", () => { await providersPage.clickNext(); // Wait for redirect to provider page - await providersPage.verifyLoadProviderPageAfterNewProvider(); - } - ) + scansPage = new ScansPage(page); + await scansPage.verifyPageLoaded(); + }, + ); test( "should add a new AWS provider with assume role credentials with Access Key and Secret Key", { - tag: ["@critical", "@e2e", "@providers", "@aws","@serial", "@PROVIDER-E2E-002"], + tag: [ + "@critical", + "@e2e", + "@providers", + "@aws", + "@serial", + "@PROVIDER-E2E-002", + ], }, async ({ page }) => { - // Validate required environment variables if (!accountId || !accessKey || !secretKey || !roleArn) { throw new Error( @@ -140,7 +153,9 @@ test.describe.serial("Add AWS Provider", () => { await providersPage.clickNext(); // Select role credentials type - await providersPage.selectCredentialsType(AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN); + await providersPage.selectCredentialsType( + AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN, + ); await providersPage.verifyCredentialsPageLoaded(); // Fill role credentials @@ -152,7 +167,9 @@ test.describe.serial("Add AWS Provider", () => { await providersPage.clickNext(); // Wait for redirect to provider page - await providersPage.verifyLoadProviderPageAfterNewProvider(); - } + scansPage = new ScansPage(page); + await scansPage.verifyPageLoaded(); + }, ); -}); +}); + diff --git a/ui/tests/scans/scans-page.ts b/ui/tests/scans/scans-page.ts new file mode 100644 index 0000000000..beb7d3cea9 --- /dev/null +++ b/ui/tests/scans/scans-page.ts @@ -0,0 +1,28 @@ +import { Page, Locator, expect } from "@playwright/test"; +import { BasePage } from "../base-page"; + +// Scan page +export class ScansPage extends BasePage { + + // Main content elements + readonly scanTable: Locator; + constructor(page: Page) { + super(page); + + // Main content elements + this.scanTable = page.locator("table"); + + } + + // Navigation methods + async goto(): Promise { + await super.goto("/scans"); + } + + // Verification methods + async verifyPageLoaded(): Promise { + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.scanTable).toBeVisible(); + await this.waitForPageLoad(); + } +}