Compare commits

...

12 Commits

Author SHA1 Message Date
StylusFrost
cbe17b0fff test(ui): update GitHub sign-in button locator in E2E tests
- Changed the locator for the GitHub sign-in button in the sign-up page tests to use the input type and value attributes for better accuracy.
- This update enhances the reliability of the E2E tests by ensuring the correct element is targeted during the GitHub OAuth flow.
2025-10-15 16:12:57 +02:00
StylusFrost
73384ef326 test(ui): update GitHub OAuth secrets in E2E test workflow
- Replaced the existing GitHub OAuth client ID and secret with new environment variables for E2E testing.
- This change ensures that the E2E tests use the correct secrets for GitHub OAuth integration, improving test reliability.
2025-10-15 11:54:16 +02:00
StylusFrost
6ad22ec274 test(ui): add GitHub OAuth secrets to E2E test workflow
- Included SOCIAL_GITHUB_OAUTH_CLIENT_ID and SOCIAL_GITHUB_OAUTH_CLIENT_SECRET in the E2E test workflow for GitHub OAuth integration.
- This enhancement supports the recent implementation of GitHub social login in the application.
2025-10-15 11:53:35 +02:00
StylusFrost
512f0bb6df test(ui): implement GitHub OAuth flow for social sign-up
- Added support for GitHub social login in the sign-up process.
- Updated the sign-up page to include a GitHub login button and related methods.
- Enhanced E2E tests to validate the complete GitHub OAuth flow, ensuring users can authenticate and return to the application successfully.
- Updated documentation to reflect the new test case for GitHub social sign-up.
2025-10-15 11:35:34 +02:00
StylusFrost
cef7fcc24b Merge branch 'master' into PROWLER-187-create-new-user 2025-10-14 11:03:23 +02:00
StylusFrost
fcf42937aa test(ui): enhance session management tests with new helper functions
- Updated the `verifyLogoutSuccess` function to use a regex for URL verification.
- Added new helper functions `getSession` and `verifySessionValid` to streamline session validation in tests, ensuring that session data is correctly retrieved and validated.
2025-10-14 10:26:00 +02:00
StylusFrost
2c9d8ad8ea test(ui): simplify sign-up page tests by removing redundant loading state verification
- Removed the `verifyLoadingState` method from the `SignUpPage` class as it was redundant.
- Updated comments in the sign-up test to enhance clarity and focus on key actions.
- Added a call to `verifyNoErrors` to ensure no errors occur during the sign-up process.
2025-10-10 18:21:43 +02:00
StylusFrost
f424342e7e test(ui): add E2E tests document for user sign-up flow
- Documented test case details including flow steps, expected results, and key verification points.
- Enhanced existing sign-up test specifications to include relevant tags for better categorization and tracking.
2025-10-10 11:53:18 +02:00
StylusFrost
9b7e4f59e1 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.
2025-10-09 16:19:27 +02:00
StylusFrost
d21222aa3a test(ui): format Playwright configuration for consistency
- Added missing commas and improved formatting in the Playwright configuration file to enhance readability and maintain consistency across the codebase.
2025-10-09 11:05:09 +02:00
StylusFrost
bdbb2fad78 test(ui): update Playwright test commands to specify project. Compatibility with current e2e test
- Modified Playwright test commands in package.json to explicitly use the 'chromium' project.
2025-10-09 10:54:34 +02:00
StylusFrost
cf7b66101c test(ui): enhance Playwright test setups for user authentication
- Added multiple authentication setup files for different user roles (admin, manage scans, manage integrations, etc.) to streamline end-to-end testing.
- Introduced a helper function to authenticate users and save their state for reuse in tests.
- Updated Playwright configuration to include new authentication projects.

This change improves the testing framework by allowing for more comprehensive and role-specific user authentication scenarios.
2025-10-09 10:27:37 +02:00
19 changed files with 1141 additions and 6 deletions

View File

@@ -18,6 +18,11 @@ jobs:
AUTH_TRUST_HOST: true
NEXTAUTH_URL: 'http://localhost:3000'
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1'
E2E_GITHUB_USER: ${{ secrets.E2E_GITHUB_USER }}
E2E_GITHUB_PASSWORD: ${{ secrets.E2E_GITHUB_PASSWORD }}
SOCIAL_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.E2E_SOCIAL_GITHUB_OAUTH_CLIENT_ID }}
SOCIAL_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.E2E_SOCIAL_GITHUB_OAUTH_CLIENT_SECRET }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

3
ui/.gitignore vendored
View File

@@ -33,4 +33,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts
playwright/.auth

View File

@@ -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",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:headed": "playwright test --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"
},

View File

@@ -20,9 +20,73 @@ export default defineConfig({
},
projects: [
// ===========================================
// Authentication Setup Projects
// ===========================================
// These projects handle user authentication for different permission levels
// Each setup creates authenticated state files that can be reused by test suites
// Admin user authentication setup
// Creates authenticated state for admin users with full system permissions
{
name: "admin.auth.setup",
testMatch: "admin.auth.setup.ts",
},
// Scans management user authentication setup
// Creates authenticated state for users with scan management permissions
{
name: "manage-scans.auth.setup",
testMatch: "manage-scans.auth.setup.ts",
},
// Integrations management user authentication setup
// Creates authenticated state for users with integration management permissions
{
name: "manage-integrations.auth.setup",
testMatch: "manage-integrations.auth.setup.ts",
},
// Account management user authentication setup
// Creates authenticated state for users with account management permissions
{
name: "manage-account.auth.setup",
testMatch: "manage-account.auth.setup.ts",
},
// Cloud providers management user authentication setup
// Creates authenticated state for users with cloud provider management permissions
{
name: "manage-cloud-providers.auth.setup",
testMatch: "manage-cloud-providers.auth.setup.ts",
},
// Unlimited visibility user authentication setup
// Creates authenticated state for users with unlimited visibility permissions
{
name: "unlimited-visibility.auth.setup",
testMatch: "unlimited-visibility.auth.setup.ts",
},
// Invite and manage users authentication setup
// Creates authenticated state for users with user invitation and management permissions
{
name: "invite-and-manage-users.auth.setup",
testMatch: "invite-and-manage-users.auth.setup.ts",
},
// ===========================================
// Test Suite Projects
// ===========================================
// These projects run the actual test suites
{
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",
},
],

159
ui/tests/base-page.ts Normal file
View File

@@ -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` });
}
}

View File

@@ -1,4 +1,5 @@
import { Page, expect } from "@playwright/test";
import { SignInPage, SignInCredentials } from "./sign-in/sign-in-page";
export const ERROR_MESSAGES = {
INVALID_CREDENTIALS: "Invalid email or password",
@@ -138,6 +139,41 @@ export async function verifyDashboardRoute(page: Page) {
await expect(page).toHaveURL("/");
}
export async function authenticateAndSaveState(
page: Page,
email: string,
password: string,
storagePath: string,
) {
if (!email || !password) {
throw new Error('Email and password are required for authentication and save state');
}
// Create SignInPage instance
const signInPage = new SignInPage(page);
const credentials: SignInCredentials = { email, password };
// Perform authentication steps using Page Object Model
await signInPage.goto();
await signInPage.login(credentials);
await signInPage.verifySuccessfulLogin();
// 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);
}
export async function getSession(page: Page) {
const response = await page.request.get("/api/auth/session");
return response.json();

103
ui/tests/home/home-page.ts Normal file
View File

@@ -0,0 +1,103 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
export class HomePage extends BasePage {
// Main content elements
readonly mainContent: Locator;
readonly breadcrumbs: Locator;
readonly overviewHeading: Locator;
// Navigation elements
readonly navigationMenu: Locator;
readonly userMenu: Locator;
readonly signOutButton: Locator;
// Dashboard elements
readonly dashboardCards: Locator;
readonly overviewSection: Locator;
// UI elements
readonly logo: Locator;
constructor(page: Page) {
super(page);
// Main content elements
this.mainContent = page.locator("main");
this.breadcrumbs = page.getByLabel("Breadcrumbs");
this.overviewHeading = page.getByRole("heading", { name: "Overview", exact: true });
// Navigation elements
this.navigationMenu = page.locator("nav");
this.userMenu = page.getByRole("button", { name: /user menu/i });
this.signOutButton = page.getByRole("button", { name: "Sign out" });
// Dashboard elements
this.dashboardCards = page.locator('[data-testid="dashboard-card"]');
this.overviewSection = page.locator('[data-testid="overview-section"]');
// UI elements
this.logo = page.locator('svg[width="300"]');
}
// Navigation methods
async goto(): Promise<void> {
await super.goto("/");
}
// Verification methods
async verifyPageLoaded(): Promise<void> {
await expect(this.page).toHaveURL("/");
await expect(this.mainContent).toBeVisible();
await expect(this.overviewHeading).toBeVisible();
}
async verifyBreadcrumbs(): Promise<void> {
await expect(this.breadcrumbs).toBeVisible();
await expect(this.overviewHeading).toBeVisible();
}
async verifyMainContent(): Promise<void> {
await expect(this.mainContent).toBeVisible();
}
// Navigation methods
async navigateToOverview(): Promise<void> {
await this.overviewHeading.click();
}
async openUserMenu(): Promise<void> {
await this.userMenu.click();
}
async signOut(): Promise<void> {
await this.openUserMenu();
await this.signOutButton.click();
}
// Dashboard methods
async verifyDashboardCards(): Promise<void> {
await expect(this.dashboardCards.first()).toBeVisible();
}
async verifyOverviewSection(): Promise<void> {
await expect(this.overviewSection).toBeVisible();
}
// Utility methods
// Accessibility methods
async verifyKeyboardNavigation(): Promise<void> {
// Test tab navigation through main elements
await this.page.keyboard.press("Tab");
await expect(this.themeToggle).toBeFocused();
}
async waitForContentLoad(): Promise<void> {
await this.page.waitForFunction(() => {
const main = document.querySelector("main");
return main && main.offsetHeight > 0;
});
}
}

View File

@@ -0,0 +1,16 @@
import { test as authAdminSetup } from '@playwright/test';
import { authenticateAndSaveState } from '@/tests/helpers';
const adminUserFile = 'playwright/.auth/admin_user.json';
authAdminSetup('authenticate as admin e2e user', async ({ page }) => {
const adminEmail = process.env.E2E_ADMIN_USER;
const adminPassword = process.env.E2E_ADMIN_PASSWORD;
if (!adminEmail || !adminPassword) {
throw new Error('E2E_ADMIN_USER and E2E_ADMIN_PASSWORD environment variables are required');
}
await authenticateAndSaveState(page, adminEmail, adminPassword, adminUserFile);
});

View File

@@ -0,0 +1,15 @@
import { test as authInviteAndManageUsersSetup } from '@playwright/test';
import { authenticateAndSaveState } from '@/tests/helpers';
const inviteAndManageUsersUserFile = 'playwright/.auth/invite_and_manage_users_user.json';
authInviteAndManageUsersSetup('authenticate as invite and manage users e2e user', async ({ page }) => {
const inviteAndManageUsersEmail = process.env.E2E_INVITE_AND_MANAGE_USERS_USER;
const inviteAndManageUsersPassword = process.env.E2E_INVITE_AND_MANAGE_USERS_PASSWORD;
if (!inviteAndManageUsersEmail || !inviteAndManageUsersPassword) {
throw new Error('E2E_INVITE_AND_MANAGE_USERS_USER and E2E_INVITE_AND_MANAGE_USERS_PASSWORD environment variables are required');
}
await authenticateAndSaveState(page, inviteAndManageUsersEmail, inviteAndManageUsersPassword, inviteAndManageUsersUserFile);
});

View File

@@ -0,0 +1,16 @@
import { test as authManageAccountSetup } from '@playwright/test';
import { authenticateAndSaveState } from '@/tests/helpers';
const manageAccountUserFile = 'playwright/.auth/manage_account_user.json';
authManageAccountSetup('authenticate as manage account e2e user', async ({ page }) => {
const accountEmail = process.env.E2E_MANAGE_ACCOUNT_USER;
const accountPassword = process.env.E2E_MANAGE_ACCOUNT_PASSWORD;
if (!accountEmail || !accountPassword) {
throw new Error('E2E_MANAGE_ACCOUNT_USER and E2E_MANAGE_ACCOUNT_PASSWORD environment variables are required');
}
await authenticateAndSaveState(page, accountEmail, accountPassword, manageAccountUserFile);
});

View File

@@ -0,0 +1,16 @@
import { test as authManageCloudProvidersSetup } from '@playwright/test';
import { authenticateAndSaveState } from '@/tests/helpers';
const manageCloudProvidersUserFile = 'playwright/.auth/manage_cloud_providers_user.json';
authManageCloudProvidersSetup('authenticate as manage cloud providers e2e user', async ({ page }) => {
const cloudProvidersEmail = process.env.E2E_MANAGE_CLOUD_PROVIDERS_USER;
const cloudProvidersPassword = process.env.E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD;
if (!cloudProvidersEmail || !cloudProvidersPassword) {
throw new Error('E2E_MANAGE_CLOUD_PROVIDERS_USER and E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD environment variables are required');
}
await authenticateAndSaveState(page, cloudProvidersEmail, cloudProvidersPassword, manageCloudProvidersUserFile);
});

View File

@@ -0,0 +1,15 @@
import { test as authManageIntegrationsSetup } from '@playwright/test';
import { authenticateAndSaveState } from '@/tests/helpers';
const manageIntegrationsUserFile = 'playwright/.auth/manage_integrations_user.json';
authManageIntegrationsSetup('authenticate as integrations e2e user', async ({ page }) => {
const integrationsEmail = process.env.E2E_MANAGE_INTEGRATIONS_USER;
const integrationsPassword = process.env.E2E_MANAGE_INTEGRATIONS_PASSWORD;
if (!integrationsEmail || !integrationsPassword) {
throw new Error('E2E_MANAGE_INTEGRATIONS_USER and E2E_MANAGE_INTEGRATIONS_PASSWORD environment variables are required');
}
await authenticateAndSaveState(page, integrationsEmail, integrationsPassword, manageIntegrationsUserFile);
});

View File

@@ -0,0 +1,15 @@
import { test as authManageScansSetup } from '@playwright/test';
import { authenticateAndSaveState } from '@/tests/helpers';
const manageScansUserFile = 'playwright/.auth/manage_scans_user.json';
authManageScansSetup('authenticate as scans e2e user', async ({ page }) => {
const scansEmail = process.env.E2E_MANAGE_SCANS_USER;
const scansPassword = process.env.E2E_MANAGE_SCANS_PASSWORD;
if (!scansEmail || !scansPassword) {
throw new Error('E2E_MANAGE_SCANS_USER and E2E_MANAGE_SCANS_PASSWORD environment variables are required');
}
await authenticateAndSaveState(page, scansEmail, scansPassword, manageScansUserFile);
});

View File

@@ -0,0 +1,15 @@
import { test as authUnlimitedVisibilitySetup } from '@playwright/test';
import { authenticateAndSaveState } from '@/tests/helpers';
const unlimitedVisibilityUserFile = 'playwright/.auth/unlimited_visibility_user.json';
authUnlimitedVisibilitySetup('authenticate as unlimited visibility e2e user', async ({ page }) => {
const unlimitedVisibilityEmail = process.env.E2E_UNLIMITED_VISIBILITY_USER;
const unlimitedVisibilityPassword = process.env.E2E_UNLIMITED_VISIBILITY_PASSWORD;
if (!unlimitedVisibilityEmail || !unlimitedVisibilityPassword) {
throw new Error('E2E_UNLIMITED_VISIBILITY_USER and E2E_UNLIMITED_VISIBILITY_PASSWORD environment variables are required');
}
await authenticateAndSaveState(page, unlimitedVisibilityEmail, unlimitedVisibilityPassword, unlimitedVisibilityUserFile);
});

View File

@@ -20,7 +20,7 @@ import {
ERROR_MESSAGES,
URLS,
verifyLoadingState,
} from "./helpers";
} from "../helpers";
test.describe("Login Flow", () => {
test.beforeEach(async ({ page }) => {

View File

@@ -0,0 +1,281 @@
import { Page, Locator, expect } 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;
// 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.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.loginButton = page.getByRole("button", { name: "Log in" });
this.form = page.locator("form");
// Social authentication buttons
this.googleButton = page.getByText("Continue with Google");
this.githubButton = page.getByText("Continue with Github");
this.samlButton = page.getByText("Continue with SAML SSO");
// Navigation elements
this.signUpLink = page.getByRole("link", { name: "Sign up" });
this.backButton = page.getByText("Back");
// UI elements
this.logo = page.locator('svg[width="300"]');
// Error messages
this.errorMessages = page.locator('[role="alert"], .error-message, [data-testid="error"]');
// SAML specific elements
this.samlModeTitle = page.getByText("Sign in with SAML SSO");
this.samlEmailInput = page.getByLabel("Email");
}
// Navigation methods
async goto(): Promise<void> {
await super.goto("/sign-in");
}
// 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.page.getByText("Sign in", { exact: true })).toBeVisible();
}
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();
}
async verifyLoginError(errorMessage: string = "Invalid email or password"): Promise<void> {
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.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 super.verifyLoadingState();
}
async verifyFormValidation(): Promise<void> {
// Check for common validation messages
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)).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;
}
// Browser interaction methods
// Session management methods
async logout(): Promise<void> {
await this.homePage.signOut();
}
async verifyLogoutSuccess(): Promise<void> {
await expect(this.page).toHaveURL("/sign-in");
await expect(this.page.getByText("Sign in", { exact: true })).toBeVisible();
}
// 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.getByText("SAML Authentication Error");
if (await samlError.isVisible()) {
// Handle SAML error if present
console.log("SAML authentication error detected");
}
}
// 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);
}
}

View File

@@ -0,0 +1,201 @@
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;
// Social login buttons
readonly githubButton: 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");
// Social login buttons
this.githubButton = page.getByRole("button", { name: "Continue with Github" });
}
async goto(): Promise<void> {
await super.goto("/sign-up");
}
async verifyPageLoaded(): Promise<void> {
// Verify unique title - only appears on sign-up page
await expect(this.page.locator('p').getByText("Sign up", { exact: true }).first()).toBeVisible();
// Verify all required form fields are present
await expect(this.nameInput).toBeVisible();
await expect(this.emailInput).toBeVisible();
await expect(this.passwordInput).toBeVisible();
await expect(this.confirmPasswordInput).toBeVisible();
// Verify primary action button
await expect(this.submitButton).toBeVisible();
// Verify distinctive separator between form and social login
await expect(this.page.getByText("OR", { exact: true })).toBeVisible();
// Verify social login options are available (distinctive of sign-up vs other pages)
await expect(this.page.getByText("Continue with Github")).toBeVisible();
await expect(this.page.getByText("Continue with Google")).toBeVisible();
// Verify sign-up specific link (different from sign-in page)
await expect(this.page.getByText("Already have an account?")).toBeVisible();
await expect(this.loginLink).toBeVisible();
// Verify correct URL
expect(this.page.url()).toContain('/sign-up');
}
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 verifyRedirectToLogin(): Promise<void> {
await expect(this.page).toHaveURL("/sign-in");
}
async verifyRedirectToEmailVerification(): Promise<void> {
await expect(this.page).toHaveURL("/email-verification");
}
// Social login methods
async clickGithubLogin(): Promise<void> {
await this.githubButton.click();
}
async verifyGithubButtonVisible(): Promise<void> {
await expect(this.githubButton).toBeVisible();
}
async verifyGithubButtonEnabled(): Promise<void> {
await expect(this.githubButton).toBeEnabled();
}
async verifyRedirectToGithubOAuth(): Promise<void> {
// Verify redirect to Github OAuth page
await expect(this.page).toHaveURL(/github\.com\/login/);
}
async verifyGithubOAuthFlow(): Promise<void> {
// Verify Github OAuth page elements
await expect(this.page.getByText("Sign in to GitHub")).toBeVisible();
await expect(this.page.getByText("to continue to Prowler")).toBeVisible();
}
async fillGithubCredentials(username: string, password: string): Promise<void> {
// Fill Github login form based on MCP exploration
await this.page.getByRole("textbox", { name: "Username or email address" }).fill(username);
await this.page.getByRole("textbox", { name: "Password" }).fill(password);
}
async submitGithubLogin(): Promise<void> {
// Click Github Sign in button
await this.page.locator('input[type="submit"][name="commit"][value="Sign in"]').click();
}
async completeGithubOAuth(username: string, password: string): Promise<void> {
// Complete the Github OAuth flow
await this.fillGithubCredentials(username, password);
await this.submitGithubLogin();
}
async verifyGithubApplicationInfo(): Promise<void> {
// Verify Prowler application info is displayed on GitHub OAuth page
await expect(this.page.locator('img[alt*="Prowler"]')).toBeVisible();
// Verify the OAuth consent message shows Prowler app name
await expect(this.page.getByText(/to continue to.*Prowler/i)).toBeVisible();
// Verify "Sign in to GitHub" text is present
await expect(this.page.getByText("Sign in to GitHub")).toBeVisible();
// Verify GitHub OAuth form elements are present
await expect(this.page.getByRole("textbox", { name: /username or email/i })).toBeVisible();
await expect(this.page.getByRole("textbox", { name: /password/i })).toBeVisible();
await expect(this.page.locator('input[type="submit"][name="commit"][value="Sign in"]')).toBeVisible();
}
}

View File

@@ -0,0 +1,96 @@
# E2E Tests: User Sign-Up
**Suite ID:** `SIGNUP-E2E`
**Feature:** New user registration flow.
---
## Test Case: `SIGNUP-E2E-001` - Successful new user registration and login
**Priority:** `critical`
**Tags:**
- type → @e2e
- feature → @signup
**Description/Objetive:** Registers a new user with valid data, verifies redirect to Login, and confirms the user can authenticate.
**Preconditions:**
- Application is running, email domain & password is acceptable for sign-up.
- No existing data in Prowler is required; the test can run on a clean state.
### Flow Steps:
1. Navigate to the Sign up page.
2. Fill the form with valid data (unique email, valid password, terms accepted).
3. Submit the form.
4. Verify redirect to the Login page.
5. Log in with the newly created credentials.
### Expected Result:
- Sign-up succeeds and redirects to Login.
- User can log in successfully using the created credentials and reach the home page.
### Key verification points:
- After submitting sign-up, the URL changes to `/sign-in`.
- The newly created credentials can be used to sign in successfully.
- After login, the user lands on the home (`/`) and main content is visible.
### Notes:
- Test data uses a random base36 suffix to avoid collisions with email.
---
## Test Case: `SIGNUP-E2E-002` - Github Social Sign-up OAuth Flow
**Priority:** `critical`
**Tags:**
- type → @e2e
- feature → @signup
- social → @social
**Description/Objective:** Validates that users can complete the full Github OAuth flow for social sign-up, including authentication and successful return to Prowler
**Preconditions:**
- Application is running
- Github OAuth app is configured
- E2E_GITHUB_USER and E2E_GITHUB_PASSWORD environment variables are set with valid Github credentials
### Flow Steps:
1. Navigate to sign-up page
2. Verify page loads with social login options
3. Verify Github login button is visible and enabled
4. Click "Continue with Github" button
5. Verify redirect to Github OAuth page
6. Verify OAuth configuration parameters
7. Fill Github credentials (username and password)
8. Submit Github login form
9. Verify successful redirect back to Prowler
### Expected Result:
- User is redirected to Github OAuth authorization page
- OAuth URL contains correct client_id, redirect_uri, and scope parameters
- Github OAuth page displays proper application information
- User can successfully authenticate with Github credentials
- User is redirected back to Prowler application after successful authentication
### Key verification points:
- Github button is visible and clickable on sign-up page
- Redirect to github.com/login occurs correctly
- OAuth URL structure follows GitHub OAuth format (https://github.com/login)
- GitHub OAuth page displays Prowler application logo and information
- GitHub OAuth page shows correct consent message "to continue to Prowler"
- GitHub OAuth page shows "Sign in to GitHub" header
- GitHub login form elements are present and accessible (username/email, password, sign in button)
- Github login form accepts credentials correctly
- Successful authentication redirects back to Prowler home
- After redirect, verify authenticated area is visible (e.g., main dashboard content)
### Notes:
- Test requires E2E_GITHUB_USER and E2E_GITHUB_PASSWORD environment variables
- Test completes full OAuth flow including Github authentication
- Test verifies successful social sign-up integration
- Github credentials must be valid for test to pass

View File

@@ -0,0 +1,81 @@
import { test, expect } from "@playwright/test";
import { SignUpPage } from "./sign-up-page";
import { SignInPage } from "../sign-in/sign-in-page";
import { makeSuffix, TEST_CREDENTIALS } from "../helpers";
test.describe("Sign Up Flow", () => {
test("should register a new user successfully", { tag: ['@critical', '@e2e', '@signup', '@SIGNUP-E2E-001'] }, async ({ page }) => {
const signUpPage = new SignUpPage(page);
await signUpPage.goto();
// Generate unique test data
const suffix = makeSuffix(10);
const uniqueEmail = `e2e+${suffix}@prowler.com`;
// Fill and submit the sign-up form
await signUpPage.signup({
name: `E2E User ${suffix}`,
company: `Test E2E Co ${suffix}`,
email: uniqueEmail,
password: "Thisisapassword123@",
confirmPassword: "Thisisapassword123@",
acceptTerms: true,
});
// Verify no errors occurred during sign-up
await signUpPage.verifyNoErrors();
// Verify redirect to login page
await signUpPage.verifyRedirectToLogin();
// Verify the newly created user can log in successfully
const signInPage = new SignInPage(page);
await signInPage.login({
email: uniqueEmail,
password: "Thisisapassword123@",
});
await signInPage.verifySuccessfulLogin();
});
test("should complete Github OAuth flow for social sign-up", { tag: ['@critical', '@e2e', '@signup', '@social', '@SIGNUP-E2E-002'] }, async ({ page }) => {
// Verify Github credentials are available
const githubUsername = process.env.E2E_GITHUB_USER;
const githubPassword = process.env.E2E_GITHUB_PASSWORD;
if (!githubUsername || !githubPassword) {
throw new Error('E2E_GITHUB_USER and E2E_GITHUB_PASSWORD environment variables are required for Github OAuth tests');
}
const signUpPage = new SignUpPage(page);
await signUpPage.goto();
// Verify page loaded correctly
await signUpPage.verifyPageLoaded();
// Verify Github social login button is visible and enabled
await signUpPage.verifyGithubButtonVisible();
await signUpPage.verifyGithubButtonEnabled();
// Click on Github login button
await signUpPage.clickGithubLogin();
// Verify redirect to Github OAuth
await signUpPage.verifyRedirectToGithubOAuth();
// Verify Github OAuth page loaded correctly
await signUpPage.verifyGithubOAuthFlow();
// Verify GitHub displays correct application information
await signUpPage.verifyGithubApplicationInfo();
// Complete Github OAuth login
await signUpPage.completeGithubOAuth(githubUsername, githubPassword);
// Verify the user is redirected to the home page after successful authentication
const signInPage = new SignInPage(page);
await signInPage.verifySuccessfulLogin();
});
});