feat: establish Playwright E2E testing infrastructure with configuration and initial tests
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
name: Playwright E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev, dev-angular-19]
|
||||
pull_request:
|
||||
branches: [main, dev, dev-angular-19]
|
||||
|
||||
jobs:
|
||||
playwright:
|
||||
name: Run Playwright Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 22x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.12.0'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Build application
|
||||
run: npm run build
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test --reporter=html,github
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
@@ -63,3 +63,9 @@ cypress/videos/
|
||||
|
||||
src/assets/env.js
|
||||
src/environments/.env.ts
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
+5
-1
@@ -9,7 +9,11 @@ module.exports = [
|
||||
{
|
||||
ignores: [
|
||||
'projects/**/*',
|
||||
'dist/**/*'
|
||||
'dist/**/*',
|
||||
'.angular/**/*',
|
||||
'node_modules/**/*',
|
||||
'playwright-report/**/*',
|
||||
'test-results/**/*'
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
Generated
+1937
-2354
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -28,7 +28,11 @@
|
||||
"env": "node version.js",
|
||||
"postinstall": "node version.js",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run"
|
||||
"cypress:run": "cypress run",
|
||||
"playwright": "playwright test",
|
||||
"playwright:ui": "playwright test --ui",
|
||||
"playwright:headed": "playwright test --headed",
|
||||
"playwright:debug": "playwright test --debug"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.15",
|
||||
@@ -81,6 +85,7 @@
|
||||
"@angular/compiler-cli": "20.3.15",
|
||||
"@angular/language-service": "20.3.15",
|
||||
"@cypress/schematic": "^2.5.2",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@types/d3-dispatch": "^3.0.1",
|
||||
"@types/d3-drag": "^3.0.1",
|
||||
"@types/d3-scale": "^3.2.2",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Playwright configuration for Mifos X Web App E2E tests.
|
||||
*
|
||||
* Optimized for:
|
||||
* - Self-signed SSL certificate handling (local Fineract backend)
|
||||
* - CI/CD efficiency (artifacts only on failure)
|
||||
* - Debugging support (traces, videos, screenshots)
|
||||
*
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig({
|
||||
// Test directory
|
||||
testDir: './playwright/tests',
|
||||
|
||||
// Exclude Angular component tests
|
||||
testIgnore: 'src/**',
|
||||
|
||||
// Fail the build if test.only is left in source (CI safety)
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
// Retry failed tests in CI
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
// Limit parallel workers in CI
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
||||
// Reporter configuration
|
||||
reporter: [
|
||||
[
|
||||
'html',
|
||||
{ outputFolder: 'playwright-report', open: 'never' }],
|
||||
['list']
|
||||
],
|
||||
|
||||
// Global test settings
|
||||
use: {
|
||||
// Base URL for the Angular app
|
||||
baseURL: 'http://localhost:4200',
|
||||
|
||||
// Handle self-signed certificates from Fineract backend
|
||||
ignoreHTTPSErrors: true,
|
||||
|
||||
// Collect trace on failure for debugging
|
||||
trace: 'retain-on-failure',
|
||||
|
||||
// Record video on failure only (storage efficient)
|
||||
video: 'retain-on-failure',
|
||||
|
||||
// Screenshot on failure only
|
||||
screenshot: 'only-on-failure',
|
||||
|
||||
// Default navigation timeout (increased for CI and financial app)
|
||||
navigationTimeout: 60000,
|
||||
|
||||
// Default action timeout
|
||||
actionTimeout: 20000
|
||||
},
|
||||
|
||||
// Global test timeout (per test)
|
||||
timeout: process.env.CI ? 90000 : 60000,
|
||||
|
||||
// Configure projects for different browsers
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
// Launch options for handling SSL in headed mode
|
||||
launchOptions: {
|
||||
args: ['--ignore-certificate-errors']
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Web server configuration
|
||||
// In CI: builds production bundle (without base-href), then serves it with http-server
|
||||
// Locally: reuses existing ng serve if running
|
||||
webServer: {
|
||||
command: process.env.CI
|
||||
? 'npm run build && npx http-server ./dist/web-app/browser -p 4200 --silent'
|
||||
: 'npm run start',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 180000,
|
||||
// Add retry logic for server startup
|
||||
...(process.env.CI && {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe'
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
# Playwright E2E Testing
|
||||
|
||||
Production-grade End-to-End testing infrastructure for Mifos X Web App using [Playwright](https://playwright.dev/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running the tests, ensure the local environment is running:
|
||||
|
||||
1. **Backend:** Ensure Fineract is running on `https://localhost:8443`
|
||||
2. **Frontend:** Serve the Angular app:
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run playwright
|
||||
|
||||
# Run with UI mode (recommended for debugging)
|
||||
npm run playwright:ui
|
||||
|
||||
# Run with visible browser
|
||||
npm run playwright:headed
|
||||
|
||||
# Debug mode (step through tests)
|
||||
npm run playwright:debug
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
This framework follows the **Page Object Model (POM)** pattern:
|
||||
|
||||
```
|
||||
playwright/
|
||||
├── pages/
|
||||
│ ├── BasePage.ts # Abstract base class with common utilities
|
||||
│ └── LoginPage.ts # Login page object
|
||||
└── tests/
|
||||
└── login.spec.ts # Login smoke tests
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
| Principle | Implementation |
|
||||
| -------------------------- | --------------------------------------------- |
|
||||
| **Separation of Concerns** | Locators in Page Objects, assertions in Tests |
|
||||
| **DRY** | Common actions in `BasePage` |
|
||||
| **Maintainability** | Centralized locators per page |
|
||||
| **CI/CD Optimization** | Artifacts only on failure |
|
||||
|
||||
## Configuration
|
||||
|
||||
### SSL Certificate Handling
|
||||
|
||||
The local Fineract backend uses a self-signed certificate. This is handled automatically:
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
ignoreHTTPSErrors: true;
|
||||
```
|
||||
|
||||
### TypeScript Isolation
|
||||
|
||||
Uses a dedicated `tsconfig.playwright.json` to avoid conflicts with Jest:
|
||||
|
||||
```json
|
||||
{
|
||||
"types": ["node"] // Excludes Jest types
|
||||
}
|
||||
```
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### 1. Create a Page Object
|
||||
|
||||
```typescript
|
||||
// playwright/pages/DashboardPage.ts
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class DashboardPage extends BasePage {
|
||||
readonly url = '/#/';
|
||||
|
||||
get welcomeMessage() {
|
||||
return this.page.locator('.welcome-message');
|
||||
}
|
||||
|
||||
async assertOnDashboard(): Promise<void> {
|
||||
await expect(this.page).toHaveURL(/.*\//);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Write the Test
|
||||
|
||||
```typescript
|
||||
// playwright/tests/dashboard.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../pages/LoginPage';
|
||||
import { DashboardPage } from '../pages/DashboardPage';
|
||||
|
||||
test('should display dashboard after login', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
|
||||
await loginPage.navigate();
|
||||
await loginPage.loginAndWaitForDashboard('mifos', 'password');
|
||||
await dashboardPage.assertOnDashboard();
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
Tests run automatically on push/PR via GitHub Actions:
|
||||
|
||||
```bash
|
||||
.github/workflows/playwright.yml
|
||||
```
|
||||
|
||||
### Viewing Test Reports
|
||||
|
||||
After CI runs, download the `playwright-report` artifact from the Actions tab.
|
||||
|
||||
## Test Credentials
|
||||
|
||||
| Username | Password | Environment |
|
||||
| -------- | ---------- | ------------- |
|
||||
| `mifos` | `password` | Local/Sandbox |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tests fail with SSL errors
|
||||
|
||||
Ensure `ignoreHTTPSErrors: true` is set in `playwright.config.ts`.
|
||||
|
||||
### Tests timeout on CI
|
||||
|
||||
Increase the timeout in `playwright.config.ts`:
|
||||
|
||||
```typescript
|
||||
navigationTimeout: 60000,
|
||||
```
|
||||
|
||||
### TypeScript conflicts with Jest
|
||||
|
||||
Ensure tests are only in `playwright/` directory and `tsconfig.playwright.json` excludes other directories.
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../pages/login.page';
|
||||
|
||||
/**
|
||||
* Login Smoke Tests
|
||||
*
|
||||
* This test suite validates the core login functionality of the Mifos X Web App.
|
||||
* These tests use a strict Page Object Model with selectors from Playwright codegen.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Angular dev server running on http://localhost:4200
|
||||
* - Fineract backend accessible (via proxy to https://localhost:8443)
|
||||
*
|
||||
* Test Data:
|
||||
* - Valid credentials: mifos / password
|
||||
*/
|
||||
test.describe('Login Page', () => {
|
||||
let loginPage: LoginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.navigate();
|
||||
});
|
||||
|
||||
test('should display the login form', async () => {
|
||||
// Verify the login form is visible
|
||||
await loginPage.assertOnLoginPage();
|
||||
|
||||
// Verify form elements are present (using codegen selectors)
|
||||
await expect(loginPage.usernameInput).toBeVisible();
|
||||
await expect(loginPage.passwordInput).toBeVisible();
|
||||
await expect(loginPage.loginButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should disable login button when form is empty', async () => {
|
||||
// Login button should be disabled with empty fields
|
||||
await expect(loginPage.loginButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should show validation errors for required fields', async ({ page }) => {
|
||||
// Focus and blur username field to trigger validation
|
||||
await loginPage.usernameInput.focus();
|
||||
await loginPage.passwordInput.focus();
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Check for validation error messages
|
||||
const hasErrors = await loginPage.getErrorCount();
|
||||
expect(hasErrors).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should enable login button when credentials are entered', async () => {
|
||||
// Fill in credentials using the wrapper clicks (as per codegen)
|
||||
await loginPage.usernameDivWrapper.click();
|
||||
await loginPage.usernameInput.fill('mifos');
|
||||
await loginPage.passwordDivWrapper.click();
|
||||
await loginPage.passwordInput.fill('password');
|
||||
|
||||
// Login button should become enabled
|
||||
await expect(loginPage.loginButton).toBeEnabled();
|
||||
});
|
||||
|
||||
// Skip in CI - requires Fineract backend
|
||||
test.skip(!!process.env.CI, 'Requires Fineract backend');
|
||||
test('should successfully login with valid credentials', async () => {
|
||||
// Perform login with valid credentials
|
||||
// This uses the login() method which follows the exact codegen sequence
|
||||
await loginPage.loginAndWaitForDashboard('mifos', 'password');
|
||||
|
||||
// Assert we're no longer on the login page
|
||||
await loginPage.assertLoginSuccess();
|
||||
});
|
||||
|
||||
test('should handle invalid credentials gracefully', async ({ page }) => {
|
||||
// Attempt login with wrong password
|
||||
await loginPage.login('mifos', 'wrongpassword');
|
||||
|
||||
// Wait for the login attempt to process and any error notification to appear
|
||||
// The app shows a snackbar notification for authentication errors
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Should remain on login page after failed attempt (URL still contains /login)
|
||||
await expect(page).toHaveURL(/.*login.*/);
|
||||
|
||||
// Verify we're still on the login page by checking form elements are visible
|
||||
await expect(loginPage.usernameInput).toBeVisible();
|
||||
await expect(loginPage.passwordInput).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* Simple test that exactly mirrors the codegen script.
|
||||
* This is the baseline test generated from codegen.
|
||||
*/
|
||||
// Skip in CI - requires Fineract backend
|
||||
test.skip(!!process.env.CI, 'Requires Fineract backend');
|
||||
test('codegen baseline: login with mifos credentials', async () => {
|
||||
// This test uses the exact codegen interaction sequence
|
||||
await loginPage.login('mifos', 'password');
|
||||
|
||||
// Verify successful login
|
||||
await loginPage.assertLoginSuccess();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Accessibility smoke test
|
||||
*/
|
||||
test.describe('Login Page Accessibility', () => {
|
||||
let loginPage: LoginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.navigate();
|
||||
});
|
||||
|
||||
test('should have proper page title', async ({ page }) => {
|
||||
const title = await page.title();
|
||||
expect(title).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should support keyboard navigation', async ({ page }) => {
|
||||
// Tab through form elements
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Form should be navigable via keyboard
|
||||
await expect(loginPage.loginButton).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@
|
||||
window['env']['fineractPlatformTenantIds'] = '$FINERACT_PLATFORM_TENANTS_IDENTIFIER';
|
||||
|
||||
window['env']['tenantLogoUrl'] = '$TENANT_LOGO_URL';
|
||||
|
||||
|
||||
// Language Environment variables
|
||||
window['env']['defaultLanguage'] = '$MIFOS_DEFAULT_LANGUAGE';
|
||||
window['env']['supportedLanguages'] = '$MIFOS_SUPPORTED_LANGUAGES';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"outDir": "./dist-playwright",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"playwright/**/*.ts",
|
||||
"playwright.config.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"src",
|
||||
"e2e",
|
||||
"cypress"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user