Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ The same flow runs in CI via `.github/workflows/e2e.yml`. See
[`DOCS/E2E_TESTING.md`](DOCS/E2E_TESTING.md) for writing specs, and prefer `data-testid`
over element selectors so tests survive markup changes.

Once the backend above is up, `npm run seed:demo-data` populates it with a representative
dataset — an office, staff, a center, a group, an active loan, a loan pending approval, a
savings account, a fixed deposit, a share account, a manual journal entry and two reports —
for manual sanity testing rather than the narrow fixtures an individual spec builds for
itself. It prints what it created. This is its own Playwright project (`demo-seed`), so it
never runs as a side effect of the `backend` project in CI.

## UI Components

The UI layer is **Ionic** (`@ionic/angular` v8), configured in `mode: 'md'`.
Expand Down
115 changes: 115 additions & 0 deletions e2e/demo-data.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.
*/

/**
* Populates a freshly provisioned Fineract instance with a representative dataset for a
* human to click through — release-candidate sanity testing, a demo, a screenshot pass —
* rather than the narrow fixtures an individual spec builds for itself.
*
* Composed entirely from the existing seed-api.ts building blocks the specs already use and
* trust; this adds no new seeding logic of its own; it only sequences what is already there
* into one representative pass and prints what it made.
*
* Not part of any CI project's default run — this is a standalone project
* (`--project=demo-seed`) so it never executes as a side effect of `--project=backend`, and
* it does not depend on the `setup` project, so `npm run seed:demo-data` works on its own
* against a backend that has had nothing else run against it yet.
*
* Run with:
* npm run seed:demo-data
*
* or, against a non-default backend:
* FINERACT_SERVER_URL=/fineract-provider/api/v1 npm run seed:demo-data
*/

import { test as setup } from '@playwright/test';

import {
createApiContext,
ensureReferenceData,
seedActiveLoan,
seedCenter,
seedChartReport,
seedClient,
seedCollateralProduct,
seedFixedDepositAccount,
seedGroup,
seedLoanCollateralType,
seedLoanDatatable,
seedManualJournalEntry,
seedOffice,
seedPendingLoan,
seedReportDefinition,
seedSavingsAccountWithTransactions,
seedShareAccount,
seedStaff,
} from './utils/seed-api';

setup('seed a representative demo dataset', async () => {
// Generous: this makes roughly twenty sequential API calls against a backend whose first
// requests after boot are already slow while caches warm.
setup.setTimeout(180000);

const api = await createApiContext();
try {
await ensureReferenceData(api);
await seedLoanDatatable(api);
await seedCollateralProduct(api);
await seedLoanCollateralType(api);

const office = await seedOffice(api, 'Demo');
const staff = await seedStaff(api, 'DemoOfficer');
const center = await seedCenter(api, 'Demo Center');
const group = await seedGroup(api, 'Demo Group');

const browsingClient = await seedClient(api, 'DemoBrowse');
const activeLoan = await seedActiveLoan(api, 'DemoActive');
const pendingLoan = await seedPendingLoan(api, 'DemoQueue');
const savings = await seedSavingsAccountWithTransactions(api, 'DemoSavings');
const fixedDeposit = await seedFixedDepositAccount(api, 'DemoFixed');
const shareAccount = await seedShareAccount(api, 'DemoShares');
const journalEntry = await seedManualJournalEntry(api, 'DemoJournal');
const chartReport = await seedChartReport(api, 'DemoChart');
const reportDefinition = await seedReportDefinition(api, 'DemoReportDef');

console.log(
[
'',
'Demo dataset ready. Sign in with the usual local admin credentials and look for:',
'',
` Office ${office.officeName} (#${office.officeId})`,
` Staff ${staff.staffName}`,
` Center ${center.centerName} (#${center.centerId}) — pending`,
` Group ${group.groupName} (#${group.groupId}) — no parent center yet`,
` Client ${browsingClient.displayName} (#${browsingClient.clientId})`,
` Active loan ${activeLoan.displayName} — #${activeLoan.loanId}, disbursed`,
` Pending loan ${pendingLoan.clientName} — ${pendingLoan.accountNo}, in the approval queue`,
` Savings account ${savings.clientName} — #${savings.savingsId}, one deposit + one hold`,
` Fixed deposit ${fixedDeposit.clientName} — #${fixedDeposit.accountId}, pending approval`,
` Share account ${shareAccount.clientName} — ${shareAccount.productName}, pending approval`,
` Manual journal entry ${journalEntry.debitAccountName} / ${journalEntry.creditAccountName} — reversible`,
` Chart report ${chartReport.reportName} (Reports > Run Reports)`,
` Report definition ${reportDefinition.reportName} (System > Report Definitions)`,
'',
].join('\n'),
);
} finally {
await api.dispose();
}
});
24 changes: 24 additions & 0 deletions e2e/global-search-nav-shortcuts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ test.describe('Global search navigation shortcuts', () => {
await expect(page).toHaveURL('/organization/offices');
});

test('keeps the results open across the mousedown that starts a click, so a slow render cannot race the dropdown closed', async ({
page,
}) => {
await login(page);

await searchBox(page).fill('Offices');
await expect(offices(page)).toBeVisible();

// The bug this pins down: on a slow render, the searchbar's 150ms blur-triggered hide
// could fire before a click's mouseup landed, collapsing the list out from under the
// click and silently swallowing the navigation. `(mousedown)="$event.preventDefault()"`
// on each result stops the searchbar from blurring at all when a result is the click's
// target, so the list must still be open here even though the click has not completed.
const box = await offices(page).boundingBox();
if (!box) throw new Error('offices result not found');
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.waitForTimeout(200); // longer than the 150ms hide timeout
await expect(results(page)).toBeVisible();
await page.mouse.up();

await expect(page).toHaveURL('/organization/offices');
});

test('withholds a shortcut the user has no permission to reach', async ({ page }) => {
await login(page, ['READ_CLIENT']);

Expand Down
99 changes: 99 additions & 0 deletions e2e/guidance-tour-dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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.
*/

/**
* Cover for the dashboard tour's second step highlighting the wrong element.
*
* `targetSelector: 'ul'` on that step matched `document.querySelector('ul')`, which returns
* the FIRST `<ul>` in the DOM — the sidebar's own `<ul class="nav-list">`, which renders before
* the dashboard's main content. The step's copy describes the "System Overview & Status" card
* (a `.status-list`), so the tour scrolled to and outlined the sidebar instead of the thing it
* was actually talking about. Fixed by pointing the selector at `.status-list` directly.
*/

import { test, expect } from './fixtures';

const TENANT = 'default';
const USER = 'mifos';
const PASSWORD = 'password';

test.beforeEach(async ({ 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.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('highlights the System Status card, not the sidebar, on the dashboard tour', async ({
page,
}) => {
await page.locator('.tour-btn').click();
await expect(page.locator('app-guidance-tour')).toBeVisible();

await page.getByRole('button', { name: 'Next' }).click();
// Scoped to the tour card specifically: the dashboard's own cards (Pending Approvals, Loan
// Status Distribution, etc.) each have their own ion-card-title too.
await expect(page.locator('.guidance-card ion-card-title')).toContainText(
'System Overview & Status',
);

// The sidebar's own nav list must never carry the highlight — that was the bug: the bare
// 'ul' selector matched it first because it sits earlier in the DOM than the dashboard.
await expect(page.locator('.nav-list')).not.toHaveClass(/guidance-highlight/);

// The dashboard's System Status card is the thing the step is actually describing, and it
// alone should carry the highlight.
await expect(page.locator('.status-list')).toHaveClass(/guidance-highlight/);
});
Loading
Loading