-
Notifications
You must be signed in to change notification settings - Fork 960
Web 1039 playwright client lifecycle happy paths e 2 e specs #3725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
IOhacker
merged 1 commit into
openMF:dev
from
devvaansh:WEB-1039-web-playwright-client-lifecycle-happy-path-e-2-e-specs
Jul 24, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /** | ||
| * Copyright since 2026 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 '../../../fixtures/test-fixtures'; | ||
| import { createTestClient } from '../../../factories/client.factory'; | ||
| import { ClientViewPage } from '../../../pages/client-view.page'; | ||
| import { ActivateClientPage } from '../../../pages/client-actions/activate-client.page'; | ||
|
|
||
| /** | ||
| * WA-3.3 client lifecycle happy path: Pending → Active. | ||
| * | ||
| * Pattern: | ||
| * 1. Factory creates the predecessor-state client (pending). | ||
| * 2. UI drives the transition via `chooseAction('Activate')`. | ||
| * 3. Assert snackbar copy + `getClient().status.value` + | ||
| * timeline entry (`activatedOnDate`). | ||
| */ | ||
| const SUBMITTED_ON_DATE = '01 January 2024'; | ||
| const ACTIVATION_DATE = '02 January 2024'; | ||
|
|
||
| test.describe('Client lifecycle · Activate (Pending → Active)', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.addInitScript(() => { | ||
| const creds = localStorage.getItem('mifosXCredentials'); | ||
| if (creds) { | ||
| sessionStorage.setItem('mifosXCredentials', creds); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| test('activates a pending client from the client actions flow', async ({ | ||
| page, | ||
| fineractApi, | ||
| apiSetup, | ||
| cleanupGuard | ||
| }) => { | ||
| const client = await createTestClient(apiSetup, cleanupGuard, { | ||
| submittedOnDate: SUBMITTED_ON_DATE | ||
| }); | ||
|
|
||
| const clientViewPage = new ClientViewPage(page, client.resourceId); | ||
| const activatePage = new ActivateClientPage(page, client.resourceId); | ||
|
|
||
| await clientViewPage.navigate(); | ||
| await clientViewPage.waitForLoad(); | ||
| await clientViewPage.chooseAction('Activate'); | ||
|
|
||
| await activatePage.waitForLoad(); | ||
| await activatePage.submitActivation({ activationDate: ACTIVATION_DATE }); | ||
|
|
||
| await clientViewPage.waitForLoad(); | ||
| await expect(clientViewPage.successSnackbar).toContainText('Client activated successfully.'); | ||
|
|
||
| const clientDetails = await fineractApi.getClient(client.resourceId); | ||
| expect(clientDetails.status?.value).toBe('Active'); | ||
| expect(clientDetails.timeline?.activatedOnDate).toEqual([ | ||
| 2024, | ||
| 1, | ||
| 2 | ||
| ]); | ||
| }); | ||
| }); |
85 changes: 85 additions & 0 deletions
85
playwright/tests/clients/lifecycle/reactivate-after-close.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| /** | ||
| * Copyright since 2026 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 '../../../fixtures/test-fixtures'; | ||
| import { ClientViewPage } from '../../../pages/client-view.page'; | ||
| import { ReactivateClientPage } from '../../../pages/client-actions/reactivate-client.page'; | ||
|
|
||
| /** | ||
| * WA-3.3 client lifecycle happy path: Closed → Pending (Reactivate). | ||
| * | ||
| * Pattern: | ||
| * 1. API creates an active client, then closes it, so the client | ||
| * enters the predecessor `Closed` state. This flow deliberately | ||
| * bypasses `createTestClient` because active/closed clients | ||
| * cannot be hard-deleted by the cleanup guard. | ||
| * 2. UI drives the transition via `chooseAction('Reactivate')`. | ||
| * 3. Assert snackbar copy + `getClient().status.value` returns to | ||
| * `Pending` (Fineract's documented reactivate destination — a | ||
| * subsequent Activate is required to reach `Active`) + the | ||
| * `closedOnDate` timeline entry is cleared. | ||
| */ | ||
| const SUBMITTED_ON_DATE = '01 January 2024'; | ||
| const ACTIVATION_DATE = '02 January 2024'; | ||
| const CLOSURE_DATE = '03 January 2024'; | ||
| const REACTIVATION_DATE = '04 January 2024'; | ||
| const CLOSURE_REASON_NAME = 'E2E Close Client Reason'; | ||
|
|
||
| test.describe('Client lifecycle · Reactivate (Closed → Pending)', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.addInitScript(() => { | ||
| const creds = localStorage.getItem('mifosXCredentials'); | ||
| if (creds) { | ||
| sessionStorage.setItem('mifosXCredentials', creds); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| test('reactivates a closed client from the client actions flow', async ({ page, fineractApi }) => { | ||
| const closureReason = await fineractApi.ensureClientClosureReason(CLOSURE_REASON_NAME); | ||
| const officeId = await fineractApi.getFirstOfficeId(); | ||
|
|
||
| const uniqueSuffix = Date.now(); | ||
| const createClientResponse = await fineractApi.createActiveClient(officeId, { | ||
| firstname: `Reactivate${uniqueSuffix}`, | ||
| lastname: 'Client', | ||
| submittedOnDate: SUBMITTED_ON_DATE, | ||
| activationDate: ACTIVATION_DATE | ||
| }); | ||
|
|
||
| const clientId = createClientResponse.resourceId; | ||
| await fineractApi.closeClient(clientId, closureReason.id, CLOSURE_DATE); | ||
|
|
||
| const clientViewPage = new ClientViewPage(page, clientId); | ||
| const reactivatePage = new ReactivateClientPage(page, clientId); | ||
|
|
||
| await clientViewPage.navigate(); | ||
| await clientViewPage.waitForLoad(); | ||
| await clientViewPage.chooseAction('Reactivate'); | ||
|
|
||
| await reactivatePage.waitForLoad(); | ||
| await reactivatePage.submitReactivation({ reactivationDate: REACTIVATION_DATE }); | ||
|
|
||
| await clientViewPage.waitForLoad(); | ||
| await expect(clientViewPage.successSnackbar).toContainText('Client reactivated successfully.'); | ||
|
|
||
| const clientDetails = await fineractApi.getClient(clientId); | ||
| // In Fineract, reactivating a closed client moves it back to | ||
| // `Pending` state — a subsequent Activate action is required to | ||
| // return to `Active`. The Pending status combined with the cleared | ||
| // `closedOnDate` timeline entry is the definitive evidence that | ||
| // the reactivate command was recorded server-side. | ||
| expect(clientDetails.status?.value).toBe('Pending'); | ||
| expect(clientDetails.timeline?.closedOnDate).toBeFalsy(); | ||
| expect(clientDetails.timeline?.submittedOnDate).toEqual([ | ||
| 2024, | ||
| 1, | ||
| 1 | ||
| ]); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /** | ||
| * Copyright since 2026 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 '../../../fixtures/test-fixtures'; | ||
| import { createTestClient } from '../../../factories/client.factory'; | ||
| import { ClientViewPage } from '../../../pages/client-view.page'; | ||
| import { RejectClientPage } from '../../../pages/client-actions/reject-client.page'; | ||
|
|
||
| /** | ||
| * WA-3.3 client lifecycle happy path: Pending → Rejected. | ||
| * | ||
| * Pattern: | ||
| * 1. Factory creates a pending client. | ||
| * 2. UI drives the transition via `chooseAction('Reject')`. | ||
| * 3. Assert snackbar copy + `getClient().status.value` + | ||
| * timeline entry (`rejectedOnDate`). | ||
| */ | ||
| const SUBMITTED_ON_DATE = '01 January 2024'; | ||
| const REJECTION_DATE = '02 January 2024'; | ||
| const REJECTION_REASON_NAME = 'E2E Reject Client Reason'; | ||
|
|
||
| test.describe('Client lifecycle · Reject (Pending → Rejected)', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.addInitScript(() => { | ||
| const creds = localStorage.getItem('mifosXCredentials'); | ||
| if (creds) { | ||
| sessionStorage.setItem('mifosXCredentials', creds); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| test('rejects a pending client from the client actions flow', async ({ | ||
| page, | ||
| fineractApi, | ||
| apiSetup, | ||
| cleanupGuard | ||
| }) => { | ||
| await fineractApi.ensureClientRejectionReason(REJECTION_REASON_NAME); | ||
|
|
||
| const client = await createTestClient(apiSetup, cleanupGuard, { | ||
| submittedOnDate: SUBMITTED_ON_DATE | ||
| }); | ||
|
|
||
| const clientViewPage = new ClientViewPage(page, client.resourceId); | ||
| const rejectPage = new RejectClientPage(page, client.resourceId); | ||
|
|
||
| await clientViewPage.navigate(); | ||
| await clientViewPage.waitForLoad(); | ||
| await clientViewPage.chooseAction('Reject'); | ||
|
|
||
| await rejectPage.waitForLoad(); | ||
| await rejectPage.submitRejection({ | ||
| rejectionDate: REJECTION_DATE, | ||
| reasonName: REJECTION_REASON_NAME | ||
| }); | ||
|
|
||
| await clientViewPage.waitForLoad(); | ||
| await expect(clientViewPage.successSnackbar).toContainText('Client rejected successfully.'); | ||
|
|
||
| const clientDetails = await fineractApi.getClient(client.resourceId); | ||
| expect(clientDetails.status?.value).toBe('Rejected'); | ||
| // Fineract's GET /clients/{id} response does not surface `rejectedOnDate` | ||
| // in the timeline payload for rejected clients (verified against the | ||
| // running backend on 2026-07-15). The status transition itself is the | ||
| // definitive evidence of the reject action; we still assert that the | ||
| // pre-existing `submittedOnDate` timeline entry is preserved so any | ||
| // future regression that wipes the timeline object is caught. | ||
| expect(clientDetails.timeline?.submittedOnDate).toEqual([ | ||
| 2024, | ||
| 1, | ||
| 1 | ||
| ]); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /** | ||
| * Copyright since 2026 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 '../../../fixtures/test-fixtures'; | ||
| import { createTestClient } from '../../../factories/client.factory'; | ||
| import { ClientViewPage } from '../../../pages/client-view.page'; | ||
| import { ROUTES } from '../../../config/routes'; | ||
|
|
||
| /** | ||
| * WA-3.3 client lifecycle happy path: Rejected → Pending (Undo Rejection). | ||
| * | ||
| * Pattern: | ||
| * 1. Factory creates a pending client; API immediately rejects it so | ||
| * the client enters the predecessor `Rejected` state. | ||
| * 2. UI drives the transition via `chooseAction('Undo Rejection')`. | ||
| * Undo Rejection carries a single `reopenedDate` control and has | ||
| * no dedicated page object yet, so the field is driven inline. | ||
| * 3. Assert snackbar copy + `getClient().status.value` returns to | ||
| * `Pending` + `timeline.rejectedOnDate` is cleared. | ||
| */ | ||
| const SUBMITTED_ON_DATE = '01 January 2024'; | ||
| const REJECTION_DATE = '02 January 2024'; | ||
| const REOPENED_DATE = '03 January 2024'; | ||
| const REJECTION_REASON_NAME = 'E2E Reject Client Reason'; | ||
|
|
||
| test.describe('Client lifecycle · Undo Rejection (Rejected → Pending)', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.addInitScript(() => { | ||
| const creds = localStorage.getItem('mifosXCredentials'); | ||
| if (creds) { | ||
| sessionStorage.setItem('mifosXCredentials', creds); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| test('undoes a rejection from the client actions flow', async ({ page, fineractApi, apiSetup, cleanupGuard }) => { | ||
| const rejectionReason = await fineractApi.ensureClientRejectionReason(REJECTION_REASON_NAME); | ||
|
|
||
| const client = await createTestClient(apiSetup, cleanupGuard, { | ||
| submittedOnDate: SUBMITTED_ON_DATE | ||
| }); | ||
|
|
||
| // Predecessor state: reject through the API so the UI transition | ||
| // under test is Undo Rejection, not the initial rejection. | ||
| await fineractApi.rejectClient(client.resourceId, rejectionReason.id, REJECTION_DATE); | ||
|
|
||
| const clientViewPage = new ClientViewPage(page, client.resourceId); | ||
|
|
||
| await clientViewPage.navigate(); | ||
| await clientViewPage.waitForLoad(); | ||
| await clientViewPage.chooseAction('Undo Rejection'); | ||
|
|
||
| await expect(page).toHaveURL(new RegExp(`${ROUTES.clientAction(client.resourceId, 'Undo Rejection')}$`)); | ||
|
|
||
| const reopenedDateInput = page.locator('input[formcontrolname="reopenedDate"]'); | ||
| await reopenedDateInput.waitFor({ state: 'visible', timeout: 30000 }); | ||
| await reopenedDateInput.fill(REOPENED_DATE); | ||
| await reopenedDateInput.blur(); | ||
|
|
||
| const confirmButton = page.getByRole('button', { name: 'Confirm' }); | ||
| await expect(confirmButton).toBeEnabled(); | ||
| await confirmButton.click(); | ||
|
|
||
| await clientViewPage.waitForLoad(); | ||
| await expect(clientViewPage.successSnackbar).toContainText('Client rejection undone successfully.'); | ||
|
|
||
| const clientDetails = await fineractApi.getClient(client.resourceId); | ||
| expect(clientDetails.status?.value).toBe('Pending'); | ||
| // Fineract clears the rejection timestamp when a rejection is undone. | ||
| expect(clientDetails.timeline?.rejectedOnDate).toBeFalsy(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /** | ||
| * Copyright since 2026 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 '../../../fixtures/test-fixtures'; | ||
| import { createTestClient } from '../../../factories/client.factory'; | ||
| import { ClientViewPage } from '../../../pages/client-view.page'; | ||
| import { WithdrawClientPage } from '../../../pages/client-actions/withdraw-client.page'; | ||
|
|
||
| /** | ||
| * WA-3.3 client lifecycle happy path: Pending → Withdrawn. | ||
| * | ||
| * Pattern: | ||
| * 1. Factory creates a pending client. | ||
| * 2. UI drives the transition via `chooseAction('Withdraw')`. | ||
| * 3. Assert snackbar copy + `getClient().status.value` + | ||
| * timeline entry (`withdrawnOnDate`). | ||
| */ | ||
| const SUBMITTED_ON_DATE = '01 January 2024'; | ||
| const WITHDRAWAL_DATE = '02 January 2024'; | ||
| const WITHDRAWAL_REASON_NAME = 'E2E Withdraw Client Reason'; | ||
|
|
||
| test.describe('Client lifecycle · Withdraw (Pending → Withdrawn)', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.addInitScript(() => { | ||
| const creds = localStorage.getItem('mifosXCredentials'); | ||
| if (creds) { | ||
| sessionStorage.setItem('mifosXCredentials', creds); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| test('withdraws a pending client from the client actions flow', async ({ | ||
| page, | ||
| fineractApi, | ||
| apiSetup, | ||
| cleanupGuard | ||
| }) => { | ||
| await fineractApi.ensureClientWithdrawalReason(WITHDRAWAL_REASON_NAME); | ||
|
|
||
| const client = await createTestClient(apiSetup, cleanupGuard, { | ||
| submittedOnDate: SUBMITTED_ON_DATE | ||
| }); | ||
|
|
||
| const clientViewPage = new ClientViewPage(page, client.resourceId); | ||
| const withdrawPage = new WithdrawClientPage(page, client.resourceId); | ||
|
|
||
| await clientViewPage.navigate(); | ||
| await clientViewPage.waitForLoad(); | ||
| await clientViewPage.chooseAction('Withdraw'); | ||
|
|
||
| await withdrawPage.waitForLoad(); | ||
| await withdrawPage.submitWithdrawal({ | ||
| withdrawalDate: WITHDRAWAL_DATE, | ||
| reasonName: WITHDRAWAL_REASON_NAME | ||
| }); | ||
|
|
||
| await clientViewPage.waitForLoad(); | ||
| await expect(clientViewPage.successSnackbar).toContainText('Client withdrawn successfully.'); | ||
|
|
||
| const clientDetails = await fineractApi.getClient(client.resourceId); | ||
| expect(clientDetails.status?.value).toBe('Withdrawn'); | ||
| // Fineract's GET /clients/{id} response does not surface `withdrawnOnDate` | ||
| // in the timeline payload for withdrawn clients (verified against the | ||
| // running backend on 2026-07-15). The status transition itself is the | ||
| // definitive evidence of the withdraw action; we still assert that the | ||
| // pre-existing `submittedOnDate` timeline entry is preserved so any | ||
| // future regression that wipes the timeline object is caught. | ||
| expect(clientDetails.timeline?.submittedOnDate).toEqual([ | ||
| 2024, | ||
| 1, | ||
| 1 | ||
| ]); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.