mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
test(ui): implement base page object and enhance sign-up flow tests
- Introduced a BasePage class to encapsulate common functionality for page objects, improving code reusability and maintainability. - Created new page objects for HomePage, SignInPage, and SignUpPage to streamline the sign-up and login processes in tests. - Added comprehensive sign-up flow tests to validate user registration and login functionality, ensuring a smooth user experience. - Updated Playwright configuration to support new test structures and improve overall test organization.
This commit is contained in:
+4
-4
@@ -15,10 +15,10 @@
|
||||
"format:check": "./node_modules/.bin/prettier --check ./app",
|
||||
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
|
||||
"prepare": "husky",
|
||||
"test:e2e": "playwright test --project=chromium",
|
||||
"test:e2e:ui": "playwright test --project=chromium --ui",
|
||||
"test:e2e:debug": "playwright test --project=chromium --debug",
|
||||
"test:e2e:headed": "playwright test --project=chromium --headed",
|
||||
"test:e2e": "playwright test --project=chromium --project=sign-up",
|
||||
"test:e2e:ui": "playwright test --project=chromium --project=sign-up --ui",
|
||||
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --debug",
|
||||
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --headed",
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"test:e2e:install": "playwright install"
|
||||
},
|
||||
|
||||
@@ -74,14 +74,6 @@ export default defineConfig({
|
||||
name: "invite-and-manage-users.auth.setup",
|
||||
testMatch: "invite-and-manage-users.auth.setup.ts",
|
||||
},
|
||||
|
||||
// All authentication setups combined
|
||||
// Runs all authentication setup files to create all user states
|
||||
{
|
||||
name: "all.auth.setup",
|
||||
testMatch: "**/*.auth.setup.ts",
|
||||
},
|
||||
|
||||
// ===========================================
|
||||
// Test Suite Projects
|
||||
// ===========================================
|
||||
@@ -89,6 +81,12 @@ export default defineConfig({
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
testMatch: "auth-login.spec.ts",
|
||||
},
|
||||
// This project runs the sign-up test suite
|
||||
{
|
||||
name: "sign-up",
|
||||
testMatch: "sign-up.spec.ts",
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Base page object class containing common functionality
|
||||
* that can be shared across all page objects
|
||||
*/
|
||||
export abstract class BasePage {
|
||||
readonly page: Page;
|
||||
|
||||
// Common UI elements that appear on most pages
|
||||
readonly title: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
readonly themeToggle: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
// Common locators that most pages share
|
||||
this.title = page.locator("h1, h2, [role='heading']").first();
|
||||
this.loadingIndicator = page.getByText("Loading");
|
||||
this.themeToggle = page.getByLabel("Toggle theme");
|
||||
}
|
||||
|
||||
// Common navigation methods
|
||||
async goto(url: string): Promise<void> {
|
||||
await this.page.goto(url);
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// Common verification methods
|
||||
async verifyPageTitle(expectedTitle: string | RegExp): Promise<void> {
|
||||
await expect(this.page).toHaveTitle(expectedTitle);
|
||||
}
|
||||
|
||||
async verifyLoadingState(): Promise<void> {
|
||||
await expect(this.loadingIndicator).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyNoLoadingState(): Promise<void> {
|
||||
await expect(this.loadingIndicator).not.toBeVisible();
|
||||
}
|
||||
|
||||
// Common form interaction methods
|
||||
async clearInput(input: Locator): Promise<void> {
|
||||
await input.clear();
|
||||
}
|
||||
|
||||
async fillInput(input: Locator, value: string): Promise<void> {
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
async clickButton(button: Locator): Promise<void> {
|
||||
await button.click();
|
||||
}
|
||||
|
||||
// Common validation methods
|
||||
async verifyElementVisible(element: Locator): Promise<void> {
|
||||
await expect(element).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyElementNotVisible(element: Locator): Promise<void> {
|
||||
await expect(element).not.toBeVisible();
|
||||
}
|
||||
|
||||
async verifyElementText(element: Locator, expectedText: string): Promise<void> {
|
||||
await expect(element).toHaveText(expectedText);
|
||||
}
|
||||
|
||||
async verifyElementContainsText(element: Locator, expectedText: string): Promise<void> {
|
||||
await expect(element).toContainText(expectedText);
|
||||
}
|
||||
|
||||
// Common accessibility methods
|
||||
async verifyKeyboardNavigation(elements: Locator[]): Promise<void> {
|
||||
for (const element of elements) {
|
||||
await this.page.keyboard.press("Tab");
|
||||
await expect(element).toBeFocused();
|
||||
}
|
||||
}
|
||||
|
||||
async verifyAriaLabels(elements: { locator: Locator; expectedLabel: string }[]): Promise<void> {
|
||||
for (const { locator, expectedLabel } of elements) {
|
||||
await expect(locator).toHaveAttribute("aria-label", expectedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
// Common utility methods
|
||||
async getElementText(element: Locator): Promise<string> {
|
||||
return await element.textContent() || "";
|
||||
}
|
||||
|
||||
async getElementValue(element: Locator): Promise<string> {
|
||||
return await element.inputValue();
|
||||
}
|
||||
|
||||
async isElementVisible(element: Locator): Promise<boolean> {
|
||||
return await element.isVisible();
|
||||
}
|
||||
|
||||
async isElementEnabled(element: Locator): Promise<boolean> {
|
||||
return await element.isEnabled();
|
||||
}
|
||||
|
||||
// Common error handling methods
|
||||
async getFormErrors(): Promise<string[]> {
|
||||
const errorElements = await this.page.locator('[role="alert"], .error-message, [data-testid="error"]').all();
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const element of errorElements) {
|
||||
const text = await element.textContent();
|
||||
if (text) {
|
||||
errors.push(text.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
async verifyNoErrors(): Promise<void> {
|
||||
const errors = await this.getFormErrors();
|
||||
expect(errors).toHaveLength(0);
|
||||
}
|
||||
|
||||
// Common wait methods
|
||||
async waitForElement(element: Locator, timeout: number = 5000): Promise<void> {
|
||||
await element.waitFor({ timeout });
|
||||
}
|
||||
|
||||
async waitForElementToDisappear(element: Locator, timeout: number = 5000): Promise<void> {
|
||||
await element.waitFor({ state: "hidden", timeout });
|
||||
}
|
||||
|
||||
async waitForUrl(expectedUrl: string | RegExp, timeout: number = 5000): Promise<void> {
|
||||
await this.page.waitForURL(expectedUrl, { timeout });
|
||||
}
|
||||
|
||||
// Common screenshot methods
|
||||
async takeScreenshot(name: string): Promise<void> {
|
||||
await this.page.screenshot({ path: `screenshots/${name}.png` });
|
||||
}
|
||||
|
||||
async takeElementScreenshot(element: Locator, name: string): Promise<void> {
|
||||
await element.screenshot({ path: `screenshots/${name}.png` });
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -1,5 +1,5 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
import { SignInPage, SignInCredentials } from "./page-objects/sign-in-page";
|
||||
import { SignInPage, SignInCredentials } from "./sign-in/sign-in-page";
|
||||
|
||||
export const ERROR_MESSAGES = {
|
||||
INVALID_CREDENTIALS: "Invalid email or password",
|
||||
@@ -161,3 +161,15 @@ export async function authenticateAndSaveState(
|
||||
// Save authentication state
|
||||
await page.context().storageState({ path: storagePath });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random base36 suffix of specified length
|
||||
* Used for creating unique test data to avoid conflicts
|
||||
*/
|
||||
export function makeSuffix(len: number): string {
|
||||
let s = "";
|
||||
while (s.length < len) {
|
||||
s += Math.random().toString(36).slice(2);
|
||||
}
|
||||
return s.slice(0, len);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
import { BasePage } from "../base-page";
|
||||
|
||||
export class HomePage {
|
||||
readonly page: Page;
|
||||
export class HomePage extends BasePage {
|
||||
|
||||
// Main content elements
|
||||
readonly mainContent: Locator;
|
||||
@@ -18,11 +18,10 @@ export class HomePage {
|
||||
readonly overviewSection: Locator;
|
||||
|
||||
// UI elements
|
||||
readonly themeToggle: Locator;
|
||||
readonly logo: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
super(page);
|
||||
|
||||
// Main content elements
|
||||
this.mainContent = page.locator("main");
|
||||
@@ -39,18 +38,12 @@ export class HomePage {
|
||||
this.overviewSection = page.locator('[data-testid="overview-section"]');
|
||||
|
||||
// UI elements
|
||||
this.themeToggle = page.getByLabel("Toggle theme");
|
||||
this.logo = page.locator('svg[width="300"]');
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto("/");
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
await super.goto("/");
|
||||
}
|
||||
|
||||
// Verification methods
|
||||
@@ -93,15 +86,6 @@ export class HomePage {
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
async refresh(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// Accessibility methods
|
||||
async verifyKeyboardNavigation(): Promise<void> {
|
||||
@@ -110,11 +94,6 @@ export class HomePage {
|
||||
await expect(this.themeToggle).toBeFocused();
|
||||
}
|
||||
|
||||
// Wait methods
|
||||
async waitForRedirect(expectedUrl: string): Promise<void> {
|
||||
await this.page.waitForURL(expectedUrl);
|
||||
}
|
||||
|
||||
async waitForContentLoad(): Promise<void> {
|
||||
await this.page.waitForFunction(() => {
|
||||
const main = document.querySelector("main");
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ERROR_MESSAGES,
|
||||
URLS,
|
||||
verifyLoadingState,
|
||||
} from "./helpers";
|
||||
} from "../helpers";
|
||||
|
||||
test.describe("Login Flow", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
import { HomePage } from "./home-page";
|
||||
import { BasePage } from "../base-page";
|
||||
import { HomePage } from "../home/home-page";
|
||||
|
||||
export interface SignInCredentials {
|
||||
email: string;
|
||||
@@ -11,8 +12,7 @@ export interface SocialAuthConfig {
|
||||
githubEnabled: boolean;
|
||||
}
|
||||
|
||||
export class SignInPage {
|
||||
readonly page: Page;
|
||||
export class SignInPage extends BasePage {
|
||||
readonly homePage: HomePage;
|
||||
|
||||
// Form elements
|
||||
@@ -31,20 +31,17 @@ export class SignInPage {
|
||||
readonly backButton: Locator;
|
||||
|
||||
// UI elements
|
||||
readonly title: Locator;
|
||||
readonly logo: Locator;
|
||||
readonly themeToggle: Locator;
|
||||
|
||||
// Error messages
|
||||
readonly errorMessages: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
|
||||
// SAML specific elements
|
||||
readonly samlModeTitle: Locator;
|
||||
readonly samlEmailInput: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
super(page);
|
||||
this.homePage = new HomePage(page);
|
||||
|
||||
// Form elements
|
||||
@@ -63,13 +60,10 @@ export class SignInPage {
|
||||
this.backButton = page.getByText("Back");
|
||||
|
||||
// UI elements
|
||||
this.title = page.getByText("Sign in", { exact: true });
|
||||
this.logo = page.locator('svg[width="300"]');
|
||||
this.themeToggle = page.getByLabel("Toggle theme");
|
||||
|
||||
// Error messages
|
||||
this.errorMessages = page.locator('[role="alert"], .error-message, [data-testid="error"]');
|
||||
this.loadingIndicator = page.getByText("Loading");
|
||||
|
||||
// SAML specific elements
|
||||
this.samlModeTitle = page.getByText("Sign in with SAML SSO");
|
||||
@@ -78,12 +72,7 @@ export class SignInPage {
|
||||
|
||||
// Navigation methods
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto("/sign-in");
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
await super.goto("/sign-in");
|
||||
}
|
||||
|
||||
// Form interaction methods
|
||||
@@ -148,7 +137,7 @@ export class SignInPage {
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
await expect(this.page).toHaveTitle(/Prowler/);
|
||||
await expect(this.logo).toBeVisible();
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyFormElements(): Promise<void> {
|
||||
@@ -188,13 +177,13 @@ export class SignInPage {
|
||||
}
|
||||
|
||||
async verifyNormalModeActive(): Promise<void> {
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
await expect(this.passwordInput).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyLoadingState(): Promise<void> {
|
||||
await expect(this.loginButton).toHaveAttribute("aria-disabled", "true");
|
||||
await expect(this.loadingIndicator).toBeVisible();
|
||||
await super.verifyLoadingState();
|
||||
}
|
||||
|
||||
async verifyFormValidation(): Promise<void> {
|
||||
@@ -239,30 +228,7 @@ export class SignInPage {
|
||||
return emailValue.length > 0 && passwordValue.length > 0;
|
||||
}
|
||||
|
||||
async getFormErrors(): Promise<string[]> {
|
||||
const errorElements = await this.errorMessages.all();
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const element of errorElements) {
|
||||
const text = await element.textContent();
|
||||
if (text) {
|
||||
errors.push(text.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Browser interaction methods
|
||||
async refresh(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// Session management methods
|
||||
async logout(): Promise<void> {
|
||||
@@ -271,7 +237,7 @@ export class SignInPage {
|
||||
|
||||
async verifyLogoutSuccess(): Promise<void> {
|
||||
await expect(this.page).toHaveURL("/sign-in");
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
// Advanced interaction methods
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
import { BasePage } from "../base-page";
|
||||
|
||||
export interface SignUpData {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
company?: string;
|
||||
invitationToken?: string | null;
|
||||
acceptTerms?: boolean;
|
||||
}
|
||||
|
||||
export class SignUpPage extends BasePage {
|
||||
|
||||
// Form inputs
|
||||
readonly nameInput: Locator;
|
||||
readonly companyInput: Locator;
|
||||
readonly emailInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly confirmPasswordInput: Locator;
|
||||
readonly invitationTokenInput: Locator;
|
||||
|
||||
// UI elements
|
||||
readonly submitButton: Locator;
|
||||
readonly loginLink: Locator;
|
||||
readonly termsCheckbox: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
|
||||
// Prefer stable name attributes to avoid label ambiguity in composed inputs
|
||||
this.nameInput = page.locator('input[name="name"]');
|
||||
this.companyInput = page.locator('input[name="company"]');
|
||||
this.emailInput = page.getByLabel("Email");
|
||||
this.passwordInput = page.locator('input[name="password"]');
|
||||
this.confirmPasswordInput = page.locator('input[name="confirmPassword"]');
|
||||
this.invitationTokenInput = page.locator('input[name="invitationToken"]');
|
||||
|
||||
this.submitButton = page.getByRole("button", { name: "Sign up" });
|
||||
this.loginLink = page.getByRole("link", { name: "Log in" });
|
||||
this.termsCheckbox = page.getByText("I agree with the");
|
||||
}
|
||||
|
||||
async goto(): Promise<void> {
|
||||
await super.goto("/sign-up");
|
||||
}
|
||||
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
await expect(this.page.getByText("Sign up", { exact: true })).toBeVisible();
|
||||
await expect(this.emailInput).toBeVisible();
|
||||
await expect(this.submitButton).toBeVisible();
|
||||
}
|
||||
|
||||
async fillName(name: string): Promise<void> {
|
||||
await this.nameInput.fill(name);
|
||||
}
|
||||
|
||||
async fillCompany(company?: string): Promise<void> {
|
||||
if (company) {
|
||||
await this.companyInput.fill(company);
|
||||
}
|
||||
}
|
||||
|
||||
async fillEmail(email: string): Promise<void> {
|
||||
await this.emailInput.fill(email);
|
||||
}
|
||||
|
||||
async fillPassword(password: string): Promise<void> {
|
||||
await this.passwordInput.fill(password);
|
||||
}
|
||||
|
||||
async fillConfirmPassword(confirmPassword: string): Promise<void> {
|
||||
await this.confirmPasswordInput.fill(confirmPassword);
|
||||
}
|
||||
|
||||
async fillInvitationToken(token?: string | null): Promise<void> {
|
||||
if (token) {
|
||||
await this.invitationTokenInput.fill(token);
|
||||
}
|
||||
}
|
||||
|
||||
async acceptTermsIfPresent(accept: boolean = true): Promise<void> {
|
||||
// Only in cloud env; check presence before interacting
|
||||
if (await this.termsCheckbox.isVisible()) {
|
||||
if (accept) {
|
||||
await this.termsCheckbox.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async submit(): Promise<void> {
|
||||
await this.submitButton.click();
|
||||
}
|
||||
|
||||
async signup(data: SignUpData): Promise<void> {
|
||||
await this.fillName(data.name);
|
||||
await this.fillCompany(data.company);
|
||||
await this.fillEmail(data.email);
|
||||
await this.fillPassword(data.password);
|
||||
await this.fillConfirmPassword(data.confirmPassword);
|
||||
await this.fillInvitationToken(data.invitationToken ?? undefined);
|
||||
await this.acceptTermsIfPresent(data.acceptTerms ?? true);
|
||||
await this.submit();
|
||||
}
|
||||
|
||||
async verifyLoadingState(): Promise<void> {
|
||||
await expect(this.submitButton).toHaveAttribute("aria-disabled", "true");
|
||||
await super.verifyLoadingState();
|
||||
}
|
||||
|
||||
async verifyRedirectToLogin(): Promise<void> {
|
||||
await expect(this.page).toHaveURL("/sign-in");
|
||||
}
|
||||
|
||||
async verifyRedirectToEmailVerification(): Promise<void> {
|
||||
await expect(this.page).toHaveURL("/email-verification");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { SignUpPage } from "./sign-up-page";
|
||||
import { SignInPage } from "../sign-in/sign-in-page";
|
||||
import { makeSuffix } from "../helpers";
|
||||
|
||||
test.describe("Sign Up Flow", () => {
|
||||
test("should register a new user successfully", { tag: ['@critical'] }, async ({ page }) => {
|
||||
// Initialize page objects for sign-up and sign-in flows
|
||||
const signUpPage = new SignUpPage(page);
|
||||
await signUpPage.goto();
|
||||
|
||||
// Generate unique test data to avoid conflicts with existing users
|
||||
// Create a base36 random suffix of exactly 10 characters for uniqueness
|
||||
const suffix = makeSuffix(10);
|
||||
const uniqueEmail = `e2e+${suffix}@prowler.com`;
|
||||
|
||||
// Fill and submit the sign-up form with valid test data
|
||||
// Name is exactly 19 characters to meet the max length requirement (20 chars)
|
||||
await signUpPage.signup({
|
||||
name: `E2E User ${suffix}`, // 9 chars + 10 chars = 19 chars
|
||||
company: `Test E2E Co ${suffix}`,
|
||||
email: uniqueEmail,
|
||||
password: "Thisisapassword123@",
|
||||
confirmPassword: "Thisisapassword123@",
|
||||
acceptTerms: true,
|
||||
});
|
||||
|
||||
// Verify successful sign-up redirects to login page (OSS environment)
|
||||
await signUpPage.verifyRedirectToLogin();
|
||||
|
||||
// Verify the newly created user can successfully log in
|
||||
// This ensures the user account was properly created and is functional
|
||||
const signInPage = new SignInPage(page);
|
||||
await signInPage.login({
|
||||
email: uniqueEmail,
|
||||
password: "Thisisapassword123@",
|
||||
});
|
||||
await signInPage.verifySuccessfulLogin();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user