blob: b939a5c39f9c6a3a669efe73947f46c4a2d86539 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Covers the Loan Portfolio Summary screen — the search/report tool ported from web-app's
* "fund mapping" feature (which, despite its name, has no fund field and performs no bulk
* reassignment; it is a read-only advance search over the loan portfolio). Mocked throughout.
*/
import { test, expect, Page } from './fixtures';
import { ionSelect } from './utils/ionic-locators';
const TENANT = 'default';
const USER = 'mifos';
const PASSWORD = 'password';
async function mockLoanPortfolioSummary(page: Page) {
await page.route(/\/api\/v1\//, async (route) => {
await route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
});
await page.route('**/config.json*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ fineractApiUrl: '/api/v1', defaultTenant: TENANT }),
});
});
await page.route('**/api/v1/authentication**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
username: USER,
userId: 1,
base64EncodedAuthenticationKey: 'YmFzZTY0',
authenticated: true,
officeId: 1,
officeName: 'Head Office',
roles: [{ id: 1, name: 'Role', description: 'Role' }],
permissions: ['ALL_FUNCTIONS'],
}),
});
});
await page.route(/\/api\/v1\/businessdate/, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ type: 'BUSINESS_DATE', date: [2026, 8, 16] }]),
});
});
await page.route('**/api/v1/search/template', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
offices: [{ id: 1, name: 'Head Office' }],
loanProducts: [{ id: 2, name: 'Personal Loan' }],
}),
});
});
}
async function login(page: Page) {
await page.goto('/login');
await page.locator('#tenantId').fill(TENANT);
await page.locator('#username').fill(USER);
await page.locator('#password').fill(PASSWORD);
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL('/dashboard');
}
test.describe('Loan Portfolio Summary', () => {
test.beforeEach(async ({ page }) => {
await mockLoanPortfolioSummary(page);
await login(page);
await page.goto('/organization/loan-portfolio-summary');
await expect(page.getByRole('button', { name: 'Search' })).toBeVisible();
});
test('searching sends entities: loans and shows the grouped results', async ({ page }) => {
let requestBody: Record<string, unknown> | undefined;
await page.route('**/api/v1/search/advance', async (route) => {
requestBody = route.request().postDataJSON();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ officeName: 'Head Office', loanProductName: 'Personal Loan', count: 5, percentage: 40 },
]),
});
});
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByRole('cell', { name: 'Head Office' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Personal Loan' })).toBeVisible();
expect(requestBody?.['entities']).toEqual(['loans']);
expect(requestBody).not.toHaveProperty('outStandingAmountPercentage');
expect(requestBody).not.toHaveProperty('outstandingAmount');
});
test('Edit Parameters returns from the results view to the filter form', async ({ page }) => {
await page.route('**/api/v1/search/advance', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ officeName: 'Head Office', loanProductName: 'Personal Loan' }]),
});
});
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByRole('cell', { name: 'Head Office' })).toBeVisible();
await page.getByRole('button', { name: 'Edit Parameters' }).click();
await expect(page.getByRole('button', { name: 'Search' })).toBeVisible();
});
test('checking the outstanding-amount filter sends the between min/max values', async ({
page,
}) => {
let requestBody: Record<string, unknown> | undefined;
await page.route('**/api/v1/search/advance', async (route) => {
requestBody = route.request().postDataJSON();
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
});
await page.locator('ion-checkbox[aria-label="Outstanding Amount"]').click();
// 'between' is the default comparison condition, so its min/max fields are already showing.
await page.getByRole('spinbutton', { name: 'Minimum Value' }).fill('500');
await page.getByRole('spinbutton', { name: 'Maximum Value' }).fill('1000');
await page.getByRole('button', { name: 'Search' }).click();
expect(requestBody?.['includeOutstandingAmount']).toBe(true);
expect(requestBody?.['outstandingAmountCondition']).toBe('between');
expect(requestBody?.['minOutstandingAmount']).toBe(500);
expect(requestBody?.['maxOutstandingAmount']).toBe(1000);
});
test('switching to a single-value comparison condition sends one value', async ({ page }) => {
let requestBody: Record<string, unknown> | undefined;
await page.route('**/api/v1/search/advance', async (route) => {
requestBody = route.request().postDataJSON();
await route.fulfill({ status: 200, contentType: 'application/json', body: '[]' });
});
await page.locator('ion-checkbox[aria-label="Outstanding Percentage"]').click();
await ionSelect(page, 'Comparison Condition').click();
await page
.locator('ion-alert, ion-popover, ion-action-sheet')
.getByRole('radio', { name: '>=', exact: true })
.click();
await page.getByRole('spinbutton', { name: 'Comparison Value' }).fill('75');
await page.getByRole('button', { name: 'Search' }).click();
expect(requestBody?.['includeOutStandingAmountPercentage']).toBe(true);
expect(requestBody?.['outStandingAmountPercentageCondition']).toBe('>=');
expect(requestBody?.['outStandingAmountPercentage']).toBe(75);
});
});