refactor(ui): redo the whole app with styles (#9234)

This commit is contained in:
Alejandro Bailo
2025-11-19 11:37:17 +01:00
committed by GitHub
parent c418c59b53
commit c23e2502f3
267 changed files with 5339 additions and 6952 deletions

View File

@@ -97,7 +97,9 @@ export async function verifyNormalModeActive(page: Page) {
}
export async function logout(page: Page) {
await page.getByRole("button", { name: "Sign out" }).click();
const navbar = page.locator("header");
await navbar.waitFor({ state: "visible" });
await navbar.getByRole("button", { name: "Sign out" }).click();
}
export async function verifyLogoutSuccess(page: Page) {
@@ -146,7 +148,9 @@ export async function authenticateAndSaveState(
storagePath: string,
) {
if (!email || !password) {
throw new Error('Email and password are required for authentication and save state');
throw new Error(
"Email and password are required for authentication and save state",
);
}
// Create SignInPage instance

View File

@@ -10,7 +10,7 @@ export interface AWSProviderData {
accessKeyId?: string;
secretAccessKey?: string;
}
// AZURE provider data
export interface AZUREProviderData {
subscriptionId: string;
@@ -26,11 +26,12 @@ export interface M365ProviderData {
// AWS credential options
export const AWS_CREDENTIAL_OPTIONS = {
AWS_ROLE_ARN: "role",
AWS_CREDENTIALS: "credentials"
AWS_CREDENTIALS: "credentials",
} as const;
// AWS credential type
type AWSCredentialType = (typeof AWS_CREDENTIAL_OPTIONS)[keyof typeof AWS_CREDENTIAL_OPTIONS];
type AWSCredentialType =
(typeof AWS_CREDENTIAL_OPTIONS)[keyof typeof AWS_CREDENTIAL_OPTIONS];
// AWS provider credential
export interface AWSProviderCredential {
@@ -43,41 +44,42 @@ export interface AWSProviderCredential {
// AZURE credential options
export const AZURE_CREDENTIAL_OPTIONS = {
AZURE_CREDENTIALS: "credentials"
AZURE_CREDENTIALS: "credentials",
} as const;
// AZURE credential type
type AZURECredentialType = (typeof AZURE_CREDENTIAL_OPTIONS)[keyof typeof AZURE_CREDENTIAL_OPTIONS];
type AZURECredentialType =
(typeof AZURE_CREDENTIAL_OPTIONS)[keyof typeof AZURE_CREDENTIAL_OPTIONS];
// AZURE provider credential
export interface AZUREProviderCredential {
type: AZURECredentialType;
clientId:string;
clientSecret:string;
tenantId:string;
clientId: string;
clientSecret: string;
tenantId: string;
}
// M365 credential options
export const M365_CREDENTIAL_OPTIONS = {
M365_CREDENTIALS: "credentials",
M365_CERTIFICATE_CREDENTIALS: "certificate"
M365_CERTIFICATE_CREDENTIALS: "certificate",
} as const;
// M365 credential type
type M365CredentialType = (typeof M365_CREDENTIAL_OPTIONS)[keyof typeof M365_CREDENTIAL_OPTIONS];
type M365CredentialType =
(typeof M365_CREDENTIAL_OPTIONS)[keyof typeof M365_CREDENTIAL_OPTIONS];
// M365 provider credential
export interface M365ProviderCredential {
type: M365CredentialType;
clientId:string;
clientSecret?:string;
tenantId:string;
certificateContent?:string;
clientId: string;
clientSecret?: string;
tenantId: string;
certificateContent?: string;
}
// Providers page
export class ProvidersPage extends BasePage {
// Button to add a new cloud provider
readonly addProviderButton: Locator;
readonly providersTable: Locator;
@@ -133,8 +135,9 @@ export class ProvidersPage extends BasePage {
constructor(page: Page) {
super(page);
// Button to add a new cloud provider
this.addProviderButton = page.getByRole("button", { name: "Add Cloud Provider", exact: true });
this.addProviderButton = page.getByRole("link", {
name: "Add Cloud Provider",
});
// Table displaying existing providers
this.providersTable = page.getByRole("table");
@@ -159,22 +162,32 @@ export class ProvidersPage extends BasePage {
// AWS provider form inputs
this.accountIdInput = page.getByRole("textbox", { name: "Account ID" });
// AZURE provider form inputs
this.azureSubscriptionIdInput = page.getByRole("textbox", { name: "Subscription ID" });
this.azureSubscriptionIdInput = page.getByRole("textbox", {
name: "Subscription ID",
});
this.azureClientIdInput = page.getByRole("textbox", { name: "Client ID" });
this.azureClientSecretInput = page.getByRole("textbox", { name: "Client Secret" });
this.azureClientSecretInput = page.getByRole("textbox", {
name: "Client Secret",
});
this.azureTenantIdInput = page.getByRole("textbox", { name: "Tenant ID" });
// M365 provider form inputs
this.m365domainIdInput = page.getByRole("textbox", { name: "Domain ID" });
this.m365ClientIdInput = page.getByRole("textbox", { name: "Client ID" });
this.m365ClientSecretInput = page.getByRole("textbox", { name: "Client Secret" });
this.m365ClientSecretInput = page.getByRole("textbox", {
name: "Client Secret",
});
this.m365TenantIdInput = page.getByRole("textbox", { name: "Tenant ID" });
this.m365CertificateContentInput = page.getByRole("textbox", { name: "Certificate Content" });
this.m365CertificateContentInput = page.getByRole("textbox", {
name: "Certificate Content",
});
// Alias input
this.aliasInput = page.getByRole("textbox", { name: "Provider alias (optional)" });
this.aliasInput = page.getByRole("textbox", {
name: "Provider alias (optional)",
});
// Navigation buttons in the form (next and back)
this.nextButton = page
@@ -212,8 +225,12 @@ export class ProvidersPage extends BasePage {
this.externalIdInput = page.getByRole("textbox", { name: "External ID" });
// Inputs for static credentials
this.accessKeyIdInput = page.getByRole("textbox", { name: "Access Key ID" });
this.secretAccessKeyInput = page.getByRole("textbox", { name: "Secret Access Key" });
this.accessKeyIdInput = page.getByRole("textbox", {
name: "Access Key ID",
});
this.secretAccessKeyInput = page.getByRole("textbox", {
name: "Secret Access Key",
});
// Delete button in confirmation modal
this.deleteProviderConfirmationButton = page.getByRole("button", {
@@ -236,19 +253,17 @@ export class ProvidersPage extends BasePage {
}
async selectAWSProvider(): Promise<void> {
// Prefer label-based click for radios, force if overlay intercepts
await this.awsProviderRadio.click({ force: true });
await this.waitForPageLoad();
}
async selectAZUREProvider(): Promise<void> {
// Prefer label-based click for radios, force if overlay intercepts
await this.azureProviderRadio.click({ force: true });
await this.waitForPageLoad();
}
async selectM365Provider(): Promise<void> {
// Select the M365 provider
@@ -256,7 +271,6 @@ export class ProvidersPage extends BasePage {
await this.waitForPageLoad();
}
async fillAWSProviderDetails(data: AWSProviderData): Promise<void> {
// Fill the AWS provider details
@@ -292,7 +306,7 @@ export class ProvidersPage extends BasePage {
// This function determines which button to click depending on the current URL and page content.
// Get the current page URL
const url = this.page.url();
const url = this.page.url();
// If on the "connect-account" step, click the "Next" button
if (/\/providers\/connect-account/.test(url)) {
@@ -328,9 +342,9 @@ export class ProvidersPage extends BasePage {
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();
// The error container has multiple p.text-text-error elements, we want the first one with the technical error
const errorMessage = this.page.locator("p.text-text-error").first();
try {
// Wait up to 15 seconds for either the error message or redirect
await Promise.race([
@@ -342,7 +356,9 @@ export class ProvidersPage extends BasePage {
// 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);
const isErrorVisible = await errorMessage
.isVisible()
.catch(() => false);
if (isErrorVisible) {
const errorText = await errorMessage.textContent();
throw new Error(
@@ -352,7 +368,9 @@ export class ProvidersPage extends BasePage {
}
} catch (error) {
// If timeout or other error, check if error message is present
const isErrorVisible = await errorMessage.isVisible().catch(() => false);
const isErrorVisible = await errorMessage
.isVisible()
.catch(() => false);
if (isErrorVisible) {
const errorText = await errorMessage.textContent();
throw new Error(
@@ -416,7 +434,7 @@ export class ProvidersPage extends BasePage {
if (type === M365_CREDENTIAL_OPTIONS.M365_CREDENTIALS) {
await this.m365StaticCredentialsRadio.click({ force: true });
} else if (type === M365_CREDENTIAL_OPTIONS.M365_CERTIFICATE_CREDENTIALS) {
await this.m365CertificateCredentialsRadio.click({ force: true });
await this.m365CertificateCredentialsRadio.click({ force: true });
} else {
throw new Error(`Invalid M365 credential type: ${type}`);
}
@@ -426,7 +444,7 @@ export class ProvidersPage extends BasePage {
async fillRoleCredentials(credentials: AWSProviderCredential): Promise<void> {
// Fill the role credentials form
if (credentials.accessKeyId) {
await this.accessKeyIdInput.fill(credentials.accessKeyId);
}
@@ -444,7 +462,9 @@ export class ProvidersPage extends BasePage {
}
}
async fillStaticCredentials(credentials: AWSProviderCredential): Promise<void> {
async fillStaticCredentials(
credentials: AWSProviderCredential,
): Promise<void> {
// Fill the static credentials form
if (credentials.accessKeyId) {
@@ -455,7 +475,9 @@ export class ProvidersPage extends BasePage {
}
}
async fillAZURECredentials(credentials: AZUREProviderCredential): Promise<void> {
async fillAZURECredentials(
credentials: AZUREProviderCredential,
): Promise<void> {
// Fill the azure credentials form
if (credentials.clientId) {
@@ -469,7 +491,9 @@ export class ProvidersPage extends BasePage {
}
}
async fillM365Credentials(credentials: M365ProviderCredential): Promise<void> {
async fillM365Credentials(
credentials: M365ProviderCredential,
): Promise<void> {
// Fill the m365 credentials form
if (credentials.clientId) {
@@ -483,14 +507,18 @@ export class ProvidersPage extends BasePage {
}
}
async fillM365CertificateCredentials(credentials: M365ProviderCredential): Promise<void> {
async fillM365CertificateCredentials(
credentials: M365ProviderCredential,
): Promise<void> {
// Fill the m365 certificate credentials form
if (credentials.clientId) {
await this.m365ClientIdInput.fill(credentials.clientId);
}
if (credentials.certificateContent) {
await this.m365CertificateContentInput.fill(credentials.certificateContent);
await this.m365CertificateContentInput.fill(
credentials.certificateContent,
);
}
if (credentials.tenantId) {
await this.m365TenantIdInput.fill(credentials.tenantId);
@@ -502,7 +530,7 @@ export class ProvidersPage extends BasePage {
await expect(this.page).toHaveTitle(/Prowler/);
await expect(this.addProviderButton).toBeVisible();
await this.page.waitForLoadState('networkidle');
await this.page.waitForLoadState("networkidle");
}
async verifyConnectAccountPageLoaded(): Promise<void> {
@@ -584,15 +612,15 @@ export class ProvidersPage extends BasePage {
// Find and use the search input to filter the table
const searchInput = this.page.getByPlaceholder(/search|filter/i);
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
@@ -600,7 +628,7 @@ export class ProvidersPage extends BasePage {
// 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<boolean> => {
const text = await row.textContent();
@@ -610,24 +638,24 @@ export class ProvidersPage extends BasePage {
// Helper function to find the row with the specific UID
const findProviderRow = async (): Promise<Locator | null> => {
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;
};
@@ -635,7 +663,7 @@ export class ProvidersPage extends BasePage {
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++) {
@@ -643,14 +671,14 @@ export class ProvidersPage extends BasePage {
dataRowCount++;
}
}
// Should have 0 or 1 data row
expect(dataRowCount).toBeLessThanOrEqual(1);
}).toPass({ timeout: 20000 });
// 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
@@ -660,7 +688,11 @@ export class ProvidersPage extends BasePage {
}
// Find and click the action button (last cell = actions column)
const actionButton = targetRow.locator("td").last().locator("button").first();
const actionButton = targetRow
.locator("td")
.last()
.locator("button")
.first();
await expect(actionButton).toBeVisible({ timeout: 5000 });
await actionButton.click();
@@ -672,11 +704,15 @@ export class ProvidersPage extends BasePage {
await deleteMenuItem.click();
// Wait for confirmation modal to appear
const modal = this.page.locator('[role="dialog"], .modal, [data-testid*="modal"]').first();
const modal = this.page
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
.first();
await expect(modal).toBeVisible({ timeout: 10000 });
// Find and click the delete confirmation button
await expect(this.deleteProviderConfirmationButton).toBeVisible({ timeout: 5000 });
await expect(this.deleteProviderConfirmationButton).toBeVisible({
timeout: 5000,
});
await this.deleteProviderConfirmationButton.click();
// Wait for modal to close (this indicates deletion was initiated)
@@ -693,17 +729,22 @@ export class ProvidersPage extends BasePage {
async selectAuthenticationMethod(method: AWSCredentialType): Promise<void> {
// Select the authentication method
// Search botton that contains text AWS SDK Default or Prowler Cloud will assume or Access & Secret Key
const button = this.page.locator('button').filter({ hasText: /AWS SDK Default|Prowler Cloud will assume|Access & Secret Key/i });
await button.click();
// Search botton that contains text AWS SDK Default or Prowler Cloud will assume or Access & Secret Key
const button = this.page.locator("button").filter({
hasText: /AWS SDK Default|Prowler Cloud will assume|Access & Secret Key/i,
});
await button.click();
if (method === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) {
const modal = this.page.locator('[role="dialog"], .modal, [data-testid*="modal"]').first();
const modal = this.page
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
.first();
await expect(modal).toBeVisible({ timeout: 10000 });
// Select the role credentials
this.page.getByRole('option', { name: 'Access & Secret Key' }).click({ force: true });
this.page
.getByRole("option", { name: "Access & Secret Key" })
.click({ force: true });
} else {
throw new Error(`Invalid authentication method: ${method}`);
}