Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ src/environments/.env.ts
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
4 changes: 0 additions & 4 deletions config/e2e/nginx-e2e.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ server {
location /fineract-provider/ {
proxy_pass ${E2E_PROXY_TARGET}/fineract-provider/;
proxy_ssl_verify off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_read_timeout 120s;
}
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ services:
E2E_PROXY_TARGET: https://fineract:8443

# Angular app configuration (resolved by host browser via proxy)
FINERACT_API_URLS: http://localhost:4200
FINERACT_API_URL: http://localhost:4200
FINERACT_API_URLS: ''
FINERACT_API_URL: ''

FINERACT_API_PROVIDER: /fineract-provider/api
FINERACT_API_VERSION: /v1
Expand Down
14 changes: 11 additions & 3 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default defineConfig({

// Global test settings
use: {
// Base URL for the Angular app (aligned with global-setup.ts)
// Base URL for the Angular app (aligned with global-setup.ts and configurable via env for CI)
baseURL: process.env.E2E_BASE_URL || 'http://localhost:4200',

// Handle self-signed certificates from Fineract backend
Expand Down Expand Up @@ -74,17 +74,25 @@ export default defineConfig({
// Global test timeout (per test)
timeout: process.env.CI ? 180000 : 120000,

// Configure projects for different browsers
// Configure projects for authentication setup and browser testing
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
testDir: './playwright',
retries: process.env.CI ? 2 : 0
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
// Launch options for handling SSL in headed mode
launchOptions: {
args: ['--ignore-certificate-errors']
}
}
},
dependencies: ['setup']
}
],

Expand Down
53 changes: 53 additions & 0 deletions playwright/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { test as setup, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
import { LoginPage } from './pages/login.page';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page, browser }) => {
const authPath = path.resolve(authFile);
const authDir = path.dirname(authPath);
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true });
}
if (fs.existsSync(authPath)) {
fs.unlinkSync(authPath);
}

const username = process.env.E2E_USERNAME || 'mifos';
const password = process.env.E2E_PASSWORD || 'password';

const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.loginAndWaitForDashboard(username, password);

console.log('Auth setup: copying mifosXCredentials from sessionStorage → localStorage');
const credsCopied = await page.evaluate(() => {
const creds = sessionStorage.getItem('mifosXCredentials');
if (!creds) return false;
localStorage.setItem('mifosXCredentials', creds);
return true;
});

if (!credsCopied) {
throw new Error('CRITICAL: mifosXCredentials not found in sessionStorage. ' + 'Did the auth storage key change?');
}

await page.context().storageState({ path: authFile });
console.log('Auth setup: storageState saved to', authFile);

const verifyContext = await browser.newContext({ storageState: authFile });
const verifyPage = await verifyContext.newPage();
await verifyPage.goto('/#/');
await expect(verifyPage).not.toHaveURL(/.*login.*/, { timeout: 30000 });
await verifyContext.close();
console.log('Auth setup: storageState verification passed ✓');
});
29 changes: 29 additions & 0 deletions playwright/tests/authenticated-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { test, expect } from '@playwright/test';
test.describe('Authenticated Smoke Tests', () => {
test.beforeEach(async ({ page }) => {
// Playwright storageState only restores localStorage and cookies.
// The Fineract web app expects credentials in sessionStorage.
// We injected credentials into localStorage during auth.setup.ts,
// so we must restore them to sessionStorage before the page runs.
await page.addInitScript(() => {
const creds = localStorage.getItem('mifosXCredentials');
if (creds) {
sessionStorage.setItem('mifosXCredentials', creds);
}
});
});

test('should load dashboard without login redirect', async ({ page }) => {
await page.goto('/#/');

await expect(page).not.toHaveURL(/.*login.*/, { timeout: 30000 });
await expect(page.locator('mat-toolbar')).toBeVisible({ timeout: 10000 });
});
});
6 changes: 4 additions & 2 deletions playwright/tests/login-responsive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
Expand All @@ -8,6 +8,8 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from 'playwright/pages/login.page';

test.use({ storageState: { cookies: [], origins: [] } });

/**
* Login Responsive Layout Tests
*
Expand Down Expand Up @@ -209,7 +211,7 @@ test.describe('Login Page - Responsive Layout', () => {

for (const viewport of viewports) {
await page.setViewportSize(viewport);
await page.goto('http://localhost:4200/login');
await page.goto('/#/login');
await page.waitForLoadState('networkidle');

// Check no horizontal scroll
Expand Down
13 changes: 3 additions & 10 deletions playwright/tests/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test.use({ storageState: { cookies: [], origins: [] } });

/**
* Login Smoke Tests
Expand Down Expand Up @@ -68,8 +69,6 @@ test.describe('Login Page', () => {
});

test('should successfully login with valid credentials', async () => {
test.skip(!!process.env.CI, 'Production build auth interceptor skips tenant header for absolute URLs');

// Perform login with valid credentials
// This uses the login() method which follows the exact codegen sequence
await loginPage.loginAndWaitForDashboard('mifos', 'password');
Expand All @@ -82,12 +81,8 @@ test.describe('Login 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.*/);
// Should remain on login page after failed attempt
await expect(page).toHaveURL(/.*login.*/, { timeout: 10000 });

// Verify we're still on the login page by checking form elements are visible
await expect(loginPage.usernameInput).toBeVisible();
Expand All @@ -99,8 +94,6 @@ test.describe('Login Page', () => {
* This is the baseline test generated from codegen.
*/
test('codegen baseline: login with mifos credentials', async () => {
test.skip(!!process.env.CI, 'Production build auth interceptor skips tenant header for absolute URLs');

// This test uses the exact codegen interaction sequence
await loginPage.login('mifos', 'password');

Expand Down
Loading