mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
402 lines
12 KiB
TypeScript
402 lines
12 KiB
TypeScript
import { expect, Locator, Page } from "@playwright/test";
|
|
|
|
import { BasePage } from "../base-page";
|
|
import { HomePage } from "../home/home-page";
|
|
|
|
export interface SignInCredentials {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface SocialAuthConfig {
|
|
googleEnabled: boolean;
|
|
githubEnabled: boolean;
|
|
}
|
|
|
|
export class SignInPage extends BasePage {
|
|
readonly homePage: HomePage;
|
|
|
|
// Form elements
|
|
readonly emailInput: Locator;
|
|
readonly passwordInput: Locator;
|
|
readonly loginButton: Locator;
|
|
readonly form: Locator;
|
|
|
|
// Social authentication buttons
|
|
readonly googleButton: Locator;
|
|
readonly githubButton: Locator;
|
|
readonly samlButton: Locator;
|
|
|
|
// Navigation elements
|
|
readonly signUpLink: Locator;
|
|
readonly backButton: Locator;
|
|
|
|
// UI elements
|
|
readonly logo: Locator;
|
|
readonly pageTitle: Locator;
|
|
|
|
// Error messages
|
|
readonly errorMessages: Locator;
|
|
|
|
// SAML specific elements
|
|
readonly samlModeTitle: Locator;
|
|
readonly samlEmailInput: Locator;
|
|
|
|
constructor(page: Page) {
|
|
super(page);
|
|
this.homePage = new HomePage(page);
|
|
|
|
// Form elements
|
|
this.emailInput = page.getByRole("textbox", { name: "Email" });
|
|
this.passwordInput = page.getByRole("textbox", { name: "Password" });
|
|
this.loginButton = page.getByRole("button", { name: "Log in" });
|
|
this.form = page.locator("form");
|
|
|
|
// Social authentication buttons
|
|
this.googleButton = page.getByRole("button", {
|
|
name: "Continue with Google",
|
|
});
|
|
this.githubButton = page.getByRole("button", {
|
|
name: "Continue with Github",
|
|
});
|
|
this.samlButton = page.getByRole("button", {
|
|
name: "Continue with SAML SSO",
|
|
});
|
|
|
|
// Navigation elements
|
|
this.signUpLink = page.getByRole("link", { name: "Sign up" });
|
|
this.backButton = page.getByRole("button", { name: "Back" });
|
|
|
|
// UI elements - ProwlerBrand exposes the deployed brand variant as an image.
|
|
this.logo = page.getByRole("img", {
|
|
name: /^Prowler (Cloud|Local Server)$/,
|
|
});
|
|
// Use text matching with exact=true to avoid matching other elements
|
|
this.pageTitle = page.getByText("Welcome back", { exact: true });
|
|
|
|
// Error messages - form validation errors appear as <p> with specific classes
|
|
this.errorMessages = page.locator(
|
|
"p.text-destructive, p.text-sm.text-destructive",
|
|
);
|
|
|
|
// SAML specific elements - use text matching
|
|
this.samlModeTitle = page.getByText("Sign in with SAML SSO", {
|
|
exact: true,
|
|
});
|
|
this.samlEmailInput = page.getByRole("textbox", { name: "Email" });
|
|
}
|
|
|
|
// Navigation methods
|
|
async goto(): Promise<void> {
|
|
await super.goto("/sign-in");
|
|
}
|
|
|
|
/**
|
|
* Navigate to sign-in page with an error query parameter
|
|
* Used for testing error message display (e.g., session expiry)
|
|
*/
|
|
async gotoWithError(errorType: string): Promise<void> {
|
|
await this.page.goto(`/sign-in?error=${errorType}`);
|
|
}
|
|
|
|
// Form interaction methods
|
|
async fillEmail(email: string): Promise<void> {
|
|
await this.emailInput.fill(email);
|
|
}
|
|
|
|
async fillPassword(password: string): Promise<void> {
|
|
await this.passwordInput.fill(password);
|
|
}
|
|
|
|
async fillCredentials(credentials: SignInCredentials): Promise<void> {
|
|
await this.fillEmail(credentials.email);
|
|
await this.fillPassword(credentials.password);
|
|
}
|
|
|
|
async submitForm(): Promise<void> {
|
|
await this.loginButton.click();
|
|
}
|
|
|
|
async login(credentials: SignInCredentials): Promise<void> {
|
|
await this.fillCredentials(credentials);
|
|
await this.submitForm();
|
|
}
|
|
|
|
// Social authentication methods
|
|
async clickGoogleAuth(): Promise<void> {
|
|
await this.googleButton.click();
|
|
}
|
|
|
|
async clickGithubAuth(): Promise<void> {
|
|
await this.githubButton.click();
|
|
}
|
|
|
|
async clickSamlAuth(): Promise<void> {
|
|
await this.samlButton.click();
|
|
}
|
|
|
|
// SAML SSO methods
|
|
async toggleSamlMode(): Promise<void> {
|
|
await this.clickSamlAuth();
|
|
}
|
|
|
|
async goBackFromSaml(): Promise<void> {
|
|
await this.backButton.click();
|
|
}
|
|
|
|
async fillSamlEmail(email: string): Promise<void> {
|
|
await this.samlEmailInput.fill(email);
|
|
}
|
|
|
|
async submitSamlForm(): Promise<void> {
|
|
await this.submitForm();
|
|
}
|
|
|
|
// Navigation methods
|
|
async goToSignUp(): Promise<void> {
|
|
await this.signUpLink.click();
|
|
}
|
|
|
|
// Validation and assertion methods
|
|
async verifyPageLoaded(): Promise<void> {
|
|
await expect(this.page).toHaveTitle(/Prowler/);
|
|
await expect(this.logo).toBeVisible();
|
|
await expect(this.pageTitle).toBeVisible();
|
|
await expect(this.pageTitle).toHaveText("Welcome back");
|
|
}
|
|
|
|
async verifyFormElements(): Promise<void> {
|
|
await expect(this.emailInput).toBeVisible();
|
|
await expect(this.passwordInput).toBeVisible();
|
|
await expect(this.loginButton).toBeVisible();
|
|
}
|
|
|
|
async verifySocialButtons(config: SocialAuthConfig): Promise<void> {
|
|
if (config.googleEnabled) {
|
|
await expect(this.googleButton).toBeVisible();
|
|
}
|
|
if (config.githubEnabled) {
|
|
await expect(this.githubButton).toBeVisible();
|
|
}
|
|
await expect(this.samlButton).toBeVisible();
|
|
}
|
|
|
|
async verifyNavigationLinks(): Promise<void> {
|
|
await expect(
|
|
this.page.getByText("Need to create an account?"),
|
|
).toBeVisible();
|
|
await expect(this.signUpLink).toBeVisible();
|
|
}
|
|
|
|
async verifySuccessfulLogin(): Promise<void> {
|
|
await this.homePage.verifyPageLoaded();
|
|
}
|
|
|
|
/**
|
|
* Complete login flow: navigates to sign-in, logs in, and verifies success.
|
|
* Use this when you need to authenticate from scratch in a test.
|
|
*/
|
|
async loginAndVerify(credentials: SignInCredentials): Promise<void> {
|
|
await this.goto();
|
|
await this.login(credentials);
|
|
await this.verifySuccessfulLogin();
|
|
}
|
|
|
|
async verifyLoginError(
|
|
errorMessage: string = "Invalid email or password",
|
|
): Promise<void> {
|
|
// Error messages appear as <p> elements with destructive styling
|
|
await expect(this.page.getByText(errorMessage).first()).toBeVisible();
|
|
await expect(this.page).toHaveURL("/sign-in");
|
|
}
|
|
|
|
async verifySamlModeActive(): Promise<void> {
|
|
await expect(this.samlModeTitle).toBeVisible();
|
|
await expect(this.passwordInput).not.toBeVisible();
|
|
await expect(this.backButton).toBeVisible();
|
|
}
|
|
|
|
async verifyNormalModeActive(): Promise<void> {
|
|
await expect(this.pageTitle).toHaveText("Welcome back");
|
|
await expect(this.passwordInput).toBeVisible();
|
|
}
|
|
|
|
async verifyLoadingState(): Promise<void> {
|
|
// Check that button shows loading text or is disabled
|
|
await expect(this.loginButton).toHaveAttribute("aria-disabled", "true");
|
|
}
|
|
|
|
async verifyFormValidation(): Promise<void> {
|
|
// Check for common validation messages - they appear as <p> elements
|
|
const emailError = this.page.getByText(
|
|
"Please enter a valid email address.",
|
|
);
|
|
const passwordError = this.page.getByText("Password is required.");
|
|
|
|
// At least one validation error should be visible
|
|
await expect(emailError.or(passwordError).first()).toBeVisible();
|
|
}
|
|
|
|
// Accessibility methods
|
|
async verifyKeyboardNavigation(): Promise<void> {
|
|
// Test tab navigation through form elements
|
|
await this.page.keyboard.press("Tab"); // Theme toggle
|
|
await this.page.keyboard.press("Tab"); // Email field
|
|
await expect(this.emailInput).toBeFocused();
|
|
|
|
await this.page.keyboard.press("Tab"); // Password field
|
|
await expect(this.passwordInput).toBeFocused();
|
|
|
|
await this.page.keyboard.press("Tab"); // Show password button
|
|
await this.page.keyboard.press("Tab"); // Login button
|
|
await expect(this.loginButton).toBeFocused();
|
|
}
|
|
|
|
async verifyAriaLabels(): Promise<void> {
|
|
await expect(
|
|
this.page.getByRole("textbox", { name: "Email" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
this.page.getByRole("textbox", { name: "Password" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
this.page.getByRole("button", { name: "Log in" }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
// Utility methods
|
|
async clearForm(): Promise<void> {
|
|
await this.emailInput.clear();
|
|
await this.passwordInput.clear();
|
|
}
|
|
|
|
async isFormValid(): Promise<boolean> {
|
|
const emailValue = await this.emailInput.inputValue();
|
|
const passwordValue = await this.passwordInput.inputValue();
|
|
return emailValue.length > 0 && passwordValue.length > 0;
|
|
}
|
|
|
|
// Session management methods
|
|
async logout(): Promise<void> {
|
|
await this.homePage.signOut();
|
|
}
|
|
|
|
/**
|
|
* Verifies we are on the sign-in page (URL + title visible)
|
|
* Use this when redirected to sign-in from protected routes
|
|
*/
|
|
async verifyOnSignInPage(): Promise<void> {
|
|
await expect(this.page).toHaveURL(/\/sign-in/);
|
|
await expect(this.pageTitle).toBeVisible();
|
|
}
|
|
|
|
/**
|
|
* Verifies we stayed on the sign-in page (exact URL match)
|
|
* Use after form validation errors
|
|
*/
|
|
async verifyStaysOnSignInPage(): Promise<void> {
|
|
await expect(this.page).toHaveURL("/sign-in");
|
|
}
|
|
|
|
/**
|
|
* Verifies we are NOT on the sign-in page
|
|
* Use after successful login to confirm redirect
|
|
*/
|
|
async verifyNotOnSignInPage(): Promise<void> {
|
|
await expect(this.page).not.toHaveURL(/\/sign-in/);
|
|
}
|
|
|
|
/**
|
|
* Verifies redirect to sign-in includes a callbackUrl parameter
|
|
*/
|
|
async verifyRedirectWithCallback(expectedCallback: string): Promise<void> {
|
|
await expect(this.page).toHaveURL(/\/sign-in\?.*callbackUrl=/);
|
|
const url = new URL(this.page.url());
|
|
const callbackUrl = url.searchParams.get("callbackUrl");
|
|
expect(callbackUrl).toBe(expectedCallback);
|
|
}
|
|
|
|
/**
|
|
* Wait for toast notification to appear (used for error messages)
|
|
*/
|
|
async waitForToast(): Promise<{ isVisible: boolean; text: string | null }> {
|
|
const toast = this.page.locator('[role="status"], [role="alert"]').first();
|
|
try {
|
|
await toast.waitFor({ state: "visible", timeout: 2000 });
|
|
const text = await toast.textContent();
|
|
return { isVisible: true, text };
|
|
} catch {
|
|
return { isVisible: false, text: null };
|
|
}
|
|
}
|
|
|
|
async verifyLogoutSuccess(): Promise<void> {
|
|
await this.verifyOnSignInPage();
|
|
}
|
|
|
|
// Advanced interaction methods
|
|
async fillFormWithValidation(credentials: SignInCredentials): Promise<void> {
|
|
// Fill email first and check for validation
|
|
await this.fillEmail(credentials.email);
|
|
await this.page.keyboard.press("Tab"); // Trigger validation
|
|
|
|
// Fill password
|
|
await this.fillPassword(credentials.password);
|
|
}
|
|
|
|
async submitFormWithEnterKey(): Promise<void> {
|
|
await this.passwordInput.press("Enter");
|
|
}
|
|
|
|
async submitFormWithButtonClick(): Promise<void> {
|
|
await this.submitForm();
|
|
}
|
|
|
|
// Error handling methods
|
|
async handleSamlError(): Promise<void> {
|
|
const samlError = this.page.getByRole("alert", {
|
|
name: "SAML Authentication Error",
|
|
});
|
|
if (await samlError.isVisible()) {
|
|
// SAML error detected - test may need to handle this case
|
|
}
|
|
}
|
|
|
|
// Wait methods
|
|
async waitForFormSubmission(): Promise<void> {
|
|
await this.page.waitForFunction(() => {
|
|
const button = document.querySelector('button[aria-disabled="true"]');
|
|
return button === null;
|
|
});
|
|
}
|
|
|
|
async waitForRedirect(expectedUrl: string): Promise<void> {
|
|
await this.page.waitForURL(expectedUrl);
|
|
}
|
|
|
|
// ===========================================
|
|
// Authentication Setup Methods
|
|
// ===========================================
|
|
|
|
/**
|
|
* Authenticate and save the browser state to a file.
|
|
* Used by auth setup projects to create reusable authenticated sessions.
|
|
*
|
|
* @param credentials - User credentials (email and password)
|
|
* @param storagePath - Path to save the authentication state file
|
|
*/
|
|
async authenticateAndSaveState(
|
|
credentials: SignInCredentials,
|
|
storagePath: string,
|
|
): Promise<void> {
|
|
if (!credentials.email || !credentials.password) {
|
|
throw new Error(
|
|
"Email and password are required for authentication and save state",
|
|
);
|
|
}
|
|
|
|
await this.loginAndVerify(credentials);
|
|
await this.page.context().storageState({ path: storagePath });
|
|
}
|
|
}
|