From 1147c0cef4233b6cc96f3e37f6747278a2334525 Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Mon, 23 Feb 2026 16:50:27 +0100 Subject: [PATCH] test(ui): update providers page object for modal wizard --- ui/tests/providers/providers-page.ts | 277 ++++++++++++++++----------- 1 file changed, 166 insertions(+), 111 deletions(-) diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index b96caa8362..9d647118ad 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -194,6 +194,9 @@ export interface AlibabaCloudProviderCredential { // Providers page export class ProvidersPage extends BasePage { + readonly wizardModal: Locator; + readonly wizardTitle: Locator; + // Alias input readonly aliasInput: Locator; @@ -288,6 +291,14 @@ export class ProvidersPage extends BasePage { constructor(page: Page) { super(page); + this.wizardModal = page.getByRole("dialog", { + name: /Adding A Cloud Provider/i, + }); + this.wizardTitle = page.getByRole("heading", { + name: "Adding A Cloud Provider", + exact: true, + }); + // Button to add a new cloud provider this.addProviderButton = page.getByRole("link", { name: "Add Cloud Provider", @@ -507,6 +518,25 @@ export class ProvidersPage extends BasePage { await this.addProviderButton.click(); } + async openProviderWizardModal(): Promise { + await this.clickAddProvider(); + await this.verifyWizardModalOpen(); + } + + async closeProviderWizardModal(): Promise { + await this.page.keyboard.press("Escape"); + await expect(this.wizardModal).not.toBeVisible(); + } + + async verifyWizardModalOpen(): Promise { + await expect(this.wizardModal).toBeVisible(); + await expect(this.wizardTitle).toBeVisible(); + } + + async advanceWizardStep(): Promise { + await this.clickNext(); + } + private async selectProviderRadio(radio: Locator): Promise { // Force click to handle overlay intercepts await radio.click({ force: true }); @@ -536,6 +566,59 @@ export class ProvidersPage extends BasePage { await this.selectProviderRadio(this.githubProviderRadio); } + async selectAWSSingleAccountMethod(): Promise { + await this.page + .getByRole("button", { + name: "Add A Single AWS Cloud Account", + exact: true, + }) + .click(); + } + + async selectAWSOrganizationsMethod(): Promise { + await this.page + .getByRole("button", { + name: "Add Multiple Accounts With AWS Organizations", + exact: true, + }) + .click(); + } + + async verifyOrganizationsAuthenticationStepLoaded(): Promise { + await this.verifyWizardModalOpen(); + await expect( + this.page.getByRole("heading", { + name: /Authentication Details/i, + }), + ).toBeVisible(); + } + + async verifyOrganizationsAccountSelectionStepLoaded(): Promise { + await this.verifyWizardModalOpen(); + await expect( + this.page.getByText( + /Confirm all accounts under this Organization you want to add to Prowler\./i, + ), + ).toBeVisible(); + } + + async verifyOrganizationsLaunchStepLoaded(): Promise { + await this.verifyWizardModalOpen(); + await expect(this.page.getByText(/Accounts Connected!/i)).toBeVisible(); + } + + async chooseOrganizationsScanSchedule(option: "daily" | "single"): Promise { + const trigger = this.page.getByRole("combobox"); + await trigger.click(); + + const optionName = + option === "single" + ? "Run a single scan (no recurring schedule)" + : "Scan Daily (every 24 hours)"; + + await this.page.getByRole("option", { name: optionName }).click(); + } + async fillAWSProviderDetails(data: AWSProviderData): Promise { // Fill the AWS provider details @@ -599,116 +682,83 @@ export class ProvidersPage extends BasePage { } async clickNext(): Promise { - // The wizard interface may use different labels for its primary action button on each step. - // This function determines which button to click depending on the current URL and page content. + await this.verifyWizardModalOpen(); - // Get the current page URL - const url = this.page.url(); - - // If on the "connect-account" step, click the "Next" button - if (/\/providers\/connect-account/.test(url)) { - await this.nextButton.click(); + const launchScanButton = this.page.getByRole("button", { + name: "Launch scan", + exact: true, + }); + if (await launchScanButton.isVisible().catch(() => false)) { + await launchScanButton.click(); + await this.handleLaunchScanCompletion(); return; } - // If on the "add-credentials" step, check for "Save" and "Next" buttons - if (/\/providers\/add-credentials/.test(url)) { - // Some UI implementations use "Save" instead of "Next" for primary action + const actionNames = [ + "Go to scans", + "Authenticate", + "Next", + "Save", + "Check connection", + ] as const; - if (await this.saveButton.count()) { - await this.saveButton.click(); - return; - } - // If "Save" is not present, try clicking the "Next" button - if (await this.nextButton.count()) { - await this.nextButton.click(); + for (const actionName of actionNames) { + const button = this.page.getByRole("button", { + name: actionName, + exact: true, + }); + if (await button.isVisible().catch(() => false)) { + await button.click(); return; } } - // If on the "test-connection" step, click the "Launch scan" button - if (/\/providers\/test-connection/.test(url)) { - const buttonByText = this.page - .locator("button") - .filter({ hasText: "Launch scan" }); - - await buttonByText.click(); - - // Wait for either success (redirect to scans) or error message to appear - const errorMessage = this.page - .locator( - "div.border-border-error, div.bg-red-100, p.text-text-error-primary, p.text-danger", - ) - .first(); - - // Helper to check and throw error if visible - const checkAndThrowError = async (): Promise => { - 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"}`, - ); - } - }; - - try { - // Wait up to 15 seconds for either the error message or redirect - await Promise.race([ - errorMessage.waitFor({ state: "visible", timeout: 15000 }), - 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())) { - await checkAndThrowError(); - } - } catch (error) { - await checkAndThrowError(); - throw error; - } - - return; - } - - // Fallback logic: try finding any common primary action buttons in expected order - const candidates: Array<{ name: string | RegExp; exact?: boolean }> = [ - { name: "Next", exact: true }, // Try the "Next" button (exact match to avoid Next.js dev tools) - { name: "Save", exact: true }, // Try the "Save" button - { name: "Launch scan" }, // Try the "Launch scan" button - { name: /Continue|Proceed/i }, // Try "Continue" or "Proceed" (case-insensitive) - ]; - - // Try each candidate name and click it if found - for (const candidate of candidates) { - // Exclude Next.js dev tools button by filtering out buttons with aria-haspopup attribute - const btn = this.page - .getByRole("button", { - name: candidate.name, - exact: candidate.exact, - }) - .and(this.page.locator(":not([aria-haspopup])")); - - if (await btn.count()) { - await btn.click(); - return; - } - } - - // If none of the expected action buttons are present, throw an error throw new Error( - "Could not find an actionable Next/Save/Launch scan button on this step", + "Could not find an actionable primary button in the provider wizard modal.", ); } - async selectCredentialsType(type: AWSCredentialType): Promise { - // Ensure we are on the add-credentials page where the selector exists + private async handleLaunchScanCompletion(): Promise { + const errorMessage = this.page + .locator( + "div.border-border-error, div.bg-red-100, p.text-text-error-primary, p.text-danger", + ) + .first(); + const goToScansButton = this.page.getByRole("button", { + name: "Go to scans", + exact: true, + }); - await expect(this.page).toHaveURL(/\/providers\/add-credentials/); + try { + await Promise.race([ + this.page.waitForURL(/\/scans/, { timeout: 30000 }), + goToScansButton.waitFor({ state: "visible", timeout: 30000 }), + errorMessage.waitFor({ state: "visible", timeout: 30000 }), + ]); + } catch { + // Continue and inspect visible state below. + } + + 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"}`, + ); + } + + const isGoToScansVisible = await goToScansButton + .isVisible() + .catch(() => false); + if (isGoToScansVisible) { + await goToScansButton.click(); + await this.page.waitForURL(/\/scans/, { timeout: 30000 }); + } + } + + async selectCredentialsType(type: AWSCredentialType): Promise { + await this.verifyWizardModalOpen(); + await expect(this.roleCredentialsRadio).toBeVisible(); if (type === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) { await this.roleCredentialsRadio.click({ force: true }); @@ -720,9 +770,8 @@ export class ProvidersPage extends BasePage { } async selectM365CredentialsType(type: M365CredentialType): Promise { - // Ensure we are on the add-credentials page where the selector exists - - await expect(this.page).toHaveURL(/\/providers\/add-credentials/); + await this.verifyWizardModalOpen(); + await expect(this.m365StaticCredentialsRadio).toBeVisible(); if (type === M365_CREDENTIAL_OPTIONS.M365_CREDENTIALS) { await this.m365StaticCredentialsRadio.click({ force: true }); @@ -734,9 +783,8 @@ export class ProvidersPage extends BasePage { } async selectGCPCredentialsType(type: GCPCredentialType): Promise { - // Ensure we are on the add-credentials page where the selector exists - - await expect(this.page).toHaveURL(/\/providers\/add-credentials/); + await this.verifyWizardModalOpen(); + await expect(this.gcpServiceAccountRadio).toBeVisible(); if (type === GCP_CREDENTIAL_OPTIONS.GCP_SERVICE_ACCOUNT) { await this.gcpServiceAccountRadio.click({ force: true }); } else { @@ -745,9 +793,8 @@ export class ProvidersPage extends BasePage { } async selectGitHubCredentialsType(type: GitHubCredentialType): Promise { - // Ensure we are on the add-credentials page where the selector exists - - await expect(this.page).toHaveURL(/\/providers\/add-credentials/); + await this.verifyWizardModalOpen(); + await expect(this.githubPersonalAccessTokenRadio).toBeVisible(); if (type === GITHUB_CREDENTIAL_OPTIONS.GITHUB_PERSONAL_ACCESS_TOKEN) { await this.githubPersonalAccessTokenRadio.click({ force: true }); @@ -960,9 +1007,8 @@ export class ProvidersPage extends BasePage { async selectAlibabaCloudCredentialsType( type: AlibabaCloudCredentialType, ): Promise { - // Ensure we are on the add-credentials page where the selector exists - - await expect(this.page).toHaveURL(/\/providers\/add-credentials/); + await this.verifyWizardModalOpen(); + await expect(this.alibabacloudStaticCredentialsRadio).toBeVisible(); if (type === ALIBABACLOUD_CREDENTIAL_OPTIONS.ALIBABACLOUD_CREDENTIALS) { await this.alibabacloudStaticCredentialsRadio.click({ force: true }); @@ -1047,6 +1093,7 @@ export class ProvidersPage extends BasePage { // Verify the connect account page is loaded await this.verifyPageHasProwlerTitle(); + await this.verifyWizardModalOpen(); await expect(this.awsProviderRadio).toBeVisible(); await expect(this.ociProviderRadio).toBeVisible(); await expect(this.gcpProviderRadio).toBeVisible(); @@ -1061,6 +1108,7 @@ export class ProvidersPage extends BasePage { // Verify the credentials page is loaded await this.verifyPageHasProwlerTitle(); + await this.verifyWizardModalOpen(); await expect(this.roleCredentialsRadio).toBeVisible(); } @@ -1115,7 +1163,7 @@ export class ProvidersPage extends BasePage { // Verify the launch scan page is loaded await this.verifyPageHasProwlerTitle(); - await expect(this.page).toHaveURL(/\/providers\/test-connection/); + await this.verifyWizardModalOpen(); // Verify the Launch scan button is visible const launchScanButton = this.page @@ -1202,12 +1250,19 @@ export class ProvidersPage extends BasePage { async verifyUpdateCredentialsPageLoaded(): Promise { // Verify the update credentials page is loaded await this.verifyPageHasProwlerTitle(); - await expect(this.page).toHaveURL(/\/providers\/update-credentials/); + await this.verifyWizardModalOpen(); + await expect( + this.page.getByRole("button", { name: "Authenticate", exact: true }), + ).toBeVisible(); } async verifyTestConnectionPageLoaded(): Promise { // Verify the test connection page is loaded await this.verifyPageHasProwlerTitle(); - await expect(this.page).toHaveURL(/\/providers\/test-connection/); + await this.verifyWizardModalOpen(); + const testConnectionAction = this.page + .getByRole("button", { name: "Launch scan", exact: true }) + .or(this.page.getByRole("button", { name: "Check connection", exact: true })); + await expect(testConnectionAction).toBeVisible(); } }