feat: establish Playwright E2E testing infrastructure with configuration and initial tests
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user