Skip to content

Commit fe48b61

Browse files
committed
Add Playwright E2E tests for Client module with ClientPage page object
1 parent db8b4c5 commit fe48b61

File tree

2 files changed

+295
-0
lines changed

2 files changed

+295
-0
lines changed

playwright/pages/client.page.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import { Page, Locator, expect } from '@playwright/test';
10+
import { BasePage } from './BasePage';
11+
12+
/**
13+
* ClientPage - Page Object for the Mifos X Clients module.
14+
*
15+
* Encapsulates all client-related interactions and element locators.
16+
* Extends BasePage for common functionality.
17+
*
18+
* Locator Strategy:
19+
* - Uses formcontrolname attributes for form inputs
20+
* - Uses getByRole for buttons
21+
* - Uses CSS selectors for table elements
22+
*/
23+
export class ClientPage extends BasePage {
24+
// The URL path for the clients list page.
25+
readonly url = '/#/clients';
26+
27+
// Get the search input on the clients list page.
28+
get searchInput(): Locator {
29+
return this.page.locator('mat-form-field.search-box input');
30+
}
31+
32+
// Get the "Create Client" button.
33+
get createClientButton(): Locator {
34+
return this.page.getByRole('button', { name: /Create Client/i });
35+
}
36+
37+
// Get the "Import Client" button.
38+
get importClientButton(): Locator {
39+
return this.page.getByRole('button', { name: /Import Client/i });
40+
}
41+
42+
// Get the client list table element.
43+
get clientTable(): Locator {
44+
return this.page.locator('table.bordered-table');
45+
}
46+
47+
// Get all rows in the client list table.
48+
get clientRows(): Locator {
49+
return this.page.locator('table.bordered-table tbody tr');
50+
}
51+
52+
// Get the first client name cell (carries the routerLink).
53+
get firstClientNameCell(): Locator {
54+
return this.page.locator('table.bordered-table tbody tr td').first();
55+
}
56+
57+
// Get the "No client was found" alert message.
58+
get noClientFoundMessage(): Locator {
59+
return this.page.locator('.alert .message');
60+
}
61+
62+
// Get the paginator.
63+
get paginator(): Locator {
64+
return this.page.locator('mat-paginator');
65+
}
66+
67+
// Get the Office select dropdown.
68+
get officeSelect(): Locator {
69+
return this.page.locator('mat-select[formcontrolname="officeId"]');
70+
}
71+
72+
// Get the Legal Form select dropdown.
73+
get legalFormSelect(): Locator {
74+
return this.page.locator('mat-select[formcontrolname="legalFormId"]');
75+
}
76+
77+
// Get the First Name input field.
78+
get firstNameInput(): Locator {
79+
return this.page.locator('input[formcontrolname="firstname"]');
80+
}
81+
82+
// Get the Middle Name input field.
83+
get middleNameInput(): Locator {
84+
return this.page.locator('input[formcontrolname="middlename"]');
85+
}
86+
87+
// Get the Last Name input field.
88+
get lastNameInput(): Locator {
89+
return this.page.locator('input[formcontrolname="lastname"]');
90+
}
91+
92+
// Get the External ID input field.
93+
get externalIdInput(): Locator {
94+
return this.page.locator('input[formcontrolname="externalId"]');
95+
}
96+
97+
// Get the Submitted On date input field.
98+
get submittedOnDateInput(): Locator {
99+
return this.page.locator('input[formcontrolname="submittedOnDate"]');
100+
}
101+
102+
// Get the "Performance History" heading on the client detail page.
103+
get performanceHistoryHeading(): Locator {
104+
return this.page.getByRole('heading', { name: /Performance History/i });
105+
}
106+
107+
// Get the "Loan Accounts" heading on the client detail page.
108+
get loanAccountsHeading(): Locator {
109+
return this.page.getByRole('heading', { name: /Loan Accounts/i });
110+
}
111+
112+
// Get the "Saving Accounts" heading on the client detail page.
113+
get savingAccountsHeading(): Locator {
114+
return this.page.getByRole('heading', { name: /Saving Accounts/i });
115+
}
116+
117+
// Navigate to the Clients list page.
118+
async navigateToClients(): Promise<void> {
119+
await this.page.goto(this.url, {
120+
waitUntil: 'load',
121+
timeout: 60000
122+
});
123+
await this.waitForLoad();
124+
}
125+
126+
/**
127+
* Wait for the clients page to be fully loaded.
128+
* Overrides BasePage to wait for page-specific elements.
129+
*/
130+
async waitForLoad(): Promise<void> {
131+
await this.page.waitForLoadState('domcontentloaded');
132+
try {
133+
await this.page.waitForLoadState('networkidle', { timeout: 15000 });
134+
} catch (e) {
135+
console.log('Network idle timeout on clients page - proceeding anyway');
136+
}
137+
}
138+
139+
/**
140+
* Search for a client by name using the search input.
141+
* @param name - The client name to search for
142+
*/
143+
async searchClient(name: string): Promise<void> {
144+
await this.searchInput.click();
145+
await this.searchInput.fill(name);
146+
await this.searchInput.press('Enter');
147+
try {
148+
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
149+
} catch (e) {
150+
console.log('Network idle timeout after search - proceeding anyway');
151+
}
152+
}
153+
154+
/**
155+
* Click the "Create Client" button and wait for navigation to the form.
156+
*/
157+
async openCreateClientForm(): Promise<void> {
158+
await this.createClientButton.click();
159+
await this.page.waitForURL(/.*clients\/create.*/, {
160+
timeout: 30000,
161+
waitUntil: 'networkidle'
162+
});
163+
}
164+
165+
/**
166+
* Fill the client creation form with first name and last name.
167+
*
168+
* @param firstName - The first name to enter
169+
* @param lastName - The last name to enter
170+
*/
171+
async fillClientForm(firstName: string, lastName: string): Promise<void> {
172+
await this.firstNameInput.waitFor({ state: 'visible', timeout: 10000 });
173+
await this.firstNameInput.clear();
174+
await this.firstNameInput.fill(firstName);
175+
176+
await this.lastNameInput.clear();
177+
await this.lastNameInput.fill(lastName);
178+
}
179+
180+
/**
181+
* Click the "Next" button on the general step of the client creation stepper.
182+
*/
183+
async submitClientForm(): Promise<void> {
184+
const nextButton = this.page.getByRole('button', { name: /Next/i });
185+
await nextButton.click();
186+
}
187+
188+
/**
189+
* Click the first client name cell to open the client detail page.
190+
* The routerLink is on the name td, not the tr row.
191+
*/
192+
async openClientProfile(): Promise<void> {
193+
await this.firstClientNameCell.waitFor({ state: 'visible', timeout: 15000 });
194+
await this.firstClientNameCell.click();
195+
196+
// Avoid networkidle — the detail page fires many concurrent API calls
197+
await this.page.waitForURL(/.*clients\/\d+\/general.*/, {
198+
timeout: 30000
199+
});
200+
await this.performanceHistoryHeading.waitFor({
201+
state: 'visible',
202+
timeout: 30000
203+
});
204+
}
205+
206+
/**
207+
* Assert that the client list page is visible.
208+
*/
209+
async assertClientListVisible(): Promise<void> {
210+
await expect(this.searchInput).toBeVisible({ timeout: 15000 });
211+
}
212+
213+
/**
214+
* Assert that the create client form is visible.
215+
*/
216+
async assertCreateFormVisible(): Promise<void> {
217+
await expect(this.officeSelect).toBeVisible({ timeout: 15000 });
218+
await expect(this.legalFormSelect).toBeVisible({ timeout: 15000 });
219+
}
220+
221+
/**
222+
* Assert that the client detail page is loaded.
223+
*/
224+
async assertClientDetailVisible(): Promise<void> {
225+
await expect(this.performanceHistoryHeading).toBeVisible({ timeout: 15000 });
226+
await expect(this.page).toHaveURL(/.*clients\/\d+\/general.*/);
227+
}
228+
}

playwright/tests/client.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import { test, expect } from '@playwright/test';
10+
import { LoginPage } from '../pages/login.page';
11+
import { ClientPage } from '../pages/client.page';
12+
13+
/**
14+
* Client Module E2E Tests
15+
*
16+
* Validates the core client management functionality of the Mifos X Web App.
17+
*
18+
* Prerequisites:
19+
* - Angular dev server running on http://localhost:4200
20+
* - Fineract backend accessible (via proxy to https://localhost:8443)
21+
*
22+
* Test Data:
23+
* - Valid credentials: mifos / password
24+
*/
25+
26+
// Skip in CI - requires Fineract backend
27+
test.skip(!!process.env.CI, 'Requires Fineract backend');
28+
29+
test.describe('Client Module', () => {
30+
let loginPage: LoginPage;
31+
let clientPage: ClientPage;
32+
33+
test.beforeEach(async ({ page }) => {
34+
loginPage = new LoginPage(page);
35+
clientPage = new ClientPage(page);
36+
37+
await loginPage.navigate();
38+
await loginPage.loginAndWaitForDashboard('mifos', 'password');
39+
});
40+
41+
test('should display the client list', async () => {
42+
await clientPage.navigateToClients();
43+
44+
await clientPage.assertClientListVisible();
45+
await expect(clientPage.createClientButton).toBeVisible();
46+
});
47+
48+
test('should open the create client form', async ({ page }) => {
49+
await clientPage.navigateToClients();
50+
await clientPage.openCreateClientForm();
51+
52+
await clientPage.assertCreateFormVisible();
53+
await expect(page).toHaveURL(/.*clients\/create.*/);
54+
});
55+
56+
test('should open a client profile', async ({ page }) => {
57+
await clientPage.navigateToClients();
58+
59+
// Trigger search to populate the table
60+
await clientPage.searchClient('');
61+
await page.waitForTimeout(3000);
62+
63+
await expect(clientPage.clientRows.first()).toBeVisible({ timeout: 30000 });
64+
await clientPage.openClientProfile();
65+
await clientPage.assertClientDetailVisible();
66+
});
67+
});

0 commit comments

Comments
 (0)