Skip to content
Open
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,15 @@ openimis-dist_dkr.code-workspace
node_modules/
cypress/screenshots/
cypress/downloads/
cypress/videos/
cypress/fixtures/tmp_individuals.csv
cypress/fixtures/_generated/

.venv

.vscode

# Environment variables
.env
.env.local
.env.*.local
8 changes: 8 additions & 0 deletions cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ module.exports = defineConfig({
const row = line.split(',');
const oldCode = row[groupCodeIdx];

// Preserve empty group_code — individuals without a group are
// needed for individual program enrollment (grouped individuals
// are ineligible for individual programs).
if (!oldCode) {
newLines.push(row.join(','));
continue;
}

if (!groupMap[oldCode]) {
groupMap[oldCode] = Math.random().toString(36).substring(2, 8).toUpperCase();
}
Expand Down
217 changes: 217 additions & 0 deletions cypress/e2e/payment-cycle.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { TIMEOUTS } from '../support/constants';

describe('Payment cycle workflows', () => {
const getDateOffset = (days) => {
const date = new Date();
date.setDate(date.getDate() + days);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}-${month}-${year}`;
};

const cycleData = () => {
const uniquePart = `${Date.now().toString().slice(-5)}${Math.random().toString(36).slice(2, 4).toUpperCase()}`;
return {
// Code field max length is not as restrictive as program codes; use a short prefix
// to keep within any backend limits while still being unique per test run.
code: `PC${uniquePart}`,
startDate: getDateOffset(0),
endDate: getDateOffset(30),
status: 'PENDING',
};
};

// Payment cycles have no delete action in the UI, so created test records
// accumulate in the database. Use unique codes per run to avoid conflicts.

before(() => {
// Ensure a task group exists for PaymentCycleService tasks so ACTIVE
// cycle creation tasks are auto-assigned (ACCEPTED) and can be approved.
cy.login();
cy.ensurePaymentCycleTaskGroup();
cy.logout();
});

beforeEach(() => {
cy.login();
});

it('validates required fields before allowing payment cycle creation', () => {
cy.openCreatePaymentCycle();
cy.assertSaveDisabled();
});

it('creates a PENDING payment cycle successfully', () => {
const cycle = cycleData();

cy.createPaymentCycle(cycle);

cy.filterPaymentCycles({ code: cycle.code });
cy.assertPaymentCycleRowVisible({ code: cycle.code });
});

it('searches payment cycles by code', () => {
const targetCycle = cycleData();
const otherCycle = cycleData();

cy.createPaymentCycle(targetCycle);
cy.createPaymentCycle(otherCycle);

cy.filterPaymentCycles({ code: targetCycle.code });
cy.assertPaymentCycleRowVisible({ code: targetCycle.code });
cy.assertPaymentCycleRowNotVisible({ code: otherCycle.code });

cy.filterPaymentCycles({ code: otherCycle.code });
cy.assertPaymentCycleRowVisible({ code: otherCycle.code });
cy.assertPaymentCycleRowNotVisible({ code: targetCycle.code });
});

it('views payment cycle details from the list', () => {
const cycle = cycleData();

cy.createPaymentCycle(cycle);
cy.openPaymentCycleForViewFromList(cycle.code);
cy.assertPaymentCycleDetailFields(cycle);
});

it('creates an ACTIVE payment cycle via task approval and verifies all fields', () => {
const cycle = cycleData();

// ACTIVE cycles route through the maker-checker task workflow:
// 1. Save creates a task and shows a notification dialog.
// 2. Approve the task via All Tasks.
// 3. After approval the cycle is created as ACTIVE.
cy.createPaymentCycle({ ...cycle, status: 'ACTIVE' });
cy.approveLatestPaymentCycleTask();

cy.filterPaymentCycles({ code: cycle.code });
cy.assertPaymentCycleRowVisible({ code: cycle.code });

cy.openPaymentCycleForViewFromList(cycle.code);
cy.assertPaymentCycleDetailFields({ ...cycle, status: 'ACTIVE' });
});

it('creates a SUSPENDED payment cycle', () => {
const cycle = cycleData();

cy.createPaymentCycle({ ...cycle, status: 'SUSPENDED' });

cy.filterPaymentCycles({ code: cycle.code });
cy.assertPaymentCycleRowVisible({ code: cycle.code });
});

it('filters payment cycles by status', () => {
const pendingCycle = cycleData();
const suspendedCycle = cycleData();

cy.createPaymentCycle({ ...pendingCycle, status: 'PENDING' });
cy.createPaymentCycle({ ...suspendedCycle, status: 'SUSPENDED' });

// Verify the status filter EXCLUDES cycles with a different status.
cy.filterPaymentCycles({ status: 'PENDING' });
cy.assertPaymentCycleRowNotVisible({ code: suspendedCycle.code });

cy.filterPaymentCycles({ status: 'SUSPENDED' });
cy.assertPaymentCycleRowNotVisible({ code: pendingCycle.code });
});

it('shows read-only fields on the detail page', () => {
const cycle = cycleData();

cy.createPaymentCycle(cycle);
cy.openPaymentCycleForViewFromList(cycle.code);

cy.assertMuiInputDisabled('Code', cycle.code);
cy.assertMuiInputDisabled('Start Date', cycle.startDate);
cy.assertMuiInputDisabled('End Date');
cy.assertMuiSelectValue('Status', 'PENDING');
});

it('blocks save when the code is changed to a duplicate value', () => {
const existingCycle = cycleData();
const newCycle = cycleData();

cy.createPaymentCycle(existingCycle);

cy.openCreatePaymentCycle();
cy.fillPaymentCycleForm(newCycle);
cy.assertSaveEnabled();

cy.enterMuiInput('Code', existingCycle.code);
cy.assertSaveDisabled();
});

it('creates a payment cycle and immediately searches for it', () => {
const cycle = cycleData();

cy.createPaymentCycle(cycle);

cy.filterPaymentCycles({ code: cycle.code });
cy.assertPaymentCycleRowVisible({ code: cycle.code });
});

it('resets payment cycle filters and restores full list', () => {
cy.visit('/front/paymentCycles');
cy.contains(/\d+ Payment Cycle/, { timeout: TIMEOUTS.BACKEND_VALIDATION });
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);

cy.enterMuiInput('Code', 'FAKE_CODE');

cy.resetPaymentCycleFilters();

cy.assertMuiInput('Code', '');
});

it('filters payment cycles by date range (Date From / Date To)', () => {
const cycle = cycleData();

cy.createPaymentCycle(cycle);

// A "Date From" filter in the far future must exclude this cycle (its
// startDate is today).
cy.filterPaymentCycles({ code: cycle.code, dateFrom: getDateOffset(365) });
cy.assertPaymentCycleRowNotVisible({ code: cycle.code });

// A "Date From" filter in the past includes it.
cy.filterPaymentCycles({ code: cycle.code, dateFrom: getDateOffset(-365) });
cy.assertPaymentCycleRowVisible({ code: cycle.code });
});

it('payroll form Payment Cycle picker excludes non-ACTIVE cycles', () => {
// The PaymentCyclePicker used on the payroll create form queries
// paymentCycle with `status: ACTIVE`, so a PENDING cycle must never
// appear in its autocomplete options.
const pendingCycle = cycleData();

cy.createPaymentCycle(pendingCycle);

cy.visit('/front/payrolls');
cy.contains('Payrolls Found', { timeout: TIMEOUTS.BACKEND_VALIDATION });
cy.createClick();
cy.url({ timeout: TIMEOUTS.BACKEND_VALIDATION }).should('include', '/payrolls/payroll');

// The cycle picker fires a GraphQL query with `status: ACTIVE` on input
// change — alias it so we can await the response before asserting.
cy.aliasGraphqlQuery('paymentCycle(', 'cyclePickerFetch');

cy.contains('label', 'Payment Cycle', { timeout: TIMEOUTS.BACKEND_VALIDATION })
.siblings('.MuiInputBase-root')
.find('input')
.click({ force: true })
.clear({ force: true })
.type(pendingCycle.code, { force: true });
cy.wait('@cyclePickerFetch', { timeout: TIMEOUTS.BACKEND_VALIDATION });

// The PENDING cycle code must not appear in any autocomplete listbox option.
cy.get('body').then(($body) => {
const options = $body.find('[role="listbox"] li, [role="presentation"] li').toArray();
const pendingVisible = options.some((li) => li.innerText.includes(pendingCycle.code));
expect(
pendingVisible,
`PENDING cycle ${pendingCycle.code} must not appear in the ACTIVE-filtered picker`,
).to.equal(false);
});
});
});
Loading
Loading