Skip to content

Commit 2285ae3

Browse files
committed
WEB-1039: add Playwright client lifecycle happy-path E2E specs
Adds 5 happy-path specs under playwright/tests/clients/lifecycle/: - activate.spec.ts — Pending → Active - reject.spec.ts — Pending → Rejected - undo-rejection.spec.ts — Rejected → Pending (Undo Rejection) - withdraw.spec.ts — Pending → Withdrawn - reactivate-after-close.spec.ts — Closed → Pending (Reactivate) Each spec: API arrange → UI act → snackbar assert + GET /clients/{id} status assert + timeline entry check. All 5 pass locally.
1 parent c792443 commit 2285ae3

5 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright since 2026 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 '../../../fixtures/test-fixtures';
10+
import { createTestClient } from '../../../factories/client.factory';
11+
import { ClientViewPage } from '../../../pages/client-view.page';
12+
import { ActivateClientPage } from '../../../pages/client-actions/activate-client.page';
13+
14+
/**
15+
* WA-3.3 client lifecycle happy path: Pending → Active.
16+
*
17+
* Pattern:
18+
* 1. Factory creates the predecessor-state client (pending).
19+
* 2. UI drives the transition via `chooseAction('Activate')`.
20+
* 3. Assert snackbar copy + `getClient().status.value` +
21+
* timeline entry (`activatedOnDate`).
22+
*/
23+
const SUBMITTED_ON_DATE = '01 January 2024';
24+
const ACTIVATION_DATE = '02 January 2024';
25+
26+
test.describe('Client lifecycle · Activate (Pending → Active)', () => {
27+
test.beforeEach(async ({ page }) => {
28+
await page.addInitScript(() => {
29+
const creds = localStorage.getItem('mifosXCredentials');
30+
if (creds) {
31+
sessionStorage.setItem('mifosXCredentials', creds);
32+
}
33+
});
34+
});
35+
36+
test('activates a pending client from the client actions flow', async ({
37+
page,
38+
fineractApi,
39+
apiSetup,
40+
cleanupGuard
41+
}) => {
42+
const client = await createTestClient(apiSetup, cleanupGuard, {
43+
submittedOnDate: SUBMITTED_ON_DATE
44+
});
45+
46+
const clientViewPage = new ClientViewPage(page, client.resourceId);
47+
const activatePage = new ActivateClientPage(page, client.resourceId);
48+
49+
await clientViewPage.navigate();
50+
await clientViewPage.waitForLoad();
51+
await clientViewPage.chooseAction('Activate');
52+
53+
await activatePage.waitForLoad();
54+
await activatePage.submitActivation({ activationDate: ACTIVATION_DATE });
55+
56+
await clientViewPage.waitForLoad();
57+
await expect(clientViewPage.successSnackbar).toContainText('Client activated successfully.');
58+
59+
const clientDetails = await fineractApi.getClient(client.resourceId);
60+
expect(clientDetails.status?.value).toBe('Active');
61+
expect(clientDetails.timeline?.activatedOnDate).toEqual([
62+
2024,
63+
1,
64+
2
65+
]);
66+
});
67+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright since 2026 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 '../../../fixtures/test-fixtures';
10+
import { ClientViewPage } from '../../../pages/client-view.page';
11+
import { ReactivateClientPage } from '../../../pages/client-actions/reactivate-client.page';
12+
13+
/**
14+
* WA-3.3 client lifecycle happy path: Closed → Pending (Reactivate).
15+
*
16+
* Pattern:
17+
* 1. API creates an active client, then closes it, so the client
18+
* enters the predecessor `Closed` state. This flow deliberately
19+
* bypasses `createTestClient` because active/closed clients
20+
* cannot be hard-deleted by the cleanup guard.
21+
* 2. UI drives the transition via `chooseAction('Reactivate')`.
22+
* 3. Assert snackbar copy + `getClient().status.value` returns to
23+
* `Pending` (Fineract's documented reactivate destination — a
24+
* subsequent Activate is required to reach `Active`) + the
25+
* `closedOnDate` timeline entry is cleared.
26+
*/
27+
const SUBMITTED_ON_DATE = '01 January 2024';
28+
const ACTIVATION_DATE = '02 January 2024';
29+
const CLOSURE_DATE = '03 January 2024';
30+
const REACTIVATION_DATE = '04 January 2024';
31+
const CLOSURE_REASON_NAME = 'E2E Close Client Reason';
32+
33+
test.describe('Client lifecycle · Reactivate (Closed → Pending)', () => {
34+
test.beforeEach(async ({ page }) => {
35+
await page.addInitScript(() => {
36+
const creds = localStorage.getItem('mifosXCredentials');
37+
if (creds) {
38+
sessionStorage.setItem('mifosXCredentials', creds);
39+
}
40+
});
41+
});
42+
43+
test('reactivates a closed client from the client actions flow', async ({ page, fineractApi }) => {
44+
const closureReason = await fineractApi.ensureClientClosureReason(CLOSURE_REASON_NAME);
45+
const officeId = await fineractApi.getFirstOfficeId();
46+
47+
const uniqueSuffix = Date.now();
48+
const createClientResponse = await fineractApi.createActiveClient(officeId, {
49+
firstname: `Reactivate${uniqueSuffix}`,
50+
lastname: 'Client',
51+
submittedOnDate: SUBMITTED_ON_DATE,
52+
activationDate: ACTIVATION_DATE
53+
});
54+
55+
const clientId = createClientResponse.resourceId;
56+
await fineractApi.closeClient(clientId, closureReason.id, CLOSURE_DATE);
57+
58+
const clientViewPage = new ClientViewPage(page, clientId);
59+
const reactivatePage = new ReactivateClientPage(page, clientId);
60+
61+
await clientViewPage.navigate();
62+
await clientViewPage.waitForLoad();
63+
await clientViewPage.chooseAction('Reactivate');
64+
65+
await reactivatePage.waitForLoad();
66+
await reactivatePage.submitReactivation({ reactivationDate: REACTIVATION_DATE });
67+
68+
await clientViewPage.waitForLoad();
69+
await expect(clientViewPage.successSnackbar).toContainText('Client reactivated successfully.');
70+
71+
const clientDetails = await fineractApi.getClient(clientId);
72+
// In Fineract, reactivating a closed client moves it back to
73+
// `Pending` state — a subsequent Activate action is required to
74+
// return to `Active`. The Pending status combined with the cleared
75+
// `closedOnDate` timeline entry is the definitive evidence that
76+
// the reactivate command was recorded server-side.
77+
expect(clientDetails.status?.value).toBe('Pending');
78+
expect(clientDetails.timeline?.closedOnDate).toBeFalsy();
79+
expect(clientDetails.timeline?.submittedOnDate).toEqual([
80+
2024,
81+
1,
82+
1
83+
]);
84+
});
85+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright since 2026 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 '../../../fixtures/test-fixtures';
10+
import { createTestClient } from '../../../factories/client.factory';
11+
import { ClientViewPage } from '../../../pages/client-view.page';
12+
import { RejectClientPage } from '../../../pages/client-actions/reject-client.page';
13+
14+
/**
15+
* WA-3.3 client lifecycle happy path: Pending → Rejected.
16+
*
17+
* Pattern:
18+
* 1. Factory creates a pending client.
19+
* 2. UI drives the transition via `chooseAction('Reject')`.
20+
* 3. Assert snackbar copy + `getClient().status.value` +
21+
* timeline entry (`rejectedOnDate`).
22+
*/
23+
const SUBMITTED_ON_DATE = '01 January 2024';
24+
const REJECTION_DATE = '02 January 2024';
25+
const REJECTION_REASON_NAME = 'E2E Reject Client Reason';
26+
27+
test.describe('Client lifecycle · Reject (Pending → Rejected)', () => {
28+
test.beforeEach(async ({ page }) => {
29+
await page.addInitScript(() => {
30+
const creds = localStorage.getItem('mifosXCredentials');
31+
if (creds) {
32+
sessionStorage.setItem('mifosXCredentials', creds);
33+
}
34+
});
35+
});
36+
37+
test('rejects a pending client from the client actions flow', async ({
38+
page,
39+
fineractApi,
40+
apiSetup,
41+
cleanupGuard
42+
}) => {
43+
await fineractApi.ensureClientRejectionReason(REJECTION_REASON_NAME);
44+
45+
const client = await createTestClient(apiSetup, cleanupGuard, {
46+
submittedOnDate: SUBMITTED_ON_DATE
47+
});
48+
49+
const clientViewPage = new ClientViewPage(page, client.resourceId);
50+
const rejectPage = new RejectClientPage(page, client.resourceId);
51+
52+
await clientViewPage.navigate();
53+
await clientViewPage.waitForLoad();
54+
await clientViewPage.chooseAction('Reject');
55+
56+
await rejectPage.waitForLoad();
57+
await rejectPage.submitRejection({
58+
rejectionDate: REJECTION_DATE,
59+
reasonName: REJECTION_REASON_NAME
60+
});
61+
62+
await clientViewPage.waitForLoad();
63+
await expect(clientViewPage.successSnackbar).toContainText('Client rejected successfully.');
64+
65+
const clientDetails = await fineractApi.getClient(client.resourceId);
66+
expect(clientDetails.status?.value).toBe('Rejected');
67+
// Fineract's GET /clients/{id} response does not surface `rejectedOnDate`
68+
// in the timeline payload for rejected clients (verified against the
69+
// running backend on 2026-07-15). The status transition itself is the
70+
// definitive evidence of the reject action; we still assert that the
71+
// pre-existing `submittedOnDate` timeline entry is preserved so any
72+
// future regression that wipes the timeline object is caught.
73+
expect(clientDetails.timeline?.submittedOnDate).toEqual([
74+
2024,
75+
1,
76+
1
77+
]);
78+
});
79+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Copyright since 2026 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 '../../../fixtures/test-fixtures';
10+
import { createTestClient } from '../../../factories/client.factory';
11+
import { ClientViewPage } from '../../../pages/client-view.page';
12+
import { ROUTES } from '../../../config/routes';
13+
14+
/**
15+
* WA-3.3 client lifecycle happy path: Rejected → Pending (Undo Rejection).
16+
*
17+
* Pattern:
18+
* 1. Factory creates a pending client; API immediately rejects it so
19+
* the client enters the predecessor `Rejected` state.
20+
* 2. UI drives the transition via `chooseAction('Undo Rejection')`.
21+
* Undo Rejection carries a single `reopenedDate` control and has
22+
* no dedicated page object yet, so the field is driven inline.
23+
* 3. Assert snackbar copy + `getClient().status.value` returns to
24+
* `Pending` + `timeline.rejectedOnDate` is cleared.
25+
*/
26+
const SUBMITTED_ON_DATE = '01 January 2024';
27+
const REJECTION_DATE = '02 January 2024';
28+
const REOPENED_DATE = '03 January 2024';
29+
const REJECTION_REASON_NAME = 'E2E Reject Client Reason';
30+
31+
test.describe('Client lifecycle · Undo Rejection (Rejected → Pending)', () => {
32+
test.beforeEach(async ({ page }) => {
33+
await page.addInitScript(() => {
34+
const creds = localStorage.getItem('mifosXCredentials');
35+
if (creds) {
36+
sessionStorage.setItem('mifosXCredentials', creds);
37+
}
38+
});
39+
});
40+
41+
test('undoes a rejection from the client actions flow', async ({
42+
page,
43+
fineractApi,
44+
apiSetup,
45+
cleanupGuard
46+
}) => {
47+
const rejectionReason = await fineractApi.ensureClientRejectionReason(REJECTION_REASON_NAME);
48+
49+
const client = await createTestClient(apiSetup, cleanupGuard, {
50+
submittedOnDate: SUBMITTED_ON_DATE
51+
});
52+
53+
// Predecessor state: reject through the API so the UI transition
54+
// under test is Undo Rejection, not the initial rejection.
55+
await fineractApi.rejectClient(client.resourceId, rejectionReason.id, REJECTION_DATE);
56+
57+
const clientViewPage = new ClientViewPage(page, client.resourceId);
58+
59+
await clientViewPage.navigate();
60+
await clientViewPage.waitForLoad();
61+
await clientViewPage.chooseAction('Undo Rejection');
62+
63+
await expect(page).toHaveURL(new RegExp(`${ROUTES.clientAction(client.resourceId, 'Undo Rejection')}$`));
64+
65+
const reopenedDateInput = page.locator('input[formcontrolname="reopenedDate"]');
66+
await reopenedDateInput.waitFor({ state: 'visible', timeout: 30000 });
67+
await reopenedDateInput.fill(REOPENED_DATE);
68+
await reopenedDateInput.blur();
69+
70+
const confirmButton = page.getByRole('button', { name: 'Confirm' });
71+
await expect(confirmButton).toBeEnabled();
72+
await confirmButton.click();
73+
74+
await clientViewPage.waitForLoad();
75+
await expect(clientViewPage.successSnackbar).toContainText('Client rejection undone successfully.');
76+
77+
const clientDetails = await fineractApi.getClient(client.resourceId);
78+
expect(clientDetails.status?.value).toBe('Pending');
79+
// Fineract clears the rejection timestamp when a rejection is undone.
80+
expect(clientDetails.timeline?.rejectedOnDate).toBeFalsy();
81+
});
82+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright since 2026 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 '../../../fixtures/test-fixtures';
10+
import { createTestClient } from '../../../factories/client.factory';
11+
import { ClientViewPage } from '../../../pages/client-view.page';
12+
import { WithdrawClientPage } from '../../../pages/client-actions/withdraw-client.page';
13+
14+
/**
15+
* WA-3.3 client lifecycle happy path: Pending → Withdrawn.
16+
*
17+
* Pattern:
18+
* 1. Factory creates a pending client.
19+
* 2. UI drives the transition via `chooseAction('Withdraw')`.
20+
* 3. Assert snackbar copy + `getClient().status.value` +
21+
* timeline entry (`withdrawnOnDate`).
22+
*/
23+
const SUBMITTED_ON_DATE = '01 January 2024';
24+
const WITHDRAWAL_DATE = '02 January 2024';
25+
const WITHDRAWAL_REASON_NAME = 'E2E Withdraw Client Reason';
26+
27+
test.describe('Client lifecycle · Withdraw (Pending → Withdrawn)', () => {
28+
test.beforeEach(async ({ page }) => {
29+
await page.addInitScript(() => {
30+
const creds = localStorage.getItem('mifosXCredentials');
31+
if (creds) {
32+
sessionStorage.setItem('mifosXCredentials', creds);
33+
}
34+
});
35+
});
36+
37+
test('withdraws a pending client from the client actions flow', async ({
38+
page,
39+
fineractApi,
40+
apiSetup,
41+
cleanupGuard
42+
}) => {
43+
await fineractApi.ensureClientWithdrawalReason(WITHDRAWAL_REASON_NAME);
44+
45+
const client = await createTestClient(apiSetup, cleanupGuard, {
46+
submittedOnDate: SUBMITTED_ON_DATE
47+
});
48+
49+
const clientViewPage = new ClientViewPage(page, client.resourceId);
50+
const withdrawPage = new WithdrawClientPage(page, client.resourceId);
51+
52+
await clientViewPage.navigate();
53+
await clientViewPage.waitForLoad();
54+
await clientViewPage.chooseAction('Withdraw');
55+
56+
await withdrawPage.waitForLoad();
57+
await withdrawPage.submitWithdrawal({
58+
withdrawalDate: WITHDRAWAL_DATE,
59+
reasonName: WITHDRAWAL_REASON_NAME
60+
});
61+
62+
await clientViewPage.waitForLoad();
63+
await expect(clientViewPage.successSnackbar).toContainText('Client withdrawn successfully.');
64+
65+
const clientDetails = await fineractApi.getClient(client.resourceId);
66+
expect(clientDetails.status?.value).toBe('Withdrawn');
67+
// Fineract's GET /clients/{id} response does not surface `withdrawnOnDate`
68+
// in the timeline payload for withdrawn clients (verified against the
69+
// running backend on 2026-07-15). The status transition itself is the
70+
// definitive evidence of the withdraw action; we still assert that the
71+
// pre-existing `submittedOnDate` timeline entry is preserved so any
72+
// future regression that wipes the timeline object is caught.
73+
expect(clientDetails.timeline?.submittedOnDate).toEqual([
74+
2024,
75+
1,
76+
1
77+
]);
78+
});
79+
});

0 commit comments

Comments
 (0)