feat: establish Playwright E2E testing infrastructure with configuration and initial tests

This commit is contained in:
Devansh Vashisht
2025-12-24 04:57:28 +05:30
parent 978a62a6b3
commit 1d2a23b5b6
13 changed files with 2730 additions and 2357 deletions
+136
View File
@@ -0,0 +1,136 @@
import { Page, Locator } from '@playwright/test';
/**
* BasePage - Abstract base class for Page Object Model.
*
* Provides common functionality for all page objects:
* - Navigation helpers with wait states
* - Reusable interaction methods
* - Screenshot utilities
*
* All page objects should extend this class.
*/
export abstract class BasePage {
/**
* The Playwright Page instance.
* Protected to allow access in child classes.
*/
protected readonly page: Page;
/**
* The URL path for this page (relative to baseURL).
* Must be implemented by child classes.
*/
abstract readonly url: string;
/**
* Creates a new BasePage instance.
* @param page - The Playwright Page instance
*/
constructor(page: Page) {
this.page = page;
}
/**
* Navigate to this page's URL.
* Uses the route defined in the `url` property.
*/
async navigate(): Promise<void> {
// Navigate with extended timeout and wait until load event
await this.page.goto(this.url, {
waitUntil: 'load',
timeout: 60000
});
await this.waitForLoad();
}
/**
* Wait for the page to be fully loaded.
* Override in child classes for page-specific load conditions.
*/
async waitForLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
/**
* Get the page title.
* @returns The document title
*/
async getTitle(): Promise<string> {
return this.page.title();
}
/**
* Get the current URL.
* @returns The current page URL
*/
getCurrentUrl(): string {
return this.page.url();
}
/**
* Wait for an element to be visible.
* @param locator - The Playwright Locator
* @param timeout - Optional timeout in milliseconds
*/
async waitForVisible(locator: Locator, timeout?: number): Promise<void> {
await locator.waitFor({ state: 'visible', timeout });
}
/**
* Wait for an element to be hidden.
* @param locator - The Playwright Locator
* @param timeout - Optional timeout in milliseconds
*/
async waitForHidden(locator: Locator, timeout?: number): Promise<void> {
await locator.waitFor({ state: 'hidden', timeout });
}
/**
* Click an element and wait for navigation.
* Useful for buttons that trigger page redirects.
* @param locator - The Playwright Locator to click
*/
async clickAndWaitForNavigation(locator: Locator): Promise<void> {
await Promise.all([
this.page.waitForURL(/.*/, { waitUntil: 'networkidle' }),
locator.click()
]);
}
/**
* Fill a form field after clearing existing content.
* @param locator - The Playwright Locator for the input
* @param value - The value to fill
*/
async fillField(locator: Locator, value: string): Promise<void> {
await locator.clear();
await locator.fill(value);
}
/**
* Take a screenshot of the current page.
* @param name - Name for the screenshot file
*/
async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({ path: `playwright-report/${name}.png` });
}
/**
* Check if an element is visible.
* @param locator - The Playwright Locator
* @returns true if visible, false otherwise
*/
async isVisible(locator: Locator): Promise<boolean> {
return locator.isVisible();
}
/**
* Get text content from an element.
* @param locator - The Playwright Locator
* @returns The text content or null
*/
async getText(locator: Locator): Promise<string | null> {
return locator.textContent();
}
}
+189
View File
@@ -0,0 +1,189 @@
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
/**
* LoginPage - Page Object for the Mifos X Login page.
*
* Encapsulates all login-related interactions and element locators.
* Extends BasePage for common functionality.
*
* Locator Strategy:
* - Uses exact selectors from Playwright codegen for maximum stability
* - Preserves complex selectors (filter, nth) as generated by codegen
* - These selectors match the actual DOM structure of the Angular Material UI
*/
export class LoginPage extends BasePage {
/**
* The URL path for the login page.
*/
readonly url = '/#/login';
/**
* Centralized locators for all page elements.
* Using exact selectors from Playwright codegen.
*/
/**
* Get the username input wrapper div.
* Uses codegen selector: div.filter({ hasText: 'Username' }).nth(5)
*/
get usernameDivWrapper(): Locator {
return this.page.locator('div').filter({ hasText: 'Username' }).nth(5);
}
/**
* Get the username input field.
* Uses codegen selector for the actual input.
*/
get usernameInput(): Locator {
return this.page.getByRole('textbox', { name: 'Username' });
}
/**
* Get the password input wrapper div.
* Uses codegen selector: div.filter({ hasText: 'Password' }).nth(5)
*/
get passwordDivWrapper(): Locator {
return this.page.locator('div').filter({ hasText: 'Password' }).nth(5);
}
/**
* Get the password input field.
* Uses codegen selector for the actual input.
*/
get passwordInput(): Locator {
return this.page.getByRole('textbox', { name: 'Password' });
}
/**
* Get the login button.
* Uses codegen selector: getByRole('button', { name: 'Login' })
*/
get loginButton(): Locator {
return this.page.getByRole('button', { name: 'Login' });
}
// Additional helper locators for validation and assertions
private readonly locators = {
errorMessage: 'mat-error',
progressBar: 'mat-progress-bar',
loginForm: '#login-form'
};
/**
* Get all error messages on the page.
*/
get errorMessages(): Locator {
return this.page.locator(this.locators.errorMessage);
}
/**
* Get the progress bar (loading indicator).
*/
get progressBar(): Locator {
return this.page.locator(this.locators.progressBar);
}
/**
* Wait for the login page to be fully loaded.
* Overrides BasePage to wait for login form visibility.
*/
async waitForLoad(): Promise<void> {
// Wait for basic page load
await this.page.waitForLoadState('domcontentloaded');
// Try to wait for network idle, but don't fail if it takes too long
try {
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
} catch (e) {
console.log('Network idle timeout - proceeding anyway');
}
// Wait for the login button to be visible with extended timeout
await this.loginButton.waitFor({ state: 'visible', timeout: 60000 });
}
/**
* Perform login with the given credentials.
* Uses the exact interaction sequence from codegen.
*
* @param username - The username to enter
* @param password - The password to enter
*/
async login(username: string, password: string): Promise<void> {
// Click the username wrapper div (as per codegen)
await this.usernameDivWrapper.click();
// Fill the username input
await this.usernameInput.fill(username);
// Click the password wrapper div (as per codegen)
await this.passwordDivWrapper.click();
// Fill the password input
await this.passwordInput.fill(password);
// Click the login button
await this.loginButton.click();
}
/**
* Perform login and wait for successful navigation.
* Use this when expecting a successful login.
*
* @param username - The username to enter
* @param password - The password to enter
*/
async loginAndWaitForDashboard(username: string, password: string): Promise<void> {
await this.login(username, password);
// Wait for navigation away from login page
await this.page.waitForURL(/.*(?<!login)$/, {
timeout: 30000,
waitUntil: 'networkidle'
});
}
/**
* Check if the login button is enabled.
* @returns true if the login button is enabled
*/
async isLoginButtonEnabled(): Promise<boolean> {
return this.loginButton.isEnabled();
}
/**
* Get the count of validation errors displayed.
* @returns Number of visible error messages
*/
async getErrorCount(): Promise<number> {
return this.errorMessages.count();
}
/**
* Check if page is showing a loading state.
* @returns true if progress bar is visible
*/
async isLoading(): Promise<boolean> {
return this.progressBar.isVisible();
}
/**
* Assert that we are on the login page.
*/
async assertOnLoginPage(): Promise<void> {
await expect(this.page).toHaveURL(/.*login/);
await expect(this.loginButton).toBeVisible();
}
/**
* Assert successful login by checking we're no longer on login page.
*/
async assertLoginSuccess(): Promise<void> {
await expect(this.page).not.toHaveURL(/.*login/, { timeout: 30000 });
}
/**
* Assert that a validation error is displayed.
*/
async assertValidationError(): Promise<void> {
await expect(this.errorMessages.first()).toBeVisible();
}
}