Skip to content

Commit 00c4999

Browse files
committed
Add Playwright E2E tests for Client module
1 parent db8b4c5 commit 00c4999

File tree

2 files changed

+302
-0
lines changed

2 files changed

+302
-0
lines changed

playwright/pages/client.page.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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 { 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 (the displayName column carries the routerLink).
53+
get firstClientNameCell(): Locator {
54+
return this.page.locator('table.bordered-table tbody tr td.mat-column-displayName').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+
// Verify the UI is ready by checking for the search input
138+
await this.searchInput.waitFor({ state: 'visible', timeout: 15000 });
139+
}
140+
141+
/**
142+
* Search for a client by name using the search input.
143+
* @param name - The client name to search for
144+
*/
145+
async searchClient(name: string): Promise<void> {
146+
await this.searchInput.click();
147+
await this.searchInput.fill(name);
148+
await this.searchInput.press('Enter');
149+
try {
150+
await this.page.waitForLoadState('networkidle', { timeout: 10000 });
151+
} catch (e) {
152+
console.log('Network idle timeout after search - proceeding anyway');
153+
}
154+
// Wait for observable results: either rows appear or "no client found" message
155+
await Promise.race([
156+
this.clientRows.first().waitFor({ state: 'visible', timeout: 15000 }),
157+
this.noClientFoundMessage.waitFor({ state: 'visible', timeout: 15000 })
158+
]);
159+
}
160+
161+
/**
162+
* Click the "Create Client" button and wait for navigation to the form.
163+
*/
164+
async openCreateClientForm(): Promise<void> {
165+
await this.createClientButton.click();
166+
await this.page.waitForURL(/.*clients\/create.*/, {
167+
timeout: 30000,
168+
waitUntil: 'networkidle'
169+
});
170+
}
171+
172+
/**
173+
* Fill the client creation form with first name and last name.
174+
*
175+
* @param firstName - The first name to enter
176+
* @param lastName - The last name to enter
177+
*/
178+
async fillClientForm(firstName: string, lastName: string): Promise<void> {
179+
await this.firstNameInput.waitFor({ state: 'visible', timeout: 10000 });
180+
await this.firstNameInput.clear();
181+
await this.firstNameInput.fill(firstName);
182+
183+
await this.lastNameInput.waitFor({ state: 'visible', timeout: 10000 });
184+
await this.lastNameInput.clear();
185+
await this.lastNameInput.fill(lastName);
186+
}
187+
188+
/**
189+
* Click the "Next" button on the general step of the client creation stepper.
190+
*/
191+
async submitClientForm(): Promise<void> {
192+
const nextButton = this.page.getByRole('button', { name: /Next/i });
193+
await nextButton.click();
194+
}
195+
196+
/**
197+
* Click the first client name cell to open the client detail page.
198+
* The routerLink is on the name td, not the tr row.
199+
*/
200+
async openClientProfile(): Promise<void> {
201+
await this.firstClientNameCell.waitFor({ state: 'visible', timeout: 15000 });
202+
await this.firstClientNameCell.click();
203+
204+
// Avoid networkidle — the detail page fires many concurrent API calls
205+
await this.page.waitForURL(/.*clients\/\d+\/general.*/, {
206+
timeout: 30000
207+
});
208+
await this.performanceHistoryHeading.waitFor({
209+
state: 'visible',
210+
timeout: 30000
211+
});
212+
}
213+
214+
/**
215+
* Assert that the client list page is visible.
216+
*/
217+
async assertClientListVisible(): Promise<void> {
218+
await expect(this.searchInput).toBeVisible({ timeout: 15000 });
219+
}
220+
221+
/**
222+
* Assert that the create client form is visible.
223+
*/
224+
async assertCreateFormVisible(): Promise<void> {
225+
await expect(this.officeSelect).toBeVisible({ timeout: 15000 });
226+
await expect(this.legalFormSelect).toBeVisible({ timeout: 15000 });
227+
}
228+
229+
/**
230+
* Assert that the client detail page is loaded.
231+
*/
232+
async assertClientDetailVisible(): Promise<void> {
233+
await expect(this.performanceHistoryHeading).toBeVisible({ timeout: 15000 });
234+
await expect(this.page).toHaveURL(/.*clients\/\d+\/general.*/);
235+
}
236+
}

playwright/tests/client.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 () => {
57+
await clientPage.navigateToClients();
58+
59+
// Trigger search to populate the table
60+
await clientPage.searchClient('');
61+
62+
await expect(clientPage.clientRows.first()).toBeVisible({ timeout: 30000 });
63+
await clientPage.openClientProfile();
64+
await clientPage.assertClientDetailVisible();
65+
});
66+
});

0 commit comments

Comments
 (0)