Skip to content
Open
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ docs/

# Docker data (should never be committed)
data/

# Playwright artifacts
playwright-report/
test-results/
blob-report/
67 changes: 65 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"scripts": {
"dev": "vite",
"build": "npm run generate-sdk && tsc -b && vite build",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:ui": "playwright test --ui",
"test:e2e:install": "playwright install chromium",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
Expand Down Expand Up @@ -60,6 +64,7 @@
"tailwindcss": "^4.1.7"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@eslint/js": "^9.27.0",
"@types/node": "^22.15.21",
"@types/react": "^19.1.2",
Expand All @@ -77,6 +82,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.3.2",
"prettier": "^3.8.1",
"playwright": "^1.58.2",
"tw-animate-css": "^1.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.32.1",
Expand Down
61 changes: 61 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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 { defineConfig, devices } from '@playwright/test'

const env =
(globalThis as { process?: { env?: Record<string, string | undefined> } })
.process?.env ?? {}
const isCI = !!env.CI

export default defineConfig({
testDir: './playwright/tests',
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? 1 : undefined,
reporter: [
['html', { outputFolder: 'playwright-report', open: 'never' }],
['list'],
],
use: {
baseURL: 'http://localhost:5173',
ignoreHTTPSErrors: true,
trace: 'retain-on-failure',
video: 'retain-on-failure',
screenshot: 'only-on-failure',
navigationTimeout: 120000,
actionTimeout: 30000,
extraHTTPHeaders: {
Accept: 'application/json',
},
},
timeout: isCI ? 180000 : 120000,
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
launchOptions: {
args: ['--ignore-certificate-errors'],
},
},
},
],
webServer: {
command: isCI
? 'npm run build && npx vite preview --port 5173'
: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !isCI,
timeout: 180000,
...(isCI && {
stdout: 'pipe',
stderr: 'pipe',
}),
},
})
44 changes: 44 additions & 0 deletions playwright/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Playwright E2E Foundation (React / Vite)

This folder provides a **framework-agnostic E2E foundation** based on Page Object Model (POM).

## Core Principle

Tests (`*.spec.ts`) stay framework-agnostic.
Only the `SELECTORS` block inside each page object changes between Angular and React.

## Structure

- `types/selectors.ts`: Selector map interfaces (contracts)
- `pages/BasePage.ts`: Shared abstract base page
- `pages/login.page.ts`: React login page object (selectors swapped)
- `tests/login.spec.ts`: Framework-agnostic login tests

## SelectorMap Pattern

Each page object implements a typed selector contract. Example:

- `LoginSelectors.usernameInput`
- `LoginSelectors.passwordInput`
- `LoginSelectors.loginButton`
- `LoginSelectors.errorMessage`

TypeScript enforces selector completeness and keeps framework-specific details localized.

## Run E2E

- Install browser: `npm run test:e2e:install`
- Headless: `npm run test:e2e`
- Headed: `npm run test:e2e:headed`
- UI mode: `npm run test:e2e:ui`

## Backend-Dependent Tests

Set `SKIP_BACKEND_TESTS=true` to skip tests that require a running Fineract backend.

## Porting Workflow (Angular -> React)

1. Copy `*.spec.ts` (no changes)
2. Copy page object class
3. Swap only the `SELECTORS` block
4. Keep all public methods unchanged
144 changes: 144 additions & 0 deletions playwright/pages/BasePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* 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 { 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()
}
}
Loading
Loading