From 8907f4e9133432f60e94e47a86e4d6502a29438a Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Wed, 6 Aug 2025 12:43:26 +0800 Subject: [PATCH 01/33] Add e2e test case covering individual json schema config workflow --- .gitignore | 1 + cypress.config.js | 1 + cypress/e2e/admin.cy.js | 65 +++++++++++++++++++ .../fixtures/individual-config-minimal.json | 3 + cypress/support/commands.js | 44 ++++++------- 5 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 cypress/e2e/admin.cy.js create mode 100644 cypress/fixtures/individual-config-minimal.json diff --git a/.gitignore b/.gitignore index 81f479c..cc4cf8c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ data/* openimis-dist_dkr.code-workspace node_modules/ cypress/screenshots/ +cypress/downloads/ diff --git a/cypress.config.js b/cypress.config.js index 9fa228f..e7fcde0 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -49,6 +49,7 @@ module.exports = defineConfig({ baseUrl: 'http://localhost/front', defaultCommandTimeout: 10000, taskTimeout: 300000, + downloadsFolder: 'cypress/downloads', setupNodeEvents(on, config) { on('task', { checkSetup() { diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js new file mode 100644 index 0000000..f3f6bd4 --- /dev/null +++ b/cypress/e2e/admin.cy.js @@ -0,0 +1,65 @@ +const path = require('path'); + +describe('Django admin workflows', () => { + beforeEach(function () { + cy.login_admin_interface() + }); + + it('Configuring individual json schema reflects in advanced filters and upload template', function () { + cy.contains('a', 'Module configurations').click() + + cy.get('table#result_list').then(($table) => { + const individualLink = $table.find('a:contains("individual")') + + // Delete any existing individual config + if (individualLink.length) { + cy.wrap(individualLink).click() + cy.contains('a.deletelink', 'Delete').click() + cy.get('input[type="submit"][value*="Yes"]').click() + } + + // Create individual config using fixture config file + cy.contains('a', 'Add module configuration').click() + cy.get('input[name="module"]').type('individual') + cy.get('select[name="layer"]').select('backend') + cy.get('input[name="version"]').type(1) + + cy.fixture('individual-config-minimal.json').then((config) => { + const configString = JSON.stringify(config, null, 2); + cy.get('textarea[name="config"]') + .type(configString, { + parseSpecialCharSequences: false, + delay: 0 // Type faster + }); + + cy.get('input[value="Save"]').click() + + cy.visit('/individuals') + cy.contains('li', 'UPLOAD').click() + cy.contains('button', 'Template').click() + + const downloadedFilename = path.join( + Cypress.config('downloadsFolder'), + 'individual_upload_template.csv' + ); + cy.readFile(downloadedFilename, { timeout: 15000 }).should('exist'); + + cy.readFile(downloadedFilename) + .then(async (text) => { + expect(text.length).to.be.greaterThan(0); + expect(text).to.contain('able_bodied'); + expect(text).to.contain('educated_level'); + expect(text).to.contain('number_of_children'); + }); + + cy.contains('button', 'Cancel').click() + cy.contains('button', 'Advanced Filters').click() + cy.get('div[role="dialog"] div.MuiSelect-select').click() + cy.contains('li[role="option"]', 'Able bodied') + cy.contains('li[role="option"]', 'Educated level') + cy.contains('li[role="option"]', 'Number of children') + }); + }) + }) +}) + diff --git a/cypress/fixtures/individual-config-minimal.json b/cypress/fixtures/individual-config-minimal.json new file mode 100644 index 0000000..4134d6d --- /dev/null +++ b/cypress/fixtures/individual-config-minimal.json @@ -0,0 +1,3 @@ +{ + "individual_schema": "{\"$id\": \"https://example.com/beneficiares.schema.json\", \"type\": \"object\", \"title\": \"Record of beneficiares\", \"$schema\": \"http://json-schema.org/draft-04/schema#\", \"properties\": {\"able_bodied\": {\"type\": \"boolean\", \"description\": \"Flag determining whether someone is able bodied or not\"}, \"educated_level\": {\"type\": \"string\", \"description\": \"The level of person when it comes to the school/education/studies\"}, \"number_of_children\": {\"type\": \"integer\", \"description\": \"Number of children\"}}, \"description\": \"This document records the details beneficiares\"}" +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 66ea16e..4f29ea4 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,25 +1,19 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) \ No newline at end of file +Cypress.Commands.add('login', () => { + cy.visit('/'); + cy.fixture('cred').then((cred) => { + cy.get('input[type="text"]').type(cred.username) + cy.get('input[type="password"]').type(cred.password) + cy.get('button[type="submit"]').click() + cy.contains('Welcome Admin Admin!') + }) +}) + +Cypress.Commands.add('login_admin_interface', () => { + cy.visit('/api/admin'); + cy.fixture('cred').then((cred) => { + cy.get('input[type="text"]').type(cred.username) + cy.get('input[type="password"]').type(cred.password) + cy.get('input[type="submit"]').click() + cy.contains('Site administration').should('be.visible') + }) +}) From ae77ce24dfe9e8e99ed9cb61c033170f43947af2 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 7 Aug 2025 11:24:28 +0800 Subject: [PATCH 02/33] Add activity admin CRUD E2E tests --- cypress/e2e/admin.cy.js | 47 +++++++++++++++++++++++++++++++++++++ cypress/support/commands.js | 37 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index f3f6bd4..137a15a 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -61,5 +61,52 @@ describe('Django admin workflows', () => { }); }) }) + + it('Configures project activities', function () { + const activities = [ + 'E2E Tree Planting', + 'E2E River Cleaning', + 'E2E Soil Conservation', + 'E2E Extra', + ]; + const newName = 'E2E Water Conservation' + + cy.deleteActivities(activities.concat([newName])) + + // Create + activities.forEach(activityName => { + cy.contains('a', 'Activities').click() + cy.contains('a', 'Add Activity').click() + cy.get('input[name="name"]').type(activityName) + cy.get('input[value="Save"]').click() + cy.contains('td.field-name', activityName) + }) + + // Update + cy.contains('td.field-name', 'E2E Soil Conservation') + .parent('tr') + .find('th.field-id a') + .click(); + cy.get('input[name="name"]').clear().type(newName) + cy.get('input[value="Save"]').click() + cy.contains('td.field-name', newName) + .parent('tr') + .within(() => { + cy.get('td.field-name').should('contain', newName); + cy.get('td.field-version').should('have.text', '2'); + }); + + // Soft Delete + cy.contains('td.field-name', 'E2E Extra') + .parent('tr') + .find('th.field-id a') + .click(); + cy.get('input[name="is_deleted"]').check() + cy.get('input[value="Save"]').click() + cy.contains('td.field-name', 'E2E Extra') + .siblings('td.field-is_deleted') + .find('img[alt="True"]') + .should('exist'); + }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 4f29ea4..3178c98 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -17,3 +17,40 @@ Cypress.Commands.add('login_admin_interface', () => { cy.contains('Site administration').should('be.visible') }) }) + +Cypress.Commands.add('deleteActivities', (activityNames) => { + cy.visit('/api/admin/social_protection/activity/'); + cy.get('body').then(($body) => { + let checkedAny = false; + activityNames.forEach(activityName => { + if ($body.find(`td.field-name:contains("${activityName}")`).length) { + // Check the checkbox in the same row as the activity name + cy.contains('td.field-name', activityName) + .parent('tr') + .find('input[type="checkbox"]') + .check(); + checkedAny = true; + } + }); + + if (!checkedAny) { + Cypress.log({ + name: 'deleteActivity', + message: `Activities not found, nothing to delete`, + }); + return + } + + // Select the delete action and submit + cy.get('select[name="action"]').select('delete_selected'); + cy.get('button[type="submit"]').contains('Go').click(); + + // Confirm the deletion + cy.get('input[type="submit"][value*="Yes"]').click() + + // Verify deletion + activityNames.forEach(activityName => { + cy.contains('td.field-name', activityName).should('not.exist'); + }); + }); +}); From 8deb236f0d3587e3ca2ec3253dff312bb664420e Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 7 Aug 2025 11:58:17 +0800 Subject: [PATCH 03/33] Extract deleteModuleConfig e2e command for reuse --- cypress/e2e/admin.cy.js | 97 +++++++++++++++++-------------------- cypress/support/commands.js | 22 ++++++++- 2 files changed, 65 insertions(+), 54 deletions(-) diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index 137a15a..7506ac9 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -2,64 +2,55 @@ const path = require('path'); describe('Django admin workflows', () => { beforeEach(function () { - cy.login_admin_interface() + cy.loginAdminInterface() }); it('Configuring individual json schema reflects in advanced filters and upload template', function () { + cy.deleteModuleConfig("individual") + cy.contains('a', 'Module configurations').click() - cy.get('table#result_list').then(($table) => { - const individualLink = $table.find('a:contains("individual")') - - // Delete any existing individual config - if (individualLink.length) { - cy.wrap(individualLink).click() - cy.contains('a.deletelink', 'Delete').click() - cy.get('input[type="submit"][value*="Yes"]').click() - } - - // Create individual config using fixture config file - cy.contains('a', 'Add module configuration').click() - cy.get('input[name="module"]').type('individual') - cy.get('select[name="layer"]').select('backend') - cy.get('input[name="version"]').type(1) - - cy.fixture('individual-config-minimal.json').then((config) => { - const configString = JSON.stringify(config, null, 2); - cy.get('textarea[name="config"]') - .type(configString, { - parseSpecialCharSequences: false, - delay: 0 // Type faster - }); - - cy.get('input[value="Save"]').click() - - cy.visit('/individuals') - cy.contains('li', 'UPLOAD').click() - cy.contains('button', 'Template').click() - - const downloadedFilename = path.join( - Cypress.config('downloadsFolder'), - 'individual_upload_template.csv' - ); - cy.readFile(downloadedFilename, { timeout: 15000 }).should('exist'); - - cy.readFile(downloadedFilename) - .then(async (text) => { - expect(text.length).to.be.greaterThan(0); - expect(text).to.contain('able_bodied'); - expect(text).to.contain('educated_level'); - expect(text).to.contain('number_of_children'); - }); - - cy.contains('button', 'Cancel').click() - cy.contains('button', 'Advanced Filters').click() - cy.get('div[role="dialog"] div.MuiSelect-select').click() - cy.contains('li[role="option"]', 'Able bodied') - cy.contains('li[role="option"]', 'Educated level') - cy.contains('li[role="option"]', 'Number of children') - }); - }) + // Create individual config using fixture config file + cy.contains('a', 'Add module configuration').click() + cy.get('input[name="module"]').type('individual') + cy.get('select[name="layer"]').select('backend') + cy.get('input[name="version"]').type(1) + + cy.fixture('individual-config-minimal.json').then((config) => { + const configString = JSON.stringify(config, null, 2); + cy.get('textarea[name="config"]') + .type(configString, { + parseSpecialCharSequences: false, + delay: 0 // Type faster + }); + + cy.get('input[value="Save"]').click() + + cy.visit('/individuals') + cy.contains('li', 'UPLOAD').click() + cy.contains('button', 'Template').click() + + const downloadedFilename = path.join( + Cypress.config('downloadsFolder'), + 'individual_upload_template.csv' + ); + cy.readFile(downloadedFilename, { timeout: 15000 }).should('exist'); + + cy.readFile(downloadedFilename) + .then(async (text) => { + expect(text.length).to.be.greaterThan(0); + expect(text).to.contain('able_bodied'); + expect(text).to.contain('educated_level'); + expect(text).to.contain('number_of_children'); + }); + + cy.contains('button', 'Cancel').click() + cy.contains('button', 'Advanced Filters').click() + cy.get('div[role="dialog"] div.MuiSelect-select').click() + cy.contains('li[role="option"]', 'Able bodied') + cy.contains('li[role="option"]', 'Educated level') + cy.contains('li[role="option"]', 'Number of children') + }); }) it('Configures project activities', function () { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 3178c98..f87abdf 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -8,7 +8,7 @@ Cypress.Commands.add('login', () => { }) }) -Cypress.Commands.add('login_admin_interface', () => { +Cypress.Commands.add('loginAdminInterface', () => { cy.visit('/api/admin'); cy.fixture('cred').then((cred) => { cy.get('input[type="text"]').type(cred.username) @@ -18,6 +18,26 @@ Cypress.Commands.add('login_admin_interface', () => { }) }) +Cypress.Commands.add('deleteModuleConfig', (moduleName) => { + cy.visit('/api/admin/core/moduleconfiguration/'); + cy.get('table#result_list').then(($table) => { + const configLink = $table.find(`a:contains("${moduleName}")`) + + // Delete any existing module config with the given name + if (configLink.length) { + cy.wrap(configLink).click() + cy.contains('a.deletelink', 'Delete').click() + cy.get('input[type="submit"][value*="Yes"]').click() + cy.contains(`a:contains("${moduleName}")`).should('not.exist') + } else { + Cypress.log({ + name: 'deleteModuleConfig', + message: `Module Configuration named ${moduleName} not found, nothing to delete`, + }); + } + }) +}) + Cypress.Commands.add('deleteActivities', (activityNames) => { cy.visit('/api/admin/social_protection/activity/'); cy.get('body').then(($body) => { From 1d712af8186e9f8e62064ab5589c4ba1e0f214cd Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 7 Aug 2025 13:36:28 +0800 Subject: [PATCH 04/33] Add E2E test for menu config --- cypress/e2e/admin.cy.js | 67 +++++++++- cypress/fixtures/menu-config-sp.json | 183 +++++++++++++++++++++++++++ cypress/support/commands.js | 13 ++ 3 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 cypress/fixtures/menu-config-sp.json diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index 7506ac9..8d25cc7 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -5,6 +5,71 @@ describe('Django admin workflows', () => { cy.loginAdminInterface() }); + it('Configures menu', function () { + cy.deleteModuleConfig("fe-core") + cy.visit('/') + cy.get('div.MuiToolbar-root').should('exist') // default top toolbar menu + + cy.visit('/api/admin'); + cy.contains('a', 'Module configurations').click() + + // Create menu config using fixture config file + cy.contains('a', 'Add module configuration').click() + cy.get('input[name="module"]').type('fe-core') + cy.get('select[name="layer"]').select('frontend') + cy.get('input[name="version"]').type(1) + + cy.fixture('menu-config-sp.json').then((config) => { + const configString = JSON.stringify(config, null, 2); + cy.get('textarea[name="config"]') + .type(configString, { + parseSpecialCharSequences: false, + delay: 0 // Type faster + }); + cy.get('input[name="is_exposed"]').check() + + cy.get('input[value="Save"]').click() + + cy.visit('/') + cy.get('div.MuiDrawer-root').should('exist') // left drawer menu + + const expectedMenuItems = [ + 'Social Protection', + 'Dashboards', + 'Payments', + 'Grievance', + 'Tasks Management', + 'Administration', + ] + const expectedSubMenuItems = [ + 'Individuals', + 'Groups', + 'Import Data - API', + 'Programmes', + ] + cy.get('div.MuiDrawer-root').first().within(() => { + cy.shouldHaveMenuItemsInOrder(expectedMenuItems) + + cy.contains('div[role="button"]', 'Social Protection').click(); + + cy.contains('div[role="button"]', 'Social Protection') + .siblings('.MuiCollapse-root').within(() => { + cy.shouldHaveMenuItemsInOrder(expectedSubMenuItems) + + // Verify submenu persistence selected state + cy.contains('div[role="button"]', 'Programmes').click(); + cy.contains('div.Mui-selected[role="button"]', 'Programmes'); + + cy.contains('div[role="button"]', 'Individuals').click(); + cy.contains('div.Mui-selected[role="button"]', 'Individuals'); + + cy.visit('/front/benefitPlans') + cy.contains('div.Mui-selected[role="button"]', 'Programmes'); + }) + }); + }) + }) + it('Configuring individual json schema reflects in advanced filters and upload template', function () { cy.deleteModuleConfig("individual") @@ -26,7 +91,7 @@ describe('Django admin workflows', () => { cy.get('input[value="Save"]').click() - cy.visit('/individuals') + cy.visit('/front/individuals') cy.contains('li', 'UPLOAD').click() cy.contains('button', 'Template').click() diff --git a/cypress/fixtures/menu-config-sp.json b/cypress/fixtures/menu-config-sp.json new file mode 100644 index 0000000..69d31ee --- /dev/null +++ b/cypress/fixtures/menu-config-sp.json @@ -0,0 +1,183 @@ +{ + "menuLeft": true, + "menus": [ + { + "position": 1, + "id": "SocialProtectionMainMenu", + "name": "Social Protection", + "icon": "HowToReg", + "description": "Social Protection", + "submenus": [ + { + "position": 3, + "id": "individual.api_imports", + "icon": "Sync" + }, + { + "position": 4, + "id": "socialProtection.benefitPlans", + "icon": "Assignment" + }, + { + "position": 1, + "id": "individual.individuals" + }, + { + "position": 2, + "id": "individual.groups" + } + ] + }, + { + "position": 3, + "id": "PaymentManagement", + "name": "Payments", + "icon": "Payment", + "description": "PayrollNameMenu", + "submenus": [ + { + "position": 1, + "id": "legalAndFinance.invoices" + }, + { + "position": 2, + "id": "legalAndFinance.bills" + }, + { + "position": 3, + "id": "legalAndFinance.paymentPlans" + }, + { + "position": 4, + "id": "legalAndFinance.paymentCycles" + }, + { + "position": 5, + "id": "legalAndFinance.paymentPoint" + }, + { + "position": 6, + "id": "legalAndFinance.payrolls", + "icon": "AttachMoney" + }, + { + "position": 7, + "id": "legalAndFinance.payrollsPending" + }, + { + "position": 8, + "id": "legalAndFinance.payrollsApproved" + }, + { + "position": 9, + "id": "legalAndFinance.payrollsReconciled" + } + ] + }, + { + "position": 2, + "id": "OpenSearchReportsMenu", + "name": "Dashboard", + "icon": "Dashboard", + "description": "Main dashboard", + "submenus": [ + { + "position": 1, + "id": "openSearch.individualReports" + }, + { + "position": 2, + "id": "openSearch.groupReports" + }, + { + "position": 3, + "id": "openSearch.beneficiaryReports" + }, + { + "position": 4, + "id": "openSearch.dataUpdatesReports" + }, + { + "position": 5, + "id": "openSearch.grievanceReports" + }, + { + "position": 6, + "id": "openSearch.invoiceReports" + }, + { + "position": 7, + "id": "openSearch.paymentReports" + }, + { + "position": 8, + "id": "openSearch.openSearchConfig" + } + ] + }, + { + "position": 4, + "id": "GrievanceMainMenu", + "name": "Grievance", + "icon": "Feedback", + "description": "Grievance panel", + "submenus": [ + { + "position": 1, + "id": "grievance.add", + "icon": "ReportProblem" + }, + { + "position": 2, + "id": "grievance.grievances" + } + ] + }, + { + "position": 5, + "id": "TasksMainMenu", + "name":"Tasks Management", + "icon":"Assignment", + "description":"TaksMainMenu", + "submenus":[ + { + "position":1, + "id":"task.allTasks" + }, + { + "position":11, + "id":"admin.taskExecutionerGroups" + } + ] + }, + { + "position":6, + "id": "AdministrationMainMenu", + "name":"Administration", + "icon":"Person", + "description":"Administration", + "submenus":[ + { + "position":2, + "id":"admin.roleManagement" + }, + { + "position":1, + "id":"admin.users" + }, + { + "position":3, + "id": "admin.locations" + }, + { + "position":4, + "id": "profile.myProfile" + }, + { + "position":5, + "id":"profile.changePassword" + } + ] + } + ] +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f87abdf..e444573 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -38,6 +38,19 @@ Cypress.Commands.add('deleteModuleConfig', (moduleName) => { }) }) +Cypress.Commands.add('shouldHaveMenuItemsInOrder', (expectedMenuNames) => { + cy.get('div[role="button"]') + .filter(':visible') + .should(($buttons) => { + expect($buttons).to.have.length(expectedMenuNames.length); + + // Check each sub menu item text and order + expectedMenuNames.forEach((itemText, index) => { + expect($buttons.eq(index)).to.contain(itemText); + }); + }); +}) + Cypress.Commands.add('deleteActivities', (activityNames) => { cy.visit('/api/admin/social_protection/activity/'); cy.get('body').then(($body) => { From 6b3be4d95828a398b708a655156372342c26ada1 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 7 Aug 2025 19:07:19 +0800 Subject: [PATCH 05/33] Add E2E tests for program creation & deletion --- cypress/e2e/cash-trasfer.cy.js | 89 ++++++++++++++++++++++++++++++++++ cypress/support/commands.js | 59 ++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 cypress/e2e/cash-trasfer.cy.js diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js new file mode 100644 index 0000000..61c5435 --- /dev/null +++ b/cypress/e2e/cash-trasfer.cy.js @@ -0,0 +1,89 @@ +const getTodayFormatted = () => { + const today = new Date(); + const day = String(today.getDate()).padStart(2, '0'); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const year = today.getFullYear(); + return `${day}-${month}-${year}`; +}; + +describe('Cash transfer workflows', () => { + const testProgramNames = []; + + beforeEach(function () { + cy.login() + }); + + afterEach(() => { + testProgramNames.forEach(name => { + cy.deleteProgram(name) + }) + }) + + it('Creates and deletes an individual program', function () { + cy.visit('/front/benefitPlans'); + cy.get('[title="Create"] button').click() + + const programCode = 'E2EICP' + cy.enterMuiInput('Code', programCode) + + const programName = 'E2E Individual Cash Program' + cy.enterMuiInput('Name', programName) + + cy.contains('label', 'Date from') + .parent() + .click() + cy.contains('button', 'OK') + .click() + + cy.contains('label', 'Date to') + .parent() + .click() + cy.contains('button', 'OK') + .click() + + const maxBeneficiaries = "100" + cy.enterMuiInput('Max Beneficiaries', maxBeneficiaries) + + cy.contains('label', 'Type') + .parent() + .click() + const programType = "INDIVIDUAL" + cy.contains('li[role="option"]', programType) + .click() + + cy.get('[title="Save changes"] button').click() + + // Wait for creation to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains('Create programme').should('exist') + cy.contains('Failed to create').should('not.exist') + + // Ensure the created program gets cleaned up later + testProgramNames.push(programName); + + // Check program field values are persisted + cy.reload() + cy.assertMuiInput('Code', programCode) + cy.assertMuiInput('Name', programName) + const today = getTodayFormatted() + cy.assertMuiInput('Date from', today) + cy.assertMuiInput('Date to', today) + cy.assertMuiInput('Max Beneficiaries', maxBeneficiaries) + + // Check field values displayed in list view + cy.visit('/front/benefitPlans'); + cy.contains('tfoot', 'Rows Per Page') + cy.contains('td', programName).should('exist') + cy.contains('td', programName) + .parent('tr').within(() => { + cy.contains('td', programCode) + cy.contains('td', programType) + cy.contains('td', maxBeneficiaries) + cy.contains('td', new Date().toISOString().substring(0, 10)) + }) + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e444573..0c1d191 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -87,3 +87,62 @@ Cypress.Commands.add('deleteActivities', (activityNames) => { }); }); }); + +Cypress.Commands.add('deleteProgram', (programName) => { + cy.visit('/front/benefitPlans'); + cy.contains('tfoot', 'Rows Per Page') + + cy.get('body').then(($body) => { + const programRows = $body.find(`td:contains("${programName}")`).closest('tr'); + + if (programRows.length > 0) { + cy.log(`Found ${programRows.length} program(s) to delete`); + + programRows.each((_, row) => { + cy.wrap(row).within(() => { + // Find and click the Delete button in this row + cy.get('button[title="Delete"]') + .click(); + }); + + // Confirm deletion in dialog + cy.contains('button', 'Ok') + .should('be.visible') + .click(); + + // Wait for deletion to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + + // Verify deletion in expanded journal drawer + cy.get('.MuiDrawer-paperAnchorRight button') + .first() + .click(); + + cy.get('ul.MuiList-root li') + .first() + .should('contain', `Delete programme`); + // .should('contain', `Delete programme ${programName}`); //TODO: switch to this after fix + }); + } else { + Cypress.log({ + name: 'deleteProgram', + message: `No programs found with name "${programName}"`, + }); + } + }); +}); + +Cypress.Commands.add('enterMuiInput', (label, value) => { + cy.contains('label', label) + .siblings('.MuiInputBase-root') + .find('input') + .type(value); +}) + +Cypress.Commands.add('assertMuiInput', (label, value) => { + cy.contains('label', label) + .siblings('.MuiInputBase-root') + .find('input') + .should('be.visible') + .and('have.value', value); +}) From 3a5ed94c43f2d7577508bdb14c55d4932833634e Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 7 Aug 2025 20:09:49 +0800 Subject: [PATCH 06/33] fix: baseUrl setup to offer flexibility --- cypress.config.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index e7fcde0..b45291b 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -46,7 +46,7 @@ function waitForServerToStart(url) { module.exports = defineConfig({ e2e: { projectId: "q6gc25", // Cypress Cloud, needed for recording - baseUrl: 'http://localhost/front', + baseUrl: 'http://localhost', defaultCommandTimeout: 10000, taskTimeout: 300000, downloadsFolder: 'cypress/downloads', @@ -56,8 +56,7 @@ module.exports = defineConfig({ return fs.existsSync(serverFlagPath) }, completeSetup() { - const rootUrl = config.baseUrl.replace("/front", "") - const url = `${rootUrl}/api/graphql` + const url = `${config.baseUrl}/api/graphql` return waitForServerToStart(url) .then(() => { console.log('Server is ready!') From ebd7c32910b49975425dfa3dd236f9576597a452 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 19 Aug 2025 08:59:58 -0400 Subject: [PATCH 07/33] Add E2E tests for group programs creation and deletion --- cypress/e2e/cash-trasfer.cy.js | 75 +++++++++------------------------ cypress/support/commands.js | 77 +++++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 56 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 61c5435..6cf41ca 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -1,10 +1,3 @@ -const getTodayFormatted = () => { - const today = new Date(); - const day = String(today.getDate()).padStart(2, '0'); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const year = today.getFullYear(); - return `${day}-${month}-${year}`; -}; describe('Cash transfer workflows', () => { const testProgramNames = []; @@ -20,70 +13,42 @@ describe('Cash transfer workflows', () => { }) it('Creates and deletes an individual program', function () { - cy.visit('/front/benefitPlans'); - cy.get('[title="Create"] button').click() - const programCode = 'E2EICP' - cy.enterMuiInput('Code', programCode) - const programName = 'E2E Individual Cash Program' - cy.enterMuiInput('Name', programName) - - cy.contains('label', 'Date from') - .parent() - .click() - cy.contains('button', 'OK') - .click() + const maxBeneficiaries = "100" + const programType = "INDIVIDUAL" - cy.contains('label', 'Date to') - .parent() - .click() - cy.contains('button', 'OK') - .click() + cy.createProgram(programCode, programName, maxBeneficiaries, programType) - const maxBeneficiaries = "100" - cy.enterMuiInput('Max Beneficiaries', maxBeneficiaries) + // Ensure the created program gets cleaned up later + testProgramNames.push(programName); - cy.contains('label', 'Type') - .parent() - .click() - const programType = "INDIVIDUAL" - cy.contains('li[role="option"]', programType) - .click() + // Check program field values are persisted + cy.reload() + cy.checkProgramFieldValues(programCode, programName, maxBeneficiaries, programType) - cy.get('[title="Save changes"] button').click() + // Check field values displayed in list view + cy.visit('/front/benefitPlans'); + cy.checkProgramFieldValuesInListView(programCode, programName, maxBeneficiaries, programType) + }) - // Wait for creation to complete - cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') - cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + it('Creates and deletes an household program', function () { + const programCode = 'E2EGCP' + const programName = 'E2E Group Cash Program' + const maxBeneficiaries = "200" + const programType = "GROUP" - // Check last journal message - cy.get('ul.MuiList-root li').first().click() - cy.contains('Create programme').should('exist') - cy.contains('Failed to create').should('not.exist') + cy.createProgram(programCode, programName, maxBeneficiaries, programType) // Ensure the created program gets cleaned up later testProgramNames.push(programName); // Check program field values are persisted cy.reload() - cy.assertMuiInput('Code', programCode) - cy.assertMuiInput('Name', programName) - const today = getTodayFormatted() - cy.assertMuiInput('Date from', today) - cy.assertMuiInput('Date to', today) - cy.assertMuiInput('Max Beneficiaries', maxBeneficiaries) + cy.checkProgramFieldValues(programCode, programName, maxBeneficiaries, programType) // Check field values displayed in list view cy.visit('/front/benefitPlans'); - cy.contains('tfoot', 'Rows Per Page') - cy.contains('td', programName).should('exist') - cy.contains('td', programName) - .parent('tr').within(() => { - cy.contains('td', programCode) - cy.contains('td', programType) - cy.contains('td', maxBeneficiaries) - cy.contains('td', new Date().toISOString().substring(0, 10)) - }) + cy.checkProgramFieldValuesInListView(programCode, programName, maxBeneficiaries, programType) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 0c1d191..4d8e80f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,5 +1,13 @@ +const getTodayFormatted = () => { + const today = new Date(); + const day = String(today.getDate()).padStart(2, '0'); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const year = today.getFullYear(); + return `${day}-${month}-${year}`; +}; + Cypress.Commands.add('login', () => { - cy.visit('/'); + cy.visit('/front'); cy.fixture('cred').then((cred) => { cy.get('input[type="text"]').type(cred.username) cy.get('input[type="password"]').type(cred.password) @@ -132,6 +140,73 @@ Cypress.Commands.add('deleteProgram', (programName) => { }); }); +Cypress.Commands.add('createProgram', (programCode, programName, maxBeneficiaries, programType) => { + cy.visit('/front/benefitPlans'); + cy.get('[title="Create"] button').click() + + cy.enterMuiInput('Code', programCode) + + cy.enterMuiInput('Name', programName) + + cy.contains('label', 'Date from') + .parent() + .click() + cy.contains('button', 'OK') + .click() + + cy.contains('label', 'Date to') + .parent() + .click() + cy.contains('button', 'OK') + .click() + + cy.enterMuiInput('Max Beneficiaries', maxBeneficiaries) + + cy.contains('label', 'Type') + .parent() + .click() + cy.contains('li[role="option"]', programType) + .click() + + cy.get('[title="Save changes"] button').click() + + // Wait for creation to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains('Create programme').should('exist') + cy.contains('Failed to create').should('not.exist') +}) + +Cypress.Commands.add( + 'checkProgramFieldValues', + (programCode, programName, maxBeneficiaries, programType) => { + + cy.assertMuiInput('Code', programCode) + cy.assertMuiInput('Name', programName) + const today = getTodayFormatted() + cy.assertMuiInput('Date from', today) + cy.assertMuiInput('Date to', today) + cy.assertMuiInput('Max Beneficiaries', maxBeneficiaries) +}) + +Cypress.Commands.add( + 'checkProgramFieldValuesInListView', + (programCode, programName, maxBeneficiaries, programType) => { + + cy.contains('tfoot', 'Rows Per Page') + cy.contains('td', programName).should('exist') + cy.contains('td', programName) + .parent('tr').within(() => { + cy.contains('td', programCode) + cy.contains('td', programType) + cy.contains('td', maxBeneficiaries) + cy.contains('td', new Date().toISOString().substring(0, 10)) + }) +}) + Cypress.Commands.add('enterMuiInput', (label, value) => { cy.contains('label', label) .siblings('.MuiInputBase-root') From c26e35cd67de8cd5aec2a20eb77e4c3a3845ebcc Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 21 Aug 2025 08:26:23 -0400 Subject: [PATCH 08/33] Add individual program update E2E test --- cypress/e2e/admin.cy.js | 69 +++++++------------ cypress/e2e/cash-trasfer.cy.js | 62 +++++++++++++++++ .../fixtures/social-protection-config.json | 3 + cypress/support/commands.js | 52 +++++++++++--- 4 files changed, 133 insertions(+), 53 deletions(-) create mode 100644 cypress/fixtures/social-protection-config.json diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index 8d25cc7..16f6861 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -71,51 +71,32 @@ describe('Django admin workflows', () => { }) it('Configuring individual json schema reflects in advanced filters and upload template', function () { - cy.deleteModuleConfig("individual") - - cy.contains('a', 'Module configurations').click() - - // Create individual config using fixture config file - cy.contains('a', 'Add module configuration').click() - cy.get('input[name="module"]').type('individual') - cy.get('select[name="layer"]').select('backend') - cy.get('input[name="version"]').type(1) - - cy.fixture('individual-config-minimal.json').then((config) => { - const configString = JSON.stringify(config, null, 2); - cy.get('textarea[name="config"]') - .type(configString, { - parseSpecialCharSequences: false, - delay: 0 // Type faster - }); - - cy.get('input[value="Save"]').click() - - cy.visit('/front/individuals') - cy.contains('li', 'UPLOAD').click() - cy.contains('button', 'Template').click() - - const downloadedFilename = path.join( - Cypress.config('downloadsFolder'), - 'individual_upload_template.csv' - ); - cy.readFile(downloadedFilename, { timeout: 15000 }).should('exist'); - - cy.readFile(downloadedFilename) - .then(async (text) => { - expect(text.length).to.be.greaterThan(0); - expect(text).to.contain('able_bodied'); - expect(text).to.contain('educated_level'); - expect(text).to.contain('number_of_children'); - }); + cy.setModuleConfig('individual', 'individual-config-minimal.json') + + cy.visit('/front/individuals') + cy.contains('li', 'UPLOAD').click() + cy.contains('button', 'Template').click() + + const downloadedFilename = path.join( + Cypress.config('downloadsFolder'), + 'individual_upload_template.csv' + ); + cy.readFile(downloadedFilename, { timeout: 15000 }).should('exist'); + + cy.readFile(downloadedFilename) + .then(async (text) => { + expect(text.length).to.be.greaterThan(0); + expect(text).to.contain('able_bodied'); + expect(text).to.contain('educated_level'); + expect(text).to.contain('number_of_children'); + }); - cy.contains('button', 'Cancel').click() - cy.contains('button', 'Advanced Filters').click() - cy.get('div[role="dialog"] div.MuiSelect-select').click() - cy.contains('li[role="option"]', 'Able bodied') - cy.contains('li[role="option"]', 'Educated level') - cy.contains('li[role="option"]', 'Number of children') - }); + cy.contains('button', 'Cancel').click() + cy.contains('button', 'Advanced Filters').click() + cy.get('div[role="dialog"] div.MuiSelect-select').click() + cy.contains('li[role="option"]', 'Able bodied') + cy.contains('li[role="option"]', 'Educated level') + cy.contains('li[role="option"]', 'Number of children') }) it('Configures project activities', function () { diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 6cf41ca..6da8ab8 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -32,6 +32,68 @@ describe('Cash transfer workflows', () => { cy.checkProgramFieldValuesInListView(programCode, programName, maxBeneficiaries, programType) }) + it('Updates an individual program', function () { + // Disable maker checker + cy.loginAdminInterface() + cy.setModuleConfig('social_protection', 'social-protection-config.json') + + const programCode = 'E2EICPU' + const programName = 'E2E Individual Cash Program Updated' + const maxBeneficiaries = "100" + const programType = "INDIVIDUAL" + + cy.createProgram(programCode, programName, maxBeneficiaries, programType) + + // Ensure the created program gets cleaned up later + testProgramNames.push(programName); + + const updatedProgramCode = 'E2EICP42' + const updatedMaxBeneficiaries = "111" + const updatedInstitution = "Social Protection Agency" + const updatedDescription = "Foo bar baz" + + // Go back into the list page to find the program & edit + cy.visit('/front/benefitPlans'); + cy.contains('tfoot', 'Rows Per Page') + cy.contains('td', programName) + .parent('tr').within(() => { + // click on edit button + cy.get('a.MuiIconButton-root').click() + }) + + cy.assertMuiInput('Code', programCode) + cy.enterMuiInput('Code', updatedProgramCode) + cy.assertMuiInput('Code', updatedProgramCode) + + cy.enterMuiInput('Max Beneficiaries', updatedMaxBeneficiaries) + + cy.enterMuiInput('Institution', updatedInstitution) + + cy.enterMuiInput('Description', updatedDescription, 'textarea') + + cy.get('[title="Save changes"] button').click() + + // Wait for update to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains('Update programme').should('exist') + cy.contains('Failed to update').should('not.exist') + + // Check program field values are persisted + cy.reload() + cy.checkProgramFieldValues( + updatedProgramCode, + programName, + updatedMaxBeneficiaries, + programType, + updatedInstitution, + updatedDescription, + ) + }) + it('Creates and deletes an household program', function () { const programCode = 'E2EGCP' const programName = 'E2E Group Cash Program' diff --git a/cypress/fixtures/social-protection-config.json b/cypress/fixtures/social-protection-config.json new file mode 100644 index 0000000..180a3ab --- /dev/null +++ b/cypress/fixtures/social-protection-config.json @@ -0,0 +1,3 @@ +{ + "gql_check_benefit_plan_update": false +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 4d8e80f..a507abf 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -98,7 +98,7 @@ Cypress.Commands.add('deleteActivities', (activityNames) => { Cypress.Commands.add('deleteProgram', (programName) => { cy.visit('/front/benefitPlans'); - cy.contains('tfoot', 'Rows Per Page') + cy.contains('tfoot', 'Rows Per Page').should('be.visible') cy.get('body').then(($body) => { const programRows = $body.find(`td:contains("${programName}")`).closest('tr'); @@ -181,15 +181,23 @@ Cypress.Commands.add('createProgram', (programCode, programName, maxBeneficiarie }) Cypress.Commands.add( - 'checkProgramFieldValues', - (programCode, programName, maxBeneficiaries, programType) => { - + 'checkProgramFieldValues', + ( + programCode, + programName, + maxBeneficiaries, + programType, + institution='', + description='', + ) => { cy.assertMuiInput('Code', programCode) cy.assertMuiInput('Name', programName) const today = getTodayFormatted() cy.assertMuiInput('Date from', today) cy.assertMuiInput('Date to', today) cy.assertMuiInput('Max Beneficiaries', maxBeneficiaries) + cy.assertMuiInput('Institution', institution) + cy.assertMuiInput('Description', description, 'textarea') }) Cypress.Commands.add( @@ -207,17 +215,43 @@ Cypress.Commands.add( }) }) -Cypress.Commands.add('enterMuiInput', (label, value) => { +Cypress.Commands.add('enterMuiInput', (label, value, inputTag='input') => { cy.contains('label', label) .siblings('.MuiInputBase-root') - .find('input') - .type(value); + .find(inputTag) + .first() + .clear({force: true}) + .type(value, {force: true}); }) -Cypress.Commands.add('assertMuiInput', (label, value) => { +Cypress.Commands.add('assertMuiInput', (label, value, inputTag='input') => { cy.contains('label', label) .siblings('.MuiInputBase-root') - .find('input') + .find(inputTag) .should('be.visible') .and('have.value', value); }) + +Cypress.Commands.add('setModuleConfig', (moduleName, configFixtureFile) => { + cy.deleteModuleConfig(moduleName) + + cy.contains('a', 'Module configurations').click() + + // Create module config using fixture config file + cy.contains('a', 'Add module configuration').click() + cy.get('input[name="module"]').type(moduleName) + cy.get('select[name="layer"]').select('backend') + cy.get('input[name="version"]').type(1) + + cy.fixture(configFixtureFile).then((config) => { + const configString = JSON.stringify(config, null, 2); + cy.get('textarea[name="config"]') + .type(configString, { + parseSpecialCharSequences: false, + delay: 0 // Type faster + }); + + cy.get('input[value="Save"]').click() + cy.contains("was added successfully") + }) +}) From a48042c382bab8040ac750651a0fb1512d2af579 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 21 Aug 2025 09:31:22 -0400 Subject: [PATCH 09/33] Add group program update E2E test --- cypress/e2e/cash-trasfer.cy.js | 64 +++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 6da8ab8..61eb43f 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -96,7 +96,7 @@ describe('Cash transfer workflows', () => { it('Creates and deletes an household program', function () { const programCode = 'E2EGCP' - const programName = 'E2E Group Cash Program' + const programName = 'E2E Household Cash Program' const maxBeneficiaries = "200" const programType = "GROUP" @@ -113,4 +113,66 @@ describe('Cash transfer workflows', () => { cy.visit('/front/benefitPlans'); cy.checkProgramFieldValuesInListView(programCode, programName, maxBeneficiaries, programType) }) + + it('Updates a household program', function () { + // Disable maker checker + cy.loginAdminInterface() + cy.setModuleConfig('social_protection', 'social-protection-config.json') + + const programCode = 'E2EGCPU' + const programName = 'E2E Household Cash Program Updated' + const maxBeneficiaries = "200" + const programType = "GROUP" + + cy.createProgram(programCode, programName, maxBeneficiaries, programType) + + // Ensure the created program gets cleaned up later + testProgramNames.push(programName); + + const updatedProgramCode = 'E2EGCP42' + const updatedMaxBeneficiaries = "222" + const updatedInstitution = "Family Support Services" + const updatedDescription = "A functional program" + + // Go back into the list page to find the program & edit + cy.visit('/front/benefitPlans'); + cy.contains('tfoot', 'Rows Per Page') + cy.contains('td', programName) + .parent('tr').within(() => { + // click on edit button + cy.get('a.MuiIconButton-root').click() + }) + + cy.assertMuiInput('Code', programCode) + cy.enterMuiInput('Code', updatedProgramCode) + cy.assertMuiInput('Code', updatedProgramCode) + + cy.enterMuiInput('Max Beneficiaries', updatedMaxBeneficiaries) + + cy.enterMuiInput('Institution', updatedInstitution) + + cy.enterMuiInput('Description', updatedDescription, 'textarea') + + cy.get('[title="Save changes"] button').click() + + // Wait for update to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains('Update programme').should('exist') + cy.contains('Failed to update').should('not.exist') + + // Check program field values are persisted + cy.reload() + cy.checkProgramFieldValues( + updatedProgramCode, + programName, + updatedMaxBeneficiaries, + programType, + updatedInstitution, + updatedDescription, + ) + }) }) From ff992235e32a9cdaec41db64f0d1387d56853255 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 21 Aug 2025 10:41:26 -0400 Subject: [PATCH 10/33] Set HOSTS to fix the CSRF issue --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bb43bf7..6ba8a91 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,6 +44,7 @@ jobs: echo 'DEMO_DATASET=true' >> .env echo 'BE_TAG=develop' >> .env echo 'FE_TAG=develop' >> .env + echo 'HOSTS=localhost' >> .env cp .env.cache.example .env.cache cp .env.openSearch.example .env.openSearch cp .env.database.example .env.database From d214d823b7a8e0398a4a8c0abaf3e7e7ab99c9f8 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 4 Sep 2025 14:10:34 -0400 Subject: [PATCH 11/33] Adjust E2E tests and config such that it can be used with or without language pack --- README.md | 9 ++++-- cypress.config.js | 4 ++- cypress/e2e/admin.cy.js | 4 +-- cypress/e2e/auth.cy.js | 2 +- cypress/e2e/cash-trasfer.cy.js | 8 ++++-- cypress/support/commands.js | 50 ++++++++++++++++++++++------------ 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 32142cf..4d34497 100644 --- a/README.md +++ b/README.md @@ -104,5 +104,10 @@ This can be useful for local development or verifying a staging deployment, for example, if the target host is localhost:3000, pass it into the corresponding test command with `-- --config "baseUrl=http://localhost:3000"`: -- Headless: `npx cypress run --config "baseUrl=http://localhost:3000"` -- Headed: `npx cypress open --config "baseUrl=http://localhost:3000"` +Additionally, if you are using social protection specific language pack, +e.g. benefit plan would be called programme, you can pass in +`--env useSocialProtectionLanguagePack=true` + +- Headless: `npx cypress run --config "baseUrl=http://localhost:3000" --env useSocialProtectionLanguagePack=true` +- Headed: `npx cypress open --config "baseUrl=http://localhost:3000" --env useSocialProtectionLanguagePack=true` + diff --git a/cypress.config.js b/cypress.config.js index b45291b..a7c8680 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -44,10 +44,12 @@ function waitForServerToStart(url) { } module.exports = defineConfig({ + viewportWidth: 1280, + viewportHeight: 670, e2e: { projectId: "q6gc25", // Cypress Cloud, needed for recording baseUrl: 'http://localhost', - defaultCommandTimeout: 10000, + defaultCommandTimeout: 15000, taskTimeout: 300000, downloadsFolder: 'cypress/downloads', setupNodeEvents(on, config) { diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index 16f6861..b83117c 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -7,7 +7,7 @@ describe('Django admin workflows', () => { it('Configures menu', function () { cy.deleteModuleConfig("fe-core") - cy.visit('/') + cy.visit('/front') cy.get('div.MuiToolbar-root').should('exist') // default top toolbar menu cy.visit('/api/admin'); @@ -30,7 +30,7 @@ describe('Django admin workflows', () => { cy.get('input[value="Save"]').click() - cy.visit('/') + cy.visit('/front') cy.get('div.MuiDrawer-root').should('exist') // left drawer menu const expectedMenuItems = [ diff --git a/cypress/e2e/auth.cy.js b/cypress/e2e/auth.cy.js index 1f22020..f094813 100644 --- a/cypress/e2e/auth.cy.js +++ b/cypress/e2e/auth.cy.js @@ -1,6 +1,6 @@ describe('Unauthenticated', () => { it('Shows the login screen', () => { - cy.visit('') + cy.visit('/') cy.contains('Username') cy.contains('Password') cy.contains('button', 'Log In') diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 61eb43f..9eaa264 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -1,6 +1,7 @@ +const programTerm = Cypress.env('useSocialProtectionLanguagePack') ? 'programme' : 'benefit plan'; describe('Cash transfer workflows', () => { - const testProgramNames = []; + let testProgramNames = []; beforeEach(function () { cy.login() @@ -10,6 +11,7 @@ describe('Cash transfer workflows', () => { testProgramNames.forEach(name => { cy.deleteProgram(name) }) + testProgramNames = [] }) it('Creates and deletes an individual program', function () { @@ -79,7 +81,7 @@ describe('Cash transfer workflows', () => { // Check last journal message cy.get('ul.MuiList-root li').first().click() - cy.contains('Update programme').should('exist') + cy.contains(`Update ${programTerm}`).should('exist') cy.contains('Failed to update').should('not.exist') // Check program field values are persisted @@ -161,7 +163,7 @@ describe('Cash transfer workflows', () => { // Check last journal message cy.get('ul.MuiList-root li').first().click() - cy.contains('Update programme').should('exist') + cy.contains(`Update ${programTerm}`).should('exist') cy.contains('Failed to update').should('not.exist') // Check program field values are persisted diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a507abf..c223f9f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,3 +1,5 @@ +const programTerm = Cypress.env('useSocialProtectionLanguagePack') ? 'programme' : 'benefit plan'; + const getTodayFormatted = () => { const today = new Date(); const day = String(today.getDate()).padStart(2, '0'); @@ -28,23 +30,32 @@ Cypress.Commands.add('loginAdminInterface', () => { Cypress.Commands.add('deleteModuleConfig', (moduleName) => { cy.visit('/api/admin/core/moduleconfiguration/'); - cy.get('table#result_list').then(($table) => { - const configLink = $table.find(`a:contains("${moduleName}")`) - - // Delete any existing module config with the given name - if (configLink.length) { - cy.wrap(configLink).click() - cy.contains('a.deletelink', 'Delete').click() - cy.get('input[type="submit"][value*="Yes"]').click() - cy.contains(`a:contains("${moduleName}")`).should('not.exist') - } else { + + cy.get('body').then(($body) => { + if ($body.text().includes('0 module configurations')) { Cypress.log({ name: 'deleteModuleConfig', - message: `Module Configuration named ${moduleName} not found, nothing to delete`, + message: 'No module configurations found, skipping deletion.', + }); + } else { + cy.get('table#result_list').then(($table) => { + const configLink = $table.find(`a:contains("${moduleName}")`); + + if (configLink.length) { + cy.wrap(configLink).click(); + cy.contains('a.deletelink', 'Delete').click(); + cy.get('input[type="submit"][value*="Yes"]').click(); + cy.contains(`a:contains("${moduleName}")`).should('not.exist'); + } else { + Cypress.log({ + name: 'deleteModuleConfig', + message: `Module Configuration named ${moduleName} not found, nothing to delete.`, + }); + } }); } - }) -}) + }); +}); Cypress.Commands.add('shouldHaveMenuItemsInOrder', (expectedMenuNames) => { cy.get('div[role="button"]') @@ -110,7 +121,7 @@ Cypress.Commands.add('deleteProgram', (programName) => { cy.wrap(row).within(() => { // Find and click the Delete button in this row cy.get('button[title="Delete"]') - .click(); + .click({force: true}); }); // Confirm deletion in dialog @@ -128,8 +139,13 @@ Cypress.Commands.add('deleteProgram', (programName) => { cy.get('ul.MuiList-root li') .first() - .should('contain', `Delete programme`); - // .should('contain', `Delete programme ${programName}`); //TODO: switch to this after fix + .should('contain', `Delete ${programTerm}`); + // .should('contain', `Delete ${programTerm} ${programName}`); //TODO: switch to this after fix + + // Close journal drawer + cy.get('.MuiDrawer-paperAnchorRight button') + .first() + .click(); }); } else { Cypress.log({ @@ -176,7 +192,7 @@ Cypress.Commands.add('createProgram', (programCode, programName, maxBeneficiarie // Check last journal message cy.get('ul.MuiList-root li').first().click() - cy.contains('Create programme').should('exist') + cy.contains(`Create ${programTerm}`).should('exist') cy.contains('Failed to create').should('not.exist') }) From 98b734f55ebedb473cd74f9afe4508310d7499e1 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 15:30:57 -0400 Subject: [PATCH 12/33] Disable sonar coverage check on this repo As it is all Dockerfiles and configs --- sonar-project.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sonar-project.properties b/sonar-project.properties index b695e1d..26a16d2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,3 +4,5 @@ sonar.projectName=openimis-openimis-dist_dkr sonar.sources=./ sonar.sourceEncoding=UTF-8 + +sonar.coverage.exclusions=** From fd6344d0b1f559f3703e9a13fc8066f67c0ecedc Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 15:34:53 -0400 Subject: [PATCH 13/33] Exclude test code from duplication analysis --- sonar-project.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/sonar-project.properties b/sonar-project.properties index 26a16d2..625919a 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,3 +6,4 @@ sonar.sources=./ sonar.sourceEncoding=UTF-8 sonar.coverage.exclusions=** +sonar.cpd.exclusions=**/cypress/** From 6e91d12b4ea419bd22df9e3b92f96e142a137757 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 15:45:21 -0400 Subject: [PATCH 14/33] Move to the maintained version of sonar action --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6ba8a91..11a9ea8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: with: fetch-depth: 0 - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master + uses: SonarSource/sonarqube-scan-action@v5.3.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From ea1db8fd44a984623ba38b46a54851ed2ff30291 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 16:09:07 -0400 Subject: [PATCH 15/33] Extract language pack check into util func & use it in E2E tests --- cypress/e2e/admin.cy.js | 11 +++++++---- cypress/e2e/cash-trasfer.cy.js | 6 +++--- cypress/support/commands.js | 8 ++++---- cypress/support/utils.js | 7 +++++++ 4 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 cypress/support/utils.js diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index b83117c..9b45c70 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -1,3 +1,5 @@ +import { getProgramTerm, capitalizeWords } from '../support/utils'; + const path = require('path'); describe('Django admin workflows', () => { @@ -41,11 +43,12 @@ describe('Django admin workflows', () => { 'Tasks Management', 'Administration', ] + const programMenuText = capitalizeWords(getProgramTerm()) + 's' const expectedSubMenuItems = [ 'Individuals', 'Groups', 'Import Data - API', - 'Programmes', + programMenuText, ] cy.get('div.MuiDrawer-root').first().within(() => { cy.shouldHaveMenuItemsInOrder(expectedMenuItems) @@ -57,14 +60,14 @@ describe('Django admin workflows', () => { cy.shouldHaveMenuItemsInOrder(expectedSubMenuItems) // Verify submenu persistence selected state - cy.contains('div[role="button"]', 'Programmes').click(); - cy.contains('div.Mui-selected[role="button"]', 'Programmes'); + cy.contains('div[role="button"]', programMenuText).click(); + cy.contains('div.Mui-selected[role="button"]', programMenuText); cy.contains('div[role="button"]', 'Individuals').click(); cy.contains('div.Mui-selected[role="button"]', 'Individuals'); cy.visit('/front/benefitPlans') - cy.contains('div.Mui-selected[role="button"]', 'Programmes'); + cy.contains('div.Mui-selected[role="button"]', programMenuText); }) }); }) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 9eaa264..6d2588b 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -1,4 +1,4 @@ -const programTerm = Cypress.env('useSocialProtectionLanguagePack') ? 'programme' : 'benefit plan'; +import { getProgramTerm } from '../support/utils'; describe('Cash transfer workflows', () => { let testProgramNames = []; @@ -81,7 +81,7 @@ describe('Cash transfer workflows', () => { // Check last journal message cy.get('ul.MuiList-root li').first().click() - cy.contains(`Update ${programTerm}`).should('exist') + cy.contains(`Update ${getProgramTerm()}`).should('exist') cy.contains('Failed to update').should('not.exist') // Check program field values are persisted @@ -163,7 +163,7 @@ describe('Cash transfer workflows', () => { // Check last journal message cy.get('ul.MuiList-root li').first().click() - cy.contains(`Update ${programTerm}`).should('exist') + cy.contains(`Update ${getProgramTerm()}`).should('exist') cy.contains('Failed to update').should('not.exist') // Check program field values are persisted diff --git a/cypress/support/commands.js b/cypress/support/commands.js index c223f9f..0418a8e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,4 +1,4 @@ -const programTerm = Cypress.env('useSocialProtectionLanguagePack') ? 'programme' : 'benefit plan'; +import { getProgramTerm } from '../support/utils'; const getTodayFormatted = () => { const today = new Date(); @@ -139,8 +139,8 @@ Cypress.Commands.add('deleteProgram', (programName) => { cy.get('ul.MuiList-root li') .first() - .should('contain', `Delete ${programTerm}`); - // .should('contain', `Delete ${programTerm} ${programName}`); //TODO: switch to this after fix + .should('contain', `Delete ${getProgramTerm()}`); + // .should('contain', `Delete ${getProgramTerm()} ${programName}`); //TODO: switch to this after fix // Close journal drawer cy.get('.MuiDrawer-paperAnchorRight button') @@ -192,7 +192,7 @@ Cypress.Commands.add('createProgram', (programCode, programName, maxBeneficiarie // Check last journal message cy.get('ul.MuiList-root li').first().click() - cy.contains(`Create ${programTerm}`).should('exist') + cy.contains(`Create ${getProgramTerm()}`).should('exist') cy.contains('Failed to create').should('not.exist') }) diff --git a/cypress/support/utils.js b/cypress/support/utils.js new file mode 100644 index 0000000..952a513 --- /dev/null +++ b/cypress/support/utils.js @@ -0,0 +1,7 @@ +export function getProgramTerm() { + return Cypress.env('useSocialProtectionLanguagePack') ? 'programme' : 'benefit plan'; +} + +export function capitalizeWords(str) { + return str.replace(/\b\w/g, char => char.toUpperCase()); +} From cf3f696a7f05acc4324ca2deac70f88d49a6dd35 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 16:11:23 -0400 Subject: [PATCH 16/33] Add individual & household csv import E2E test --- cypress/e2e/cash-trasfer.cy.js | 34 ++++++ .../fixtures/individual-config-minimal.json | 4 + cypress/fixtures/individuals.csv | 101 ++++++++++++++++++ cypress/support/commands.js | 20 ++++ cypress/support/e2e.js | 1 + package-lock.json | 17 ++- package.json | 3 +- 7 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 cypress/fixtures/individuals.csv diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 6d2588b..291caf3 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -177,4 +177,38 @@ describe('Cash transfer workflows', () => { updatedDescription, ) }) + + it('Imports individuals and groups', function () { + cy.loginAdminInterface() + cy.setModuleConfig('individual', 'individual-config-minimal.json') + + cy.visit('/front/groups') + cy.getItemCount('Group').as('initialGroupCount'); + + cy.visit('/front/individuals') + cy.getItemCount('Individual').as('initialIndividualCount'); + + cy.contains('li', 'UPLOAD').click() + + cy.get('input[type="file"]').attachFile('individuals.csv'); + + cy.chooseMuiSelect('Workflow', 'Python Import Individuals') + cy.contains('button', 'Upload Individuals').click(); + + cy.contains('button', 'Upload Individuals').should('not.exist') + + cy.visit('/front/individuals') + cy.getItemCount("Individual").then(newCount => { + cy.get('@initialIndividualCount').then(initial => { + expect(newCount - initial).to.eq(100); + }); + }); + + cy.visit('/front/groups') + cy.getItemCount("Group").then(newCount => { + cy.get('@initialGroupCount').then(initial => { + expect(newCount - initial).to.eq(20); + }); + }); + }) }) diff --git a/cypress/fixtures/individual-config-minimal.json b/cypress/fixtures/individual-config-minimal.json index 4134d6d..81690cf 100644 --- a/cypress/fixtures/individual-config-minimal.json +++ b/cypress/fixtures/individual-config-minimal.json @@ -1,3 +1,7 @@ { + "enable_maker_checker_for_individual_upload": false, + "enable_maker_checker_for_individual_update": false, + "enable_maker_checker_for_group_upload": false, + "enable_maker_checker_for_group_update": false, "individual_schema": "{\"$id\": \"https://example.com/beneficiares.schema.json\", \"type\": \"object\", \"title\": \"Record of beneficiares\", \"$schema\": \"http://json-schema.org/draft-04/schema#\", \"properties\": {\"able_bodied\": {\"type\": \"boolean\", \"description\": \"Flag determining whether someone is able bodied or not\"}, \"educated_level\": {\"type\": \"string\", \"description\": \"The level of person when it comes to the school/education/studies\"}, \"number_of_children\": {\"type\": \"integer\", \"description\": \"Number of children\"}}, \"description\": \"This document records the details beneficiares\"}" } diff --git a/cypress/fixtures/individuals.csv b/cypress/fixtures/individuals.csv new file mode 100644 index 0000000..9ef46a4 --- /dev/null +++ b/cypress/fixtures/individuals.csv @@ -0,0 +1,101 @@ +first_name,last_name,dob,group_code,recipient_info,individual_role,location_name,location_code,able_bodied,educated_level,number_of_children +Michael,Lloyd,1980-01-20,QMLJIL,1,HEAD,,,True,primary,5 +Joshua,Moore,1956-06-04,QMLJIL,0,SON,,,True,primary,4 +Kevin,Wolf,1964-06-19,QMLJIL,0,NOT RELATED,,,False,none,2 +Melinda,Smith,1957-10-24,QMLJIL,0,FATHER,,,False,tertiary,2 +Julie,Mullins,2005-11-22,QMLJIL,0,DAUGHTER,,,False,secondary,7 +Richard,Wright,1975-12-12,RQAEII,1,HEAD,Holobo,R1D2M1V1,True,none,10 +Mark,Chambers,1938-12-23,RQAEII,0,FATHER,Holobo,R1D2M1V1,True,secondary,7 +John,Quinn,1962-11-23,RQAEII,0,MOTHER,Holobo,R1D2M1V1,True,tertiary,7 +Christy,Richardson,1982-04-01,RQAEII,0,GRANDMOTHER,Holobo,R1D2M1V1,True,none,0 +Derrick,Perry,1956-04-30,RQAEII,0,OTHER RELATIVE,Holobo,R1D2M1V1,False,primary,0 +Desiree,Smith,1954-01-22,TGOXBP,1,HEAD,,,True,tertiary,2 +Nicholas,Adams,1962-03-22,TGOXBP,0,BROTHER,,,True,secondary,10 +Jesse,Moore,1982-06-19,TGOXBP,0,DAUGHTER,,,True,secondary,6 +Kristen,Hubbard,2008-11-07,TGOXBP,0,DAUGHTER,,,False,primary,4 +Charles,Hamilton,1957-09-17,TGOXBP,0,SON,,,True,primary,0 +Angela,Drake,1978-11-29,DVMLGT,1,HEAD,Jamula,R1D1M2V1,True,secondary,7 +Mathew,Powell,1944-07-06,DVMLGT,0,DAUGHTER,Jamula,R1D1M2V1,False,secondary,6 +Renee,Sosa,1983-07-04,DVMLGT,0,FATHER,Jamula,R1D1M2V1,True,primary,7 +Robert,Mitchell,1970-03-17,DVMLGT,0,GRANDSON,Jamula,R1D1M2V1,False,secondary,1 +Vincent,Rogers,1941-06-20,DVMLGT,0,OTHER RELATIVE,Jamula,R1D1M2V1,False,none,4 +John,Ramirez,1963-11-10,DXVAJU,1,HEAD,Jobla,R1D1M4V1,True,primary,6 +Karen,Thomas,1934-11-19,DXVAJU,0,GRANDMOTHER,Jobla,R1D1M4V1,False,secondary,5 +Maria,Palmer,2001-11-14,DXVAJU,0,SON,Jobla,R1D1M4V1,False,tertiary,8 +Michael,Richardson,1948-05-07,DXVAJU,0,GRANDDAUGHTER,Jobla,R1D1M4V1,False,tertiary,4 +Danielle,Clarke,1997-11-09,DXVAJU,0,SON,Jobla,R1D1M4V1,True,secondary,9 +Daniel,Myers,1970-07-21,WRAZDJ,1,HEAD,,,True,none,7 +Lisa,Baker,1944-09-19,WRAZDJ,0,GRANDDAUGHTER,,,False,primary,8 +William,Richmond,1969-05-11,WRAZDJ,0,SISTER,,,True,primary,8 +Melinda,Johnson,1954-08-16,WRAZDJ,0,SPOUSE,,,False,tertiary,4 +Jennifer,Savage,1950-12-06,WRAZDJ,0,NOT RELATED,,,False,none,7 +Jacob,Love,1975-10-22,OSVBUM,1,HEAD,,,False,none,9 +John,Perkins,1937-12-18,OSVBUM,0,BROTHER,,,True,primary,6 +Robert,Shea,1957-09-26,OSVBUM,0,SON,,,True,primary,10 +Kenneth,Hoover,1982-11-27,OSVBUM,0,BROTHER,,,True,primary,9 +Mark,Smith,1958-01-06,OSVBUM,0,MOTHER,,,True,tertiary,9 +Tommy,Sosa,1983-02-09,DHLIGY,1,HEAD,Jamula,R1D1M2V1,False,tertiary,2 +Leah,Shaw,1969-05-06,DHLIGY,0,MOTHER,Jamula,R1D1M2V1,True,secondary,3 +Shawn,Nguyen,1976-07-13,DHLIGY,0,GRANDFATHER,Jamula,R1D1M2V1,False,primary,1 +Taylor,Brown,2008-01-06,DHLIGY,0,FATHER,Jamula,R1D1M2V1,True,tertiary,0 +Ashley,Davis,1989-05-25,DHLIGY,0,NOT RELATED,Jamula,R1D1M2V1,False,tertiary,2 +Amanda,Woods,2007-05-26,AJGTZR,1,HEAD,Raberjab,R1D2M1V3,False,tertiary,8 +Elizabeth,Cantu,1945-01-16,AJGTZR,0,NOT RELATED,Raberjab,R1D2M1V3,False,none,7 +Joan,Russell,1990-04-27,AJGTZR,0,SPOUSE,Raberjab,R1D2M1V3,False,tertiary,10 +Kathleen,Crawford,1970-06-17,AJGTZR,0,GRANDDAUGHTER,Raberjab,R1D2M1V3,True,none,0 +Lisa,Bryan,1964-10-11,AJGTZR,0,SPOUSE,Raberjab,R1D2M1V3,False,tertiary,1 +Margaret,Mendez,1957-08-25,RBFPGX,1,HEAD,,,True,none,10 +Adam,Curtis,1972-10-19,RBFPGX,0,MOTHER,,,False,none,3 +Fernando,Murphy,1997-10-29,RBFPGX,0,GRANDFATHER,,,True,primary,4 +Brenda,King,1935-12-13,RBFPGX,0,BROTHER,,,False,primary,8 +Ronald,Miller,1969-02-05,RBFPGX,0,SPOUSE,,,False,tertiary,5 +Kelly,Manning,1951-01-30,NHWJIJ,1,HEAD,,,True,none,10 +Andrea,Garcia,1961-11-08,NHWJIJ,0,MOTHER,,,False,tertiary,3 +Elijah,Tanner,2000-01-08,NHWJIJ,0,GRANDFATHER,,,True,primary,10 +Dawn,Torres,1957-07-12,NHWJIJ,0,BROTHER,,,True,secondary,8 +Kevin,Hurst,1951-09-23,NHWJIJ,0,GRANDMOTHER,,,False,secondary,1 +Kristy,Saunders,1992-09-27,DUCLZG,1,HEAD,Holobo,R1D2M1V1,True,primary,4 +Beth,White,1944-03-18,DUCLZG,0,GRANDDAUGHTER,Holobo,R1D2M1V1,True,primary,8 +Angela,Wang,1983-02-24,DUCLZG,0,SON,Holobo,R1D2M1V1,True,secondary,6 +Christopher,Smith,1957-06-25,DUCLZG,0,NOT RELATED,Holobo,R1D2M1V1,True,primary,10 +Ryan,Lynn,1949-04-02,DUCLZG,0,GRANDMOTHER,Holobo,R1D2M1V1,True,tertiary,4 +Jacob,Martinez,1960-05-10,FYKIWT,1,HEAD,Agdo,R1D1M1V3,False,none,1 +Tiffany,Stephens,1957-08-27,FYKIWT,0,GRANDSON,Agdo,R1D1M1V3,False,tertiary,7 +David,Wilkins,1972-04-08,FYKIWT,0,NOT RELATED,Agdo,R1D1M1V3,False,primary,2 +Patricia,Cross,1950-02-24,FYKIWT,0,DAUGHTER,Agdo,R1D1M1V3,True,none,4 +Aaron,Stein,1989-01-18,FYKIWT,0,GRANDMOTHER,Agdo,R1D1M1V3,True,none,2 +Christine,Taylor,1988-02-27,LEJBFC,1,HEAD,Rachla,R1D1M1V1,True,tertiary,4 +Karen,Nicholson,1953-04-05,LEJBFC,0,MOTHER,Rachla,R1D1M1V1,False,none,3 +Peter,Nelson,1993-11-07,LEJBFC,0,FATHER,Rachla,R1D1M1V1,False,tertiary,1 +Daniel,Pugh,2003-08-06,LEJBFC,0,SISTER,Rachla,R1D1M1V1,False,primary,2 +Stephanie,Martinez,1943-06-30,LEJBFC,0,SON,Rachla,R1D1M1V1,True,secondary,3 +Kyle,Bailey,1945-05-24,DZCMJV,1,HEAD,,,False,tertiary,1 +Laurie,Dixon,1956-02-04,DZCMJV,0,DAUGHTER,,,False,none,0 +Thomas,Campos,1970-10-10,DZCMJV,0,BROTHER,,,False,tertiary,6 +Lauren,Faulkner,1987-03-31,DZCMJV,0,DAUGHTER,,,True,tertiary,7 +Lawrence,Reyes,1964-12-15,DZCMJV,0,SON,,,False,tertiary,4 +Fred,Hughes,2004-03-06,FCDMDQ,1,HEAD,Jobla,R1D1M4V1,True,primary,9 +Kyle,Thompson,1995-10-24,FCDMDQ,0,MOTHER,Jobla,R1D1M4V1,False,primary,6 +Michael,Frank,2004-12-25,FCDMDQ,0,BROTHER,Jobla,R1D1M4V1,True,tertiary,5 +Annette,Simpson,1994-06-15,FCDMDQ,0,SPOUSE,Jobla,R1D1M4V1,True,tertiary,0 +Aaron,Bowers,1935-03-31,FCDMDQ,0,GRANDDAUGHTER,Jobla,R1D1M4V1,True,secondary,4 +Roy,Hayden,1988-05-15,TBGDCO,1,HEAD,,,True,primary,0 +Bryan,Hamilton,1983-09-23,TBGDCO,0,MOTHER,,,True,tertiary,2 +Audrey,Shepherd,2009-01-23,TBGDCO,0,MOTHER,,,False,none,3 +Thomas,Norris,1972-05-11,TBGDCO,0,MOTHER,,,True,none,4 +Sara,Lewis,1983-07-19,TBGDCO,0,GRANDSON,,,False,secondary,3 +Edward,Mcdaniel,1958-01-18,NUEPCE,1,HEAD,,,False,none,9 +Brandy,Jones,1951-03-28,NUEPCE,0,FATHER,,,False,tertiary,4 +Andrew,Rowland,1958-11-23,NUEPCE,0,FATHER,,,True,none,4 +Natalie,Rodriguez,1943-04-21,NUEPCE,0,GRANDFATHER,,,False,secondary,8 +Maria,Smith,1966-03-18,NUEPCE,0,GRANDFATHER,,,True,none,3 +Steven,Preston,1958-01-03,OWENHJ,1,HEAD,,,True,none,5 +Teresa,Anderson,1939-03-19,OWENHJ,0,GRANDDAUGHTER,,,True,primary,0 +Emily,Jones,1985-07-18,OWENHJ,0,GRANDDAUGHTER,,,True,secondary,7 +Ricky,Gray,2009-08-18,OWENHJ,0,SON,,,True,none,4 +Heather,Rodriguez,1954-08-13,OWENHJ,0,GRANDMOTHER,,,False,tertiary,5 +Brittany,Lawrence,1968-10-14,CNCODP,1,HEAD,,,False,none,3 +Carla,Jones,2002-11-30,CNCODP,0,NOT RELATED,,,False,none,2 +William,Barnes,1981-09-23,CNCODP,0,NOT RELATED,,,True,primary,1 +Jason,Clark,1971-01-14,CNCODP,0,SISTER,,,True,primary,0 +Brad,King,1968-07-21,CNCODP,0,GRANDMOTHER,,,False,tertiary,0 diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 0418a8e..a625d93 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -240,6 +240,15 @@ Cypress.Commands.add('enterMuiInput', (label, value, inputTag='input') => { .type(value, {force: true}); }) +Cypress.Commands.add('chooseMuiSelect', (label, value) => { + cy.contains('label', label) + .siblings('.MuiInputBase-root') + .find('[role="button"]') + .click() + + cy.contains('li', value).click() +}) + Cypress.Commands.add('assertMuiInput', (label, value, inputTag='input') => { cy.contains('label', label) .siblings('.MuiInputBase-root') @@ -271,3 +280,14 @@ Cypress.Commands.add('setModuleConfig', (moduleName, configFixtureFile) => { cy.contains("was added successfully") }) }) + +Cypress.Commands.add('getItemCount', (itemName) => { + const pattern = new RegExp(`\\d+ ${itemName}s? Found`); + return cy.contains(pattern) + .invoke('text') + .then((text) => { + const match = text.match(new RegExp(`(\\d+)\\s+${itemName}`)); + return parseInt(match?.[1], 10); + }); +}); + diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 42a94bf..29fa847 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,3 +1,4 @@ +import 'cypress-file-upload'; import './commands' before(() => { diff --git a/package-lock.json b/package-lock.json index c33c435..ffda01e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,9 +4,9 @@ "requires": true, "packages": { "": { - "name": "openimis-dist_dkr", "devDependencies": { - "cypress": "^13.13.0" + "cypress": "^13.13.0", + "cypress-file-upload": "^5.0.8" } }, "node_modules/@colors/colors": { @@ -606,6 +606,19 @@ "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, + "node_modules/cypress-file-upload": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz", + "integrity": "sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.2.1" + }, + "peerDependencies": { + "cypress": ">3.0.0" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", diff --git a/package.json b/package.json index 00a32de..b21a519 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "devDependencies": { - "cypress": "^13.13.0" + "cypress": "^13.13.0", + "cypress-file-upload": "^5.0.8" }, "scripts": { "cy:open": "cypress open" From 039cc2e26f1953ada166adc2179321ac58cc9547 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 16:20:10 -0400 Subject: [PATCH 17/33] Pin down 3rd party GitHub action versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As suggested by sonar scan’s security results --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 11a9ea8..de1c051 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: with: fetch-depth: 0 - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@v5.3.1 + uses: SonarSource/sonarqube-scan-action@1a6d90ebcb0e6a6b1d87e37ba693fe453195ae25 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -69,7 +69,7 @@ jobs: exit 1 - name: Cypress run - uses: cypress-io/github-action@v6 + uses: cypress-io/github-action@b8ba51a856ba5f4c15cf39007636d4ab04f23e3c with: record: false parallel: false From 5f2f7df39c6766e3699565adf9316b02031572ab Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 17:19:18 -0400 Subject: [PATCH 18/33] Increase server check timeout & log non-200 response code --- cypress.config.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index a7c8680..f253271 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -7,8 +7,8 @@ const serverFlagPath = path.resolve(__dirname, 'serverStarted') const payload = { "query": "{ moduleConfigurations { module, config, controls { field, usage } } }" } -const timeout = 4 * 60 * 1000 // 4 minutes -const interval = 10000 // Check every 10 seconds +const timeoutMinutes = 10 +const retryIntervalSeconds = 15 function waitForServerToStart(url) { console.log('Waiting for API server to start...') @@ -30,11 +30,13 @@ function waitForServerToStart(url) { } }) .catch(error => { - if (Date.now() - startTime >= timeout) { - return reject(new Error('Timed out waiting for the server to start')) + if (Date.now() - startTime >= timeoutMinutes * 60 * 1000) { + return reject( + new Error(`Timed out waiting for the server to start: ${error.message}`) + ) } else { - console.log(`Retrying in ${interval / 1000} seconds...`) - return setTimeout(checkServer, interval) + console.log(`${error.message}. Retrying in ${retryIntervalSeconds} seconds...`) + return setTimeout(checkServer, retryIntervalSeconds * 1000) } }) } @@ -50,7 +52,7 @@ module.exports = defineConfig({ projectId: "q6gc25", // Cypress Cloud, needed for recording baseUrl: 'http://localhost', defaultCommandTimeout: 15000, - taskTimeout: 300000, + taskTimeout: timeoutMinutes * 60 * 1000 + 10, downloadsFolder: 'cypress/downloads', setupNodeEvents(on, config) { on('task', { @@ -66,8 +68,9 @@ module.exports = defineConfig({ return null }) .catch(error => { - console.error('Failed to start server:', error) - return null + console.error('Failed to start server:') + console.error(error.stack); + return reject(error); }) }, removeSetupFile() { From 6f9d1b97650868b0b246c8bd8fd9d7aba20e30ec Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 5 Sep 2025 18:11:04 -0400 Subject: [PATCH 19/33] Add domain config to avoid 400 when request is redirected to https --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index de1c051..19a7eee 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,6 +44,7 @@ jobs: echo 'DEMO_DATASET=true' >> .env echo 'BE_TAG=develop' >> .env echo 'FE_TAG=develop' >> .env + echo 'DOMAIN=localhost' >> .env echo 'HOSTS=localhost' >> .env cp .env.cache.example .env.cache cp .env.openSearch.example .env.openSearch From bf0b0cc5b33ccc2d44c98d4e6dd7c3325e3eddb7 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 11 Sep 2025 08:56:14 -0400 Subject: [PATCH 20/33] Reorganize cash transfer tests to share setup steps --- cypress/e2e/cash-trasfer.cy.js | 255 +++++++++++++++++---------------- cypress/support/commands.js | 16 +++ 2 files changed, 150 insertions(+), 121 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 291caf3..fe6fbfa 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -1,6 +1,6 @@ import { getProgramTerm } from '../support/utils'; -describe('Cash transfer workflows', () => { +describe('Cash transfer program creation workflows', () => { let testProgramNames = []; beforeEach(function () { @@ -34,68 +34,6 @@ describe('Cash transfer workflows', () => { cy.checkProgramFieldValuesInListView(programCode, programName, maxBeneficiaries, programType) }) - it('Updates an individual program', function () { - // Disable maker checker - cy.loginAdminInterface() - cy.setModuleConfig('social_protection', 'social-protection-config.json') - - const programCode = 'E2EICPU' - const programName = 'E2E Individual Cash Program Updated' - const maxBeneficiaries = "100" - const programType = "INDIVIDUAL" - - cy.createProgram(programCode, programName, maxBeneficiaries, programType) - - // Ensure the created program gets cleaned up later - testProgramNames.push(programName); - - const updatedProgramCode = 'E2EICP42' - const updatedMaxBeneficiaries = "111" - const updatedInstitution = "Social Protection Agency" - const updatedDescription = "Foo bar baz" - - // Go back into the list page to find the program & edit - cy.visit('/front/benefitPlans'); - cy.contains('tfoot', 'Rows Per Page') - cy.contains('td', programName) - .parent('tr').within(() => { - // click on edit button - cy.get('a.MuiIconButton-root').click() - }) - - cy.assertMuiInput('Code', programCode) - cy.enterMuiInput('Code', updatedProgramCode) - cy.assertMuiInput('Code', updatedProgramCode) - - cy.enterMuiInput('Max Beneficiaries', updatedMaxBeneficiaries) - - cy.enterMuiInput('Institution', updatedInstitution) - - cy.enterMuiInput('Description', updatedDescription, 'textarea') - - cy.get('[title="Save changes"] button').click() - - // Wait for update to complete - cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') - cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') - - // Check last journal message - cy.get('ul.MuiList-root li').first().click() - cy.contains(`Update ${getProgramTerm()}`).should('exist') - cy.contains('Failed to update').should('not.exist') - - // Check program field values are persisted - cy.reload() - cy.checkProgramFieldValues( - updatedProgramCode, - programName, - updatedMaxBeneficiaries, - programType, - updatedInstitution, - updatedDescription, - ) - }) - it('Creates and deletes an household program', function () { const programCode = 'E2EGCP' const programName = 'E2E Household Cash Program' @@ -115,87 +53,162 @@ describe('Cash transfer workflows', () => { cy.visit('/front/benefitPlans'); cy.checkProgramFieldValuesInListView(programCode, programName, maxBeneficiaries, programType) }) +}) - it('Updates a household program', function () { +describe('Cash transfer program update workflows', () => { + before(() => { // Disable maker checker cy.loginAdminInterface() cy.setModuleConfig('social_protection', 'social-protection-config.json') + cy.logoutAdminInterface() + }) - const programCode = 'E2EGCPU' - const programName = 'E2E Household Cash Program Updated' - const maxBeneficiaries = "200" - const programType = "GROUP" - - cy.createProgram(programCode, programName, maxBeneficiaries, programType) - - // Ensure the created program gets cleaned up later - testProgramNames.push(programName); - - const updatedProgramCode = 'E2EGCP42' - const updatedMaxBeneficiaries = "222" - const updatedInstitution = "Family Support Services" - const updatedDescription = "A functional program" - - // Go back into the list page to find the program & edit - cy.visit('/front/benefitPlans'); - cy.contains('tfoot', 'Rows Per Page') - cy.contains('td', programName) - .parent('tr').within(() => { - // click on edit button - cy.get('a.MuiIconButton-root').click() - }) - - cy.assertMuiInput('Code', programCode) - cy.enterMuiInput('Code', updatedProgramCode) - cy.assertMuiInput('Code', updatedProgramCode) + describe('Individual program', () => { + const programCode = 'E2EICPU' + const programName = 'E2E Individual Cash Program Updated' + const maxBeneficiaries = "100" + const programType = "INDIVIDUAL" - cy.enterMuiInput('Max Beneficiaries', updatedMaxBeneficiaries) + before(() => { + cy.login() + cy.createProgram(programCode, programName, maxBeneficiaries, programType) + }) - cy.enterMuiInput('Institution', updatedInstitution) + after(() => { + cy.deleteProgram(programName) + }) - cy.enterMuiInput('Description', updatedDescription, 'textarea') + it('Updates an individual program', function () { + const updatedProgramCode = 'E2EICP42' + const updatedMaxBeneficiaries = "111" + const updatedInstitution = "Social Protection Agency" + const updatedDescription = "Foo bar baz" + + // Go back into the list page to find the program & edit + cy.visit('/front/benefitPlans'); + cy.contains('tfoot', 'Rows Per Page') + cy.contains('td', programName) + .parent('tr').within(() => { + // click on edit button + cy.get('a.MuiIconButton-root').click() + }) + + cy.assertMuiInput('Code', programCode) + cy.enterMuiInput('Code', updatedProgramCode) + cy.assertMuiInput('Code', updatedProgramCode) + + cy.enterMuiInput('Max Beneficiaries', updatedMaxBeneficiaries) + + cy.enterMuiInput('Institution', updatedInstitution) + + cy.enterMuiInput('Description', updatedDescription, 'textarea') + + cy.get('[title="Save changes"] button').click() + + // Wait for update to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Update ${getProgramTerm()}`).should('exist') + cy.contains('Failed to update').should('not.exist') + + // Check program field values are persisted + cy.reload() + cy.checkProgramFieldValues( + updatedProgramCode, + programName, + updatedMaxBeneficiaries, + programType, + updatedInstitution, + updatedDescription, + ) + }) + }) - cy.get('[title="Save changes"] button').click() + describe('Household program', () => { + const programCode = 'E2EGCPU' + const programName = 'E2E Household Cash Program Updated' + const maxBeneficiaries = "200" + const programType = "GROUP" - // Wait for update to complete - cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') - cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + before(() => { + cy.login() + cy.createProgram(programCode, programName, maxBeneficiaries, programType) + }) - // Check last journal message - cy.get('ul.MuiList-root li').first().click() - cy.contains(`Update ${getProgramTerm()}`).should('exist') - cy.contains('Failed to update').should('not.exist') + after(() => { + cy.deleteProgram(programName) + }) - // Check program field values are persisted - cy.reload() - cy.checkProgramFieldValues( - updatedProgramCode, - programName, - updatedMaxBeneficiaries, - programType, - updatedInstitution, - updatedDescription, - ) + it('Updates a household program', function () { + const updatedProgramCode = 'E2EGCP42' + const updatedMaxBeneficiaries = "222" + const updatedInstitution = "Family Support Services" + const updatedDescription = "A functional program" + + // Go back into the list page to find the program & edit + cy.visit('/front/benefitPlans'); + cy.contains('tfoot', 'Rows Per Page') + cy.contains('td', programName) + .parent('tr').within(() => { + // click on edit button + cy.get('a.MuiIconButton-root').click() + }) + + cy.assertMuiInput('Code', programCode) + cy.enterMuiInput('Code', updatedProgramCode) + cy.assertMuiInput('Code', updatedProgramCode) + + cy.enterMuiInput('Max Beneficiaries', updatedMaxBeneficiaries) + + cy.enterMuiInput('Institution', updatedInstitution) + + cy.enterMuiInput('Description', updatedDescription, 'textarea') + + cy.get('[title="Save changes"] button').click() + + // Wait for update to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Update ${getProgramTerm()}`).should('exist') + cy.contains('Failed to update').should('not.exist') + + // Check program field values are persisted + cy.reload() + cy.checkProgramFieldValues( + updatedProgramCode, + programName, + updatedMaxBeneficiaries, + programType, + updatedInstitution, + updatedDescription, + ) + }) }) +}) - it('Imports individuals and groups', function () { +describe('Individuals and groups/households', () => { + before(() => { + // Disable maker checker cy.loginAdminInterface() cy.setModuleConfig('individual', 'individual-config-minimal.json') + cy.logoutAdminInterface() + }) + it('Imports individuals and groups', function () { + cy.login() cy.visit('/front/groups') cy.getItemCount('Group').as('initialGroupCount'); cy.visit('/front/individuals') cy.getItemCount('Individual').as('initialIndividualCount'); - cy.contains('li', 'UPLOAD').click() - - cy.get('input[type="file"]').attachFile('individuals.csv'); - - cy.chooseMuiSelect('Workflow', 'Python Import Individuals') - cy.contains('button', 'Upload Individuals').click(); - - cy.contains('button', 'Upload Individuals').should('not.exist') + cy.uploadIndividualsCSV() cy.visit('/front/individuals') cy.getItemCount("Individual").then(newCount => { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a625d93..b236c6d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -28,6 +28,11 @@ Cypress.Commands.add('loginAdminInterface', () => { }) }) +Cypress.Commands.add('logoutAdminInterface', () => { + cy.visit('/api/admin'); + cy.contains('button', 'Log out').click() +}) + Cypress.Commands.add('deleteModuleConfig', (moduleName) => { cy.visit('/api/admin/core/moduleconfiguration/'); @@ -231,6 +236,17 @@ Cypress.Commands.add( }) }) +Cypress.Commands.add('uploadIndividualsCSV', () => { + cy.contains('li', 'UPLOAD').click() + + cy.get('input[type="file"]').attachFile('individuals.csv'); + + cy.chooseMuiSelect('Workflow', 'Python Import Individuals') + cy.contains('button', 'Upload Individuals').click(); + + cy.contains('button', 'Upload Individuals').should('not.exist') +}) + Cypress.Commands.add('enterMuiInput', (label, value, inputTag='input') => { cy.contains('label', label) .siblings('.MuiInputBase-root') From 185d9eec1c22d76cb80461a2a0501ded2f82b4cd Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 11 Sep 2025 10:48:58 -0400 Subject: [PATCH 21/33] Trigger admin user auto provision of DB core user before admin tests --- cypress/e2e/admin.cy.js | 7 +++++++ cypress/support/commands.js | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/cypress/e2e/admin.cy.js b/cypress/e2e/admin.cy.js index 9b45c70..348d052 100644 --- a/cypress/e2e/admin.cy.js +++ b/cypress/e2e/admin.cy.js @@ -3,6 +3,13 @@ import { getProgramTerm, capitalizeWords } from '../support/utils'; const path = require('path'); describe('Django admin workflows', () => { + before(function () { + // This ensures that Admin user's core user exists + // because is only auto provisioned on first login to the frontend UI + cy.login() + cy.logout() + }); + beforeEach(function () { cy.loginAdminInterface() }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index b236c6d..76f691d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -18,6 +18,11 @@ Cypress.Commands.add('login', () => { }) }) +Cypress.Commands.add('logout', () => { + cy.visit('/front'); + cy.get('button[title="Log out"]').click() +}) + Cypress.Commands.add('loginAdminInterface', () => { cy.visit('/api/admin'); cy.fixture('cred').then((cred) => { From d07c4c3ca202f00b3050be88b859e6d04914304c Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 11 Sep 2025 11:28:33 -0400 Subject: [PATCH 22/33] Add failed login E2E tests Also maps auth tests to UAT test numbers --- cypress/e2e/auth.cy.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/auth.cy.js b/cypress/e2e/auth.cy.js index f094813..3e29d8f 100644 --- a/cypress/e2e/auth.cy.js +++ b/cypress/e2e/auth.cy.js @@ -1,9 +1,11 @@ describe('Unauthenticated', () => { - it('Shows the login screen', () => { + it('Shows the login screen (OCM-1125, OCM-1126)', () => { cy.visit('/') cy.contains('Username') cy.contains('Password') + cy.get('input[type="password"]').should('be.visible') cy.contains('button', 'Log In') + cy.contains('button', 'Log In').should('be.disabled') }) }) @@ -15,7 +17,7 @@ describe('Sign in and out', () => { }) }); - it('Signs in and out the admin user', function () { + it('Signs in and out the admin user (OCM-1122)', function () { cy.get('input[type="text"]').type(this.cred.username) cy.get('input[type="password"]').type(this.cred.password) cy.get('button[type="submit"]').click() @@ -24,5 +26,23 @@ describe('Sign in and out', () => { cy.get('button[title="Log out"]').click() cy.contains('button', 'Log In') }) + + it('Rejects non-existent username (OCM-1123)', function () { + cy.get('input[type="text"]').type(this.cred.username + 'asdf') + cy.get('input[type="password"]').type(this.cred.password) + cy.get('button[type="submit"]').click() + + cy.contains("The password or the username you've entered is incorrect.") + cy.contains('button', 'Log In') + }) + + it('Rejects incorrect password (OCM-1124)', function () { + cy.get('input[type="text"]').type(this.cred.username) + cy.get('input[type="password"]').type(this.cred.password + 'asdf') + cy.get('button[type="submit"]').click() + + cy.contains("The password or the username you've entered is incorrect.") + cy.contains('button', 'Log In') + }) }) From 078a161cdf27eb60537a2758bddcfc9563abe27e Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 11 Sep 2025 13:54:55 -0400 Subject: [PATCH 23/33] Add E2E tests on default enrollment criteria config on programs --- cypress/e2e/cash-trasfer.cy.js | 87 ++++++++++++++++++++++------------ cypress/support/commands.js | 30 +++++++++++- 2 files changed, 84 insertions(+), 33 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index fe6fbfa..d8af86b 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -84,14 +84,8 @@ describe('Cash transfer program update workflows', () => { const updatedInstitution = "Social Protection Agency" const updatedDescription = "Foo bar baz" - // Go back into the list page to find the program & edit cy.visit('/front/benefitPlans'); - cy.contains('tfoot', 'Rows Per Page') - cy.contains('td', programName) - .parent('tr').within(() => { - // click on edit button - cy.get('a.MuiIconButton-root').click() - }) + cy.openProgramForEditFromList(programName) cy.assertMuiInput('Code', programCode) cy.enterMuiInput('Code', updatedProgramCode) @@ -105,14 +99,7 @@ describe('Cash transfer program update workflows', () => { cy.get('[title="Save changes"] button').click() - // Wait for update to complete - cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') - cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') - - // Check last journal message - cy.get('ul.MuiList-root li').first().click() - cy.contains(`Update ${getProgramTerm()}`).should('exist') - cy.contains('Failed to update').should('not.exist') + cy.checkProgramUpdateCompleted() // Check program field values are persisted cy.reload() @@ -125,6 +112,32 @@ describe('Cash transfer program update workflows', () => { updatedDescription, ) }) + + it('configures default enrollment criteria for an individual program', function () { + cy.visit('/front/benefitPlans'); + cy.openProgramForEditFromList(programName) + + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', 'Potential').click() + + cy.contains('Potential Beneficiary Enrollment Criteria') + cy.contains('button', 'Add Filters').click() + + cy.chooseMuiSelect('Field', 'Educated level') + cy.chooseMuiSelect('Confirm Filters', 'Contains') + cy.enterMuiInput('Value', 'prim') + cy.get('[title="Save changes"] button').click() + + cy.checkProgramUpdateCompleted() + cy.reload() + + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', 'Potential').click() + + cy.assertMuiSelectValue('Field', 'Educated level') + cy.assertMuiSelectValue('Confirm Filters', 'Contains') + cy.assertMuiInput('Value', 'prim') + }) }) describe('Household program', () => { @@ -148,14 +161,7 @@ describe('Cash transfer program update workflows', () => { const updatedInstitution = "Family Support Services" const updatedDescription = "A functional program" - // Go back into the list page to find the program & edit - cy.visit('/front/benefitPlans'); - cy.contains('tfoot', 'Rows Per Page') - cy.contains('td', programName) - .parent('tr').within(() => { - // click on edit button - cy.get('a.MuiIconButton-root').click() - }) + cy.openProgramForEditFromList(programName) cy.assertMuiInput('Code', programCode) cy.enterMuiInput('Code', updatedProgramCode) @@ -169,14 +175,7 @@ describe('Cash transfer program update workflows', () => { cy.get('[title="Save changes"] button').click() - // Wait for update to complete - cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') - cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') - - // Check last journal message - cy.get('ul.MuiList-root li').first().click() - cy.contains(`Update ${getProgramTerm()}`).should('exist') - cy.contains('Failed to update').should('not.exist') + cy.checkProgramUpdateCompleted() // Check program field values are persisted cy.reload() @@ -189,6 +188,32 @@ describe('Cash transfer program update workflows', () => { updatedDescription, ) }) + + it('configures default enrollment criteria for a household program', function () { + cy.visit('/front/benefitPlans'); + cy.openProgramForEditFromList(programName) + + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', 'Active').click() + + cy.contains('Active Beneficiary Enrollment Criteria') + cy.contains('button', 'Add Filters').click() + + cy.chooseMuiSelect('Field', 'Number of children') + cy.chooseMuiSelect('Confirm Filters', 'Greater than or equal to') + cy.enterMuiInput('Value', '2') + cy.get('[title="Save changes"] button').click() + + cy.checkProgramUpdateCompleted() + cy.reload() + + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', 'Active').click() + + cy.assertMuiSelectValue('Field', 'Number of children') + cy.assertMuiSelectValue('Confirm Filters', 'Greater than or equal to') + cy.assertMuiInput('Value', '2') + }) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 76f691d..7cb31f3 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -206,8 +206,29 @@ Cypress.Commands.add('createProgram', (programCode, programName, maxBeneficiarie cy.contains('Failed to create').should('not.exist') }) +Cypress.Commands.add('openProgramForEditFromList', (programName) => { + cy.contains('tfoot', 'Rows Per Page') + cy.contains('td', programName) + .parent('tr').within(() => { + // click on edit button + cy.get('a.MuiIconButton-root').click() + }) + cy.assertMuiInput('Name', programName) +}) + +Cypress.Commands.add('checkProgramUpdateCompleted', (programName) => { + // Wait for update to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Update ${getProgramTerm()}`).should('exist') + cy.contains('Failed to update').should('not.exist') +}) + Cypress.Commands.add( - 'checkProgramFieldValues', + 'checkProgramFieldValues', ( programCode, programName, @@ -278,6 +299,12 @@ Cypress.Commands.add('assertMuiInput', (label, value, inputTag='input') => { .and('have.value', value); }) +Cypress.Commands.add('assertMuiSelectValue', (label, value) => { + cy.contains('label', label) + .siblings('.MuiInputBase-root') + .contains(value) +}) + Cypress.Commands.add('setModuleConfig', (moduleName, configFixtureFile) => { cy.deleteModuleConfig(moduleName) @@ -311,4 +338,3 @@ Cypress.Commands.add('getItemCount', (itemName) => { return parseInt(match?.[1], 10); }); }); - From 2dad2ec5abe7a661a94cc044a4c42f05653d5dcf Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 20 Sep 2025 07:48:28 -0400 Subject: [PATCH 24/33] E2E test for creating and deleting a project under a household program --- cypress/e2e/cash-trasfer.cy.js | 62 +++++++++++++++++++++++++++++++++- cypress/support/commands.js | 39 +++++++++++++++++++++ cypress/support/utils.js | 4 +++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index d8af86b..e330d91 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -1,4 +1,4 @@ -import { getProgramTerm } from '../support/utils'; +import { getProgramTerm, getTimestamp } from '../support/utils'; describe('Cash transfer program creation workflows', () => { let testProgramNames = []; @@ -56,10 +56,17 @@ describe('Cash transfer program creation workflows', () => { }) describe('Cash transfer program update workflows', () => { + const treePlantingActivity = `E2E Tree Planting - ${getTimestamp()}`; + const activities = [ + treePlantingActivity, + `E2E River Cleaning - ${getTimestamp()}`, + ]; + before(() => { // Disable maker checker cy.loginAdminInterface() cy.setModuleConfig('social_protection', 'social-protection-config.json') + cy.createActivities(activities) cy.logoutAdminInterface() }) @@ -145,6 +152,7 @@ describe('Cash transfer program update workflows', () => { const programName = 'E2E Household Cash Program Updated' const maxBeneficiaries = "200" const programType = "GROUP" + let testProjectPaths = [] before(() => { cy.login() @@ -152,6 +160,9 @@ describe('Cash transfer program update workflows', () => { }) after(() => { + testProjectPaths.forEach(projectPath => { + cy.deleteProject(projectPath) + }) cy.deleteProgram(programName) }) @@ -214,6 +225,55 @@ describe('Cash transfer program update workflows', () => { cy.assertMuiSelectValue('Confirm Filters', 'Greater than or equal to') cy.assertMuiInput('Value', '2') }) + + it('Creates and deletes a project under a given program', function () { + cy.visit('/front/benefitPlans'); + cy.openProgramForEditFromList(programName) + cy.contains('button', 'Projects').click() + cy.contains('button', 'Create Project').click() + cy.contains('h6', 'Project details') + + const projectName = `E2E Public Works Project - ${getTimestamp()}` + cy.enterMuiInput('Name', projectName) + + cy.chooseMuiAutocomplete('Activity', treePlantingActivity) + + const regionName = 'R2 Tahida' + const districtName = 'R2D2 Vida' + cy.chooseMuiAutocomplete('Location', regionName) + cy.contains('li', districtName).click() + + const targetBeneficiaries = "50" + cy.enterMuiInput('Target Beneficiaries', targetBeneficiaries) + + const workingDays = "20" + cy.enterMuiInput('Working Days', workingDays) + + cy.get('[title="Save"] button').click() + + // Wait for creation to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Create project ${projectName}`).should('exist') + cy.contains('Failed to create').should('not.exist') + + // Ensure project is cleaned up after test ends + cy.url().then((currentUrl) => { + const path = new URL(currentUrl).pathname; + testProjectPaths.push(path) + }); + + cy.reload() + + cy.assertMuiInput('Name', projectName) + cy.assertMuiInput('Activity', treePlantingActivity) + cy.assertMuiInput('Location', districtName) + cy.assertMuiInput('Target Beneficiaries', targetBeneficiaries) + cy.assertMuiInput('Working Days', workingDays) + }) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7cb31f3..d1d162e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -117,6 +117,36 @@ Cypress.Commands.add('deleteActivities', (activityNames) => { }); }); +Cypress.Commands.add('createActivities', (activities) => { + cy.deleteActivities(activities) + + activities.forEach(activityName => { + cy.contains('a', 'Activities').click() + cy.contains('a', 'Add Activity').click() + cy.get('input[name="name"]').type(activityName) + cy.get('input[value="Save"]').click() + cy.contains('td.field-name', activityName) + }) +}) + +Cypress.Commands.add('deleteProject', (projectPath) => { + cy.visit(projectPath); + cy.get('button[title="Delete"]').click(); + cy.contains('button', 'Ok').click(); + + // Wait for deletion to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Delete project`).should('exist') + cy.contains('Failed to delete').should('not.exist') + + // Check redirect + cy.location('pathname').should('not.include', projectPath); +}) + Cypress.Commands.add('deleteProgram', (programName) => { cy.visit('/front/benefitPlans'); cy.contains('tfoot', 'Rows Per Page').should('be.visible') @@ -305,6 +335,15 @@ Cypress.Commands.add('assertMuiSelectValue', (label, value) => { .contains(value) }) +Cypress.Commands.add('chooseMuiAutocomplete', (label, value) => { + cy.contains('label', label) + .siblings('.MuiInputBase-root') + .find('input') + .click() + + cy.contains('li', value).click() +}) + Cypress.Commands.add('setModuleConfig', (moduleName, configFixtureFile) => { cy.deleteModuleConfig(moduleName) diff --git a/cypress/support/utils.js b/cypress/support/utils.js index 952a513..d38a02a 100644 --- a/cypress/support/utils.js +++ b/cypress/support/utils.js @@ -5,3 +5,7 @@ export function getProgramTerm() { export function capitalizeWords(str) { return str.replace(/\b\w/g, char => char.toUpperCase()); } + +export function getTimestamp() { + return new Date().toISOString().replace(/[:.]/g, '-'); +} From 8b638cabc98ad65dfc859118cf306a2481fe85ac Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 20 Sep 2025 10:18:15 -0400 Subject: [PATCH 25/33] E2E tests for update a project and restore a deleted project --- cypress/e2e/cash-trasfer.cy.js | 153 +++++++++++++++++++++++++-------- cypress/support/commands.js | 49 +++++++++++ 2 files changed, 164 insertions(+), 38 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index e330d91..8f6e60f 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -57,9 +57,10 @@ describe('Cash transfer program creation workflows', () => { describe('Cash transfer program update workflows', () => { const treePlantingActivity = `E2E Tree Planting - ${getTimestamp()}`; + const riverCleaningActivity = `E2E River Cleaning - ${getTimestamp()}`; const activities = [ treePlantingActivity, - `E2E River Cleaning - ${getTimestamp()}`, + riverCleaningActivity, ]; before(() => { @@ -152,7 +153,6 @@ describe('Cash transfer program update workflows', () => { const programName = 'E2E Household Cash Program Updated' const maxBeneficiaries = "200" const programType = "GROUP" - let testProjectPaths = [] before(() => { cy.login() @@ -160,9 +160,6 @@ describe('Cash transfer program update workflows', () => { }) after(() => { - testProjectPaths.forEach(projectPath => { - cy.deleteProject(projectPath) - }) cy.deleteProgram(programName) }) @@ -226,45 +223,17 @@ describe('Cash transfer program update workflows', () => { cy.assertMuiInput('Value', '2') }) - it('Creates and deletes a project under a given program', function () { - cy.visit('/front/benefitPlans'); - cy.openProgramForEditFromList(programName) - cy.contains('button', 'Projects').click() - cy.contains('button', 'Create Project').click() - cy.contains('h6', 'Project details') - + it('Creates a project under a household program', function () { const projectName = `E2E Public Works Project - ${getTimestamp()}` - cy.enterMuiInput('Name', projectName) - - cy.chooseMuiAutocomplete('Activity', treePlantingActivity) - const regionName = 'R2 Tahida' const districtName = 'R2D2 Vida' - cy.chooseMuiAutocomplete('Location', regionName) - cy.contains('li', districtName).click() - const targetBeneficiaries = "50" - cy.enterMuiInput('Target Beneficiaries', targetBeneficiaries) - const workingDays = "20" - cy.enterMuiInput('Working Days', workingDays) - cy.get('[title="Save"] button').click() - - // Wait for creation to complete - cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') - cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') - - // Check last journal message - cy.get('ul.MuiList-root li').first().click() - cy.contains(`Create project ${projectName}`).should('exist') - cy.contains('Failed to create').should('not.exist') - - // Ensure project is cleaned up after test ends - cy.url().then((currentUrl) => { - const path = new URL(currentUrl).pathname; - testProjectPaths.push(path) - }); + cy.createProject( + programName, projectName, treePlantingActivity, + regionName, districtName, targetBeneficiaries, workingDays, + ) cy.reload() @@ -274,6 +243,114 @@ describe('Cash transfer program update workflows', () => { cy.assertMuiInput('Target Beneficiaries', targetBeneficiaries) cy.assertMuiInput('Working Days', workingDays) }) + + describe('Given a project and a household program', function () { + const projectName = `E2E Existing Project - ${getTimestamp()}` + const regionName = 'R2 Tahida' + const districtName = 'R2D1 Rajo' + const targetBeneficiaries = "20" + const workingDays = "10" + let projectPath = null + + before(() => { + cy.createProject( + programName, projectName, treePlantingActivity, + regionName, districtName, targetBeneficiaries, workingDays, + ) + cy.url().then((currentUrl) => { + projectPath = new URL(currentUrl).pathname; + }); + }) + + it('Updates a project', function () { + cy.visit(projectPath) + cy.assertMuiInput('Name', projectName) + + const projectNameUpdated = `${projectName} Updated` + cy.enterMuiInput('Name', projectNameUpdated) + + cy.chooseMuiAutocomplete('Activity', riverCleaningActivity) + + const regionNameUpdated = 'R1 Region 1' + const districtNameUpdated = 'R1D1 District 1' + const municipalityName = 'R1D1M2 Jamu' + cy.chooseMuiAutocomplete('Location', regionNameUpdated) + cy.contains('li', districtNameUpdated).click() + cy.contains('li', municipalityName).click() + + const targetBeneficiariesUpdated = "20" + cy.enterMuiInput('Target Beneficiaries', targetBeneficiariesUpdated) + + const workingDaysUpdated = "10" + cy.enterMuiInput('Working Days', workingDaysUpdated) + + cy.get('[title="Save"] button').click() + + // Wait for update to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Update project ${projectName}`).should('exist') + cy.contains('Failed to update').should('not.exist') + + cy.visit(projectPath) + + cy.assertMuiInput('Name', projectNameUpdated) + cy.assertMuiInput('Activity', riverCleaningActivity) + cy.assertMuiInput('Location', municipalityName) + cy.assertMuiInput('Target Beneficiaries', targetBeneficiariesUpdated) + cy.assertMuiInput('Working Days', workingDaysUpdated) + }) + + it('Deletes and restores a project', function () { + cy.visit(projectPath) + cy.contains('Beneficiaries Assigned') + cy.contains('label', 'Name') + .siblings('.MuiInputBase-root') + .find('input') + .invoke('val') + .then((name) => { + cy.deleteProject(projectPath); + cy.reload() + + cy.contains('button', 'Projects').click() + cy.contains('label', 'Show Deleted').click() + cy.contains('button', 'Search').click() + + cy.contains('td', name) + .parent('tr').within(() => { + cy.get('button[title="Edit"]').click({force: true}); + }) + + cy.assertMuiInputDisabled('Name', name) + cy.assertMuiInputDisabled('Activity') + cy.assertMuiInputDisabled('Location') + cy.assertMuiInputDisabled('Target Beneficiaries') + cy.assertMuiInputDisabled('Working Days') + cy.assertMuiInputDisabled('Status') + cy.assertMuiInputDisabled('Program') + + cy.get('button[title="Undo Delete"]').click() + cy.contains('button', 'Ok').click(); + + // Wait for undo to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Restore project`).should('exist') + cy.contains('Failed to restore').should('not.exist') + + cy.reload() + + cy.contains('button', 'Projects').click() + cy.contains(name).should('exist'); + }); + }) + }) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index d1d162e..2728f41 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -147,6 +147,44 @@ Cypress.Commands.add('deleteProject', (projectPath) => { cy.location('pathname').should('not.include', projectPath); }) +Cypress.Commands.add('createProject', ( + programName, + projectName, + activityName, + regionName, + districtName, + targetBeneficiaries, + workingDays, +) => { + cy.visit('/front/benefitPlans'); + cy.openProgramForEditFromList(programName) + cy.contains('button', 'Projects').click() + cy.contains('button', 'Create Project').click() + cy.contains('h6', 'Project details') + + cy.enterMuiInput('Name', projectName) + + cy.chooseMuiAutocomplete('Activity', activityName) + + cy.chooseMuiAutocomplete('Location', regionName) + cy.contains('li', districtName).click() + + cy.enterMuiInput('Target Beneficiaries', targetBeneficiaries) + + cy.enterMuiInput('Working Days', workingDays) + + cy.get('[title="Save"] button').click() + + // Wait for creation to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Create project ${projectName}`).should('exist') + cy.contains('Failed to create').should('not.exist') +}) + Cypress.Commands.add('deleteProgram', (programName) => { cy.visit('/front/benefitPlans'); cy.contains('tfoot', 'Rows Per Page').should('be.visible') @@ -329,6 +367,17 @@ Cypress.Commands.add('assertMuiInput', (label, value, inputTag='input') => { .and('have.value', value); }) +Cypress.Commands.add('assertMuiInputDisabled', (label, value=null, inputTag='input') => { + const input = cy.contains('label', label) + .siblings('.MuiInputBase-root') + .find(inputTag) + input.should('be.disabled'); + + if (value) { + input.should('have.value', value) + } +}) + Cypress.Commands.add('assertMuiSelectValue', (label, value) => { cy.contains('label', label) .siblings('.MuiInputBase-root') From 8f9b02267dc2250e88312516c4f9ca7b43f62486 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sun, 21 Sep 2025 12:43:52 -0400 Subject: [PATCH 26/33] Fix E2E login before each tests so the entire suite runs correctly --- cypress/e2e/cash-trasfer.cy.js | 14 +++++++++++++- cypress/support/commands.js | 9 +++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 8f6e60f..c1f53a7 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -80,12 +80,17 @@ describe('Cash transfer program update workflows', () => { before(() => { cy.login() cy.createProgram(programCode, programName, maxBeneficiaries, programType) + cy.logout() }) after(() => { cy.deleteProgram(programName) }) + beforeEach(function () { + cy.login() + }) + it('Updates an individual program', function () { const updatedProgramCode = 'E2EICP42' const updatedMaxBeneficiaries = "111" @@ -157,18 +162,24 @@ describe('Cash transfer program update workflows', () => { before(() => { cy.login() cy.createProgram(programCode, programName, maxBeneficiaries, programType) + cy.logout() }) after(() => { cy.deleteProgram(programName) }) + beforeEach(() => { + cy.login() + }) + it('Updates a household program', function () { const updatedProgramCode = 'E2EGCP42' const updatedMaxBeneficiaries = "222" const updatedInstitution = "Family Support Services" const updatedDescription = "A functional program" + cy.visit('/front/benefitPlans'); cy.openProgramForEditFromList(programName) cy.assertMuiInput('Code', programCode) @@ -253,6 +264,7 @@ describe('Cash transfer program update workflows', () => { let projectPath = null before(() => { + cy.login() cy.createProject( programName, projectName, treePlantingActivity, regionName, districtName, targetBeneficiaries, workingDays, @@ -260,6 +272,7 @@ describe('Cash transfer program update workflows', () => { cy.url().then((currentUrl) => { projectPath = new URL(currentUrl).pathname; }); + cy.logout() }) it('Updates a project', function () { @@ -313,7 +326,6 @@ describe('Cash transfer program update workflows', () => { .invoke('val') .then((name) => { cy.deleteProject(projectPath); - cy.reload() cy.contains('button', 'Projects').click() cy.contains('label', 'Show Deleted').click() diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 2728f41..bfbff72 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -21,6 +21,7 @@ Cypress.Commands.add('login', () => { Cypress.Commands.add('logout', () => { cy.visit('/front'); cy.get('button[title="Log out"]').click() + cy.contains('label', 'Username') }) Cypress.Commands.add('loginAdminInterface', () => { @@ -134,17 +135,13 @@ Cypress.Commands.add('deleteProject', (projectPath) => { cy.get('button[title="Delete"]').click(); cy.contains('button', 'Ok').click(); - // Wait for deletion to complete - cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') - cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + // Check redirect + cy.location('pathname').should('not.include', projectPath); // Check last journal message cy.get('ul.MuiList-root li').first().click() cy.contains(`Delete project`).should('exist') cy.contains('Failed to delete').should('not.exist') - - // Check redirect - cy.location('pathname').should('not.include', projectPath); }) Cypress.Commands.add('createProject', ( From a75b81e03055d743d74150aa0b10dc45512f73ae Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 2 Oct 2025 16:27:06 -0400 Subject: [PATCH 27/33] =?UTF-8?q?Individual=20program=E2=80=99s=20Project?= =?UTF-8?q?=20create/update/delete/restore=20E2E=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/e2e/cash-trasfer.cy.js | 131 ++++++++++++++++++++++++++++++++- cypress/support/commands.js | 4 +- 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index c1f53a7..ec6d6f4 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -151,6 +151,135 @@ describe('Cash transfer program update workflows', () => { cy.assertMuiSelectValue('Confirm Filters', 'Contains') cy.assertMuiInput('Value', 'prim') }) + + it('Creates a project under an individual program', function () { + const projectName = `E2E Individual Training Project - ${getTimestamp()}` + const regionName = 'R1 Region 1' + const targetBeneficiaries = "100" + const workingDays = "60" + + cy.createProject( + programName, projectName, treePlantingActivity, + regionName, null, targetBeneficiaries, workingDays, + ) + + cy.reload() + + cy.assertMuiInput('Name', projectName) + cy.assertMuiInput('Activity', treePlantingActivity) + cy.assertMuiInput('Location', regionName) + cy.assertMuiInput('Target Beneficiaries', targetBeneficiaries) + cy.assertMuiInput('Working Days', workingDays) + }) + + describe('Given a project and an individual program', function () { + const projectName = `E2E Existing Individual Project - ${getTimestamp()}` + const regionName = 'R2 Tahida' + const districtName = 'R2D1 Rajo' + const targetBeneficiaries = "20" + const workingDays = "10" + let projectPath = null + + before(() => { + cy.login() + cy.createProject( + programName, projectName, treePlantingActivity, + regionName, districtName, targetBeneficiaries, workingDays, + ) + cy.url().then((currentUrl) => { + projectPath = new URL(currentUrl).pathname; + }); + cy.logout() + }) + + it('Updates a project', function () { + cy.visit(projectPath) + cy.assertMuiInput('Name', projectName) + + const projectNameUpdated = `${projectName} Updated` + cy.enterMuiInput('Name', projectNameUpdated) + + cy.chooseMuiAutocomplete('Activity', riverCleaningActivity) + + const regionNameUpdated = 'R1 Region 1' + const districtNameUpdated = 'R1D1 District 1' + const municipalityName = 'R1D1M2 Jamu' + cy.chooseMuiAutocomplete('Location', regionNameUpdated) + cy.contains('li', districtNameUpdated).click() + cy.contains('li', municipalityName).click() + + const targetBeneficiariesUpdated = "20" + cy.enterMuiInput('Target Beneficiaries', targetBeneficiariesUpdated) + + const workingDaysUpdated = "10" + cy.enterMuiInput('Working Days', workingDaysUpdated) + + cy.get('[title="Save"] button').click() + + // Wait for update to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Update project ${projectName}`).should('exist') + cy.contains('Failed to update').should('not.exist') + + cy.visit(projectPath) + + cy.assertMuiInput('Name', projectNameUpdated) + cy.assertMuiInput('Activity', riverCleaningActivity) + cy.assertMuiInput('Location', municipalityName) + cy.assertMuiInput('Target Beneficiaries', targetBeneficiariesUpdated) + cy.assertMuiInput('Working Days', workingDaysUpdated) + }) + + it('Deletes and restores a project', function () { + cy.visit(projectPath) + cy.contains('Beneficiaries Assigned') + cy.contains('label', 'Name') + .siblings('.MuiInputBase-root') + .find('input') + .invoke('val') + .then((name) => { + cy.deleteProject(projectPath); + + cy.contains('button', 'Projects').click() + cy.contains('label', 'Show Deleted').click() + cy.contains('button', 'Search').click() + + cy.contains('td', name) + .parent('tr').within(() => { + cy.get('button[title="Edit"]').click({force: true}); + }) + + cy.assertMuiInputDisabled('Name', name) + cy.assertMuiInputDisabled('Activity') + cy.assertMuiInputDisabled('Location') + cy.assertMuiInputDisabled('Target Beneficiaries') + cy.assertMuiInputDisabled('Working Days') + cy.assertMuiInputDisabled('Status') + cy.assertMuiInputDisabled('Program') + + cy.get('button[title="Undo Delete"]').click() + cy.contains('button', 'Ok').click(); + + // Wait for undo to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist') + cy.get('ul.MuiList-root li div[role="progressbar"]').should('not.exist') + + // Check last journal message + cy.get('ul.MuiList-root li').first().click() + cy.contains(`Restore project`).should('exist') + cy.contains('Failed to restore').should('not.exist') + + cy.reload() + + cy.contains('button', 'Projects').click() + cy.contains(name).should('exist'); + }); + }) + }) }) describe('Household program', () => { @@ -256,7 +385,7 @@ describe('Cash transfer program update workflows', () => { }) describe('Given a project and a household program', function () { - const projectName = `E2E Existing Project - ${getTimestamp()}` + const projectName = `E2E Existing Group Project - ${getTimestamp()}` const regionName = 'R2 Tahida' const districtName = 'R2D1 Rajo' const targetBeneficiaries = "20" diff --git a/cypress/support/commands.js b/cypress/support/commands.js index bfbff72..980133a 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -164,7 +164,9 @@ Cypress.Commands.add('createProject', ( cy.chooseMuiAutocomplete('Activity', activityName) cy.chooseMuiAutocomplete('Location', regionName) - cy.contains('li', districtName).click() + if (districtName) { + cy.contains('li', districtName).click() + } cy.enterMuiInput('Target Beneficiaries', targetBeneficiaries) From 60320a76e5980be86853b0a7f7c5e468e87706c5 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Wed, 15 Oct 2025 11:39:56 -0400 Subject: [PATCH 28/33] Ensure individual csv group codes are different across E2E runs Also give time for group creation --- .gitignore | 1 + cypress.config.js | 70 ++++++++++++++++++++++++---------- cypress/e2e/cash-trasfer.cy.js | 2 + cypress/support/commands.js | 13 ++++--- 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index cc4cf8c..2cdb405 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ openimis-dist_dkr.code-workspace node_modules/ cypress/screenshots/ cypress/downloads/ +cypress/fixtures/tmp_individuals.csv diff --git a/cypress.config.js b/cypress.config.js index f253271..c3c5c27 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -56,29 +56,57 @@ module.exports = defineConfig({ downloadsFolder: 'cypress/downloads', setupNodeEvents(on, config) { on('task', { - checkSetup() { - return fs.existsSync(serverFlagPath) - }, - completeSetup() { - const url = `${config.baseUrl}/api/graphql` - return waitForServerToStart(url) - .then(() => { - console.log('Server is ready!') - fs.writeFileSync(serverFlagPath, new Date().toString()) - return null - }) - .catch(error => { - console.error('Failed to start server:') - console.error(error.stack); - return reject(error); - }) - }, - removeSetupFile() { - if (fs.existsSync(serverFlagPath)) { - fs.unlinkSync(serverFlagPath) + checkSetup() { + return fs.existsSync(serverFlagPath) + }, + completeSetup() { + const url = `${config.baseUrl}/api/graphql` + return waitForServerToStart(url) + .then(() => { + console.log('Server is ready!') + fs.writeFileSync(serverFlagPath, new Date().toString()) + return null + }) + .catch(error => { + console.error('Failed to start server:') + console.error(error.stack); + return reject(error); + }) + }, + removeSetupFile() { + if (fs.existsSync(serverFlagPath)) { + fs.unlinkSync(serverFlagPath) + } + return null + }, + updateCSV() { + const fixturePath = path.join(config.fixturesFolder, 'individuals.csv'); + const tmpPath = path.join(config.fixturesFolder, 'tmp_individuals.csv'); + const csv = fs.readFileSync(fixturePath, 'utf8'); + + const lines = csv.trim().split('\n'); + const header = lines[0].split(','); + const groupCodeIdx = header.indexOf('group_code'); + + // map old group_code → new group_code + const groupMap = {}; + + const newLines = [lines[0]]; // keep header + for (let i = 1; i < lines.length; i++) { + const row = lines[i].split(','); + const oldCode = row[groupCodeIdx]; + + if (!groupMap[oldCode]) { + groupMap[oldCode] = Math.random().toString(36).substring(2, 8).toUpperCase(); } - return null + + row[groupCodeIdx] = groupMap[oldCode]; + newLines.push(row.join(',')); } + + fs.writeFileSync(tmpPath, newLines.join('\n')); + return null; + } }) }, }, diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index ec6d6f4..6d01acd 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -520,6 +520,8 @@ describe('Individuals and groups/households', () => { }); }); + cy.wait(10000) // group creation takes time + cy.visit('/front/groups') cy.getItemCount("Group").then(newCount => { cy.get('@initialGroupCount').then(initial => { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 980133a..274ad4d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -330,14 +330,17 @@ Cypress.Commands.add( }) Cypress.Commands.add('uploadIndividualsCSV', () => { - cy.contains('li', 'UPLOAD').click() + cy.task('updateCSV').then(() => { + cy.contains('li', 'UPLOAD').click() - cy.get('input[type="file"]').attachFile('individuals.csv'); + cy.get('input[type="file"]').attachFile('tmp_individuals.csv'); - cy.chooseMuiSelect('Workflow', 'Python Import Individuals') - cy.contains('button', 'Upload Individuals').click(); + cy.chooseMuiSelect('Workflow', 'Python Import Individuals') + cy.contains('button', 'Upload Individuals').click(); - cy.contains('button', 'Upload Individuals').should('not.exist') + cy.contains('button', 'Upload Individuals').should('not.exist') + cy.contains('button', 'Uploading...').should('be.disabled') + }) }) Cypress.Commands.add('enterMuiInput', (label, value, inputTag='input') => { From 7d343929ec92aed817c1d9d06cbf62fc1b9f9a81 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Thu, 16 Oct 2025 12:21:16 -0400 Subject: [PATCH 29/33] Add E2E test for group program enrollment --- cypress.config.js | 28 +++++++--- cypress/e2e/cash-trasfer.cy.js | 93 +++++++++++++++++++++++++++++++++- cypress/support/commands.js | 62 +++++++++++++++++++++-- 3 files changed, 173 insertions(+), 10 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index c3c5c27..4b032d9 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -79,21 +79,37 @@ module.exports = defineConfig({ } return null }, - updateCSV() { + updateCSV({ numIndividuals } = {}) { const fixturePath = path.join(config.fixturesFolder, 'individuals.csv'); const tmpPath = path.join(config.fixturesFolder, 'tmp_individuals.csv'); const csv = fs.readFileSync(fixturePath, 'utf8'); const lines = csv.trim().split('\n'); - const header = lines[0].split(','); - const groupCodeIdx = header.indexOf('group_code'); + const header = lines[0]; + const rows = lines.slice(1); + const headerCols = header.split(','); + const groupCodeIdx = headerCols.indexOf('group_code'); // map old group_code → new group_code const groupMap = {}; - const newLines = [lines[0]]; // keep header - for (let i = 1; i < lines.length; i++) { - const row = lines[i].split(','); + // Subset or duplicate rows + let adjustedRows = []; + if (numIndividuals && numIndividuals > 0) { + if (numIndividuals <= rows.length) { + adjustedRows = rows.slice(0, numIndividuals); + } else { + const times = Math.floor(numIndividuals / rows.length); + const remainder = numIndividuals % rows.length; + adjustedRows = Array(times).fill(rows).flat().concat(rows.slice(0, remainder)); + } + } else { + adjustedRows = rows; + } + + const newLines = [header]; + for (const line of adjustedRows) { + const row = line.split(','); const oldCode = row[groupCodeIdx]; if (!groupMap[oldCode]) { diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 6d01acd..a30475b 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -337,7 +337,7 @@ describe('Cash transfer program update workflows', () => { ) }) - it('configures default enrollment criteria for a household program', function () { + it('configures default enrollment criteria for a household program and enrolls households', function () { cy.visit('/front/benefitPlans'); cy.openProgramForEditFromList(programName) @@ -361,6 +361,97 @@ describe('Cash transfer program update workflows', () => { cy.assertMuiSelectValue('Field', 'Number of children') cy.assertMuiSelectValue('Confirm Filters', 'Greater than or equal to') cy.assertMuiInput('Value', '2') + + cy.ensureSufficientHouseholds(20) + + cy.visit('/front/groups') + cy.contains('a', 'ENROLLMENT').click() + cy.chooseMuiAutocomplete('BenefitPlan', programName) + cy.chooseMuiSelect('Status', 'ACTIVE') + + cy.assertMuiSelectValue('Field', 'Number of children') + cy.assertMuiSelectValue('Confirm Filters', 'Greater than or equal to') + cy.assertMuiInput('Value', '2') + + cy.contains('button', 'Preview Enrollment Process').click() + cy.contains('h6', 'Number Of Selected Groups') + .next('p') + .invoke('text') + .then((text) => { + const num = Number(text.trim()); + cy.wrap(num).as('numGroupsEnrolled'); + expect(num).to.be.greaterThan(0); + }); + + cy.contains('button', 'Confirm Enrollment Process').click() + + // confirmation dialog + cy.contains('h2', 'Confirm Enrollment Process') + cy.contains('button', 'Ok').click() + + // The enrollment page doesn't trigger journal update correctly + // so we'd have to reload the page here + cy.reload() + + // Verify enrollment in expanded journal drawer + cy.get('.MuiDrawer-paperAnchorRight button') + .first() + .click(); + + cy.get('ul.MuiList-root li') + .first() + .should('contain', 'Enrollment has been confirmed'); + + // maker-checker approves enrollment + cy.ensurePermissiveTaskGroup() + cy.visit('/front/AllTasks') + cy.contains('tfoot', 'Rows Per Page') + cy.get('tr') + .filter((_, tr) => ( + Cypress.$(tr).find('td:contains("import_valid_items")').length > 0 && + Cypress.$(tr).find('td:contains("RECEIVED")').length > 0 + )) + .first() + .within(() => { + cy.get('td') + .contains(new RegExp(`^${programCode}\\b`)) + .should('exist'); + + cy.get('button[title="View details"]').click(); + }); + + cy.contains('Import Valid Items Task') + cy.chooseMuiAutocomplete('Task Group', 'any'); + cy.get('[title="Save changes"] button').click(); + + cy.contains('div', 'Accept All') + .find('button') + .click(); + + cy.contains('Beneficiary Upload Confirmation') + cy.contains('button', 'Continue').click() + cy.contains('div', 'Accept All') + .find('button').should('be.disabled') + + cy.visit('/front/AllTasks') + cy.get('tr') + .filter((_, tr) => ( + Cypress.$(tr).find('td:contains("import_valid_items")').length > 0 && + Cypress.$(tr).find(`td:contains("${programCode}")`).length > 0 + )) + .first() + .within(() => { + cy.contains('td', 'COMPLETED') + }); + + cy.visit('/front/benefitPlans'); + cy.openProgramForEditFromList(programName) + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', 'Active').click() + + cy.get('@numGroupsEnrolled').then((count) => { + cy.contains(`${count} Group Beneficiaries`) + }); }) it('Creates a project under a household program', function () { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 274ad4d..9341385 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -329,8 +329,8 @@ Cypress.Commands.add( }) }) -Cypress.Commands.add('uploadIndividualsCSV', () => { - cy.task('updateCSV').then(() => { +Cypress.Commands.add('uploadIndividualsCSV', (numIndividuals) => { + cy.task('updateCSV', { numIndividuals }).then(() => { cy.contains('li', 'UPLOAD').click() cy.get('input[type="file"]').attachFile('tmp_individuals.csv'); @@ -343,6 +343,62 @@ Cypress.Commands.add('uploadIndividualsCSV', () => { }) }) +Cypress.Commands.add('ensureSufficientHouseholds', (expectedNumGroups) => { + cy.visit('/front/groups') + cy.getItemCount('Group').then(numGroups => { + const numGroupsToAdd = expectedNumGroups - numGroups; + if (numGroupsToAdd <= 0) { + Cypress.log({ + name: 'ensureSufficientHouseholds', + message: `Found ${numGroups} which is more than ${expectedNumGroups}, no need to add additional`, + }); + return + } + + const numIndividualsToAdd = numGroupsToAdd * 5 + cy.visit('/front/individuals') + cy.uploadIndividualsCSV(numIndividualsToAdd) + + cy.wait(100*numGroupsToAdd) // group creation takes time + + cy.visit('/front/groups') + cy.getItemCount("Group").then(newCount => { + expect(newCount).to.be.gte(expectedNumGroups); + }); + }) +}) + +Cypress.Commands.add('ensurePermissiveTaskGroup', () => { + cy.visit('/front/tasks/groups'); + + cy.contains('tfoot', 'Rows Per Page') + cy.get('table').then(($table) => { + const hasAnyRow = $table.find('tbody tr td:first-child') + .toArray() + .some((td) => td.innerText.trim() === 'any'); + + if (!hasAnyRow) { + cy.get('[title="Create"] button').click(); + + cy.enterMuiInput('Code', 'any'); + cy.chooseMuiSelect('Policy Status', 'ANY'); + cy.chooseMuiAutocomplete('Task Executors', 'Admin Admin'); + + cy.get('[title="Save changes"] button').click(); + + // Wait for creation to complete + cy.get('ul.MuiList-root li div[role="progressbar"]').should('exist'); + + // Verify creation in expanded journal drawer + cy.get('.MuiDrawer-paperAnchorRight button').first().click(); + + cy.get('ul.MuiList-root li').first().should('contain', 'Create task group'); + } else { + cy.log('Permissive task group named any already exists — skipping creation.'); + } + }); +}); + Cypress.Commands.add('enterMuiInput', (label, value, inputTag='input') => { cy.contains('label', label) .siblings('.MuiInputBase-root') @@ -392,7 +448,7 @@ Cypress.Commands.add('chooseMuiAutocomplete', (label, value) => { .find('input') .click() - cy.contains('li', value).click() + cy.contains('.MuiAutocomplete-popper li', value).click() }) Cypress.Commands.add('setModuleConfig', (moduleName, configFixtureFile) => { From f5cff20e0b135decaca7009ff91f5cf6553ab010 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 17 Oct 2025 10:26:29 -0400 Subject: [PATCH 30/33] E2E test for household enrollment into a project --- cypress/e2e/cash-trasfer.cy.js | 284 +++++++++++++++++---------------- cypress/support/commands.js | 133 ++++++++++++++- 2 files changed, 275 insertions(+), 142 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index a30475b..f332bd7 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -67,6 +67,7 @@ describe('Cash transfer program update workflows', () => { // Disable maker checker cy.loginAdminInterface() cy.setModuleConfig('social_protection', 'social-protection-config.json') + cy.setModuleConfig('individual', 'individual-config-minimal.json') cy.createActivities(activities) cy.logoutAdminInterface() }) @@ -127,29 +128,17 @@ describe('Cash transfer program update workflows', () => { }) it('configures default enrollment criteria for an individual program', function () { - cy.visit('/front/benefitPlans'); - cy.openProgramForEditFromList(programName) - - cy.contains('button', 'Beneficiaries').click() - cy.contains('button', 'Potential').click() - - cy.contains('Potential Beneficiary Enrollment Criteria') - cy.contains('button', 'Add Filters').click() - - cy.chooseMuiSelect('Field', 'Educated level') - cy.chooseMuiSelect('Confirm Filters', 'Contains') - cy.enterMuiInput('Value', 'prim') - cy.get('[title="Save changes"] button').click() - - cy.checkProgramUpdateCompleted() - cy.reload() - - cy.contains('button', 'Beneficiaries').click() - cy.contains('button', 'Potential').click() + const criterionField = 'Educated level' + const criterionFilter = 'Contains' + const criterionValue = 'prim' - cy.assertMuiSelectValue('Field', 'Educated level') - cy.assertMuiSelectValue('Confirm Filters', 'Contains') - cy.assertMuiInput('Value', 'prim') + cy.configureDefaultEnrollmentCriteria( + programName, + 'Potential', + criterionField, + criterionFilter, + criterionValue, + ) }) it('Creates a project under an individual program', function () { @@ -283,7 +272,7 @@ describe('Cash transfer program update workflows', () => { }) describe('Household program', () => { - const programCode = 'E2EGCPU' + let programCode = 'E2EGCPU' const programName = 'E2E Household Cash Program Updated' const maxBeneficiaries = "200" const programType = "GROUP" @@ -335,123 +324,31 @@ describe('Cash transfer program update workflows', () => { updatedInstitution, updatedDescription, ) + + // Update in case other test needs the programCode + programCode = updatedProgramCode }) it('configures default enrollment criteria for a household program and enrolls households', function () { - cy.visit('/front/benefitPlans'); - cy.openProgramForEditFromList(programName) - - cy.contains('button', 'Beneficiaries').click() - cy.contains('button', 'Active').click() - - cy.contains('Active Beneficiary Enrollment Criteria') - cy.contains('button', 'Add Filters').click() + const criterionField = 'Number of children' + const criterionFilter = 'Greater than or equal to' + const criterionValue = '2' - cy.chooseMuiSelect('Field', 'Number of children') - cy.chooseMuiSelect('Confirm Filters', 'Greater than or equal to') - cy.enterMuiInput('Value', '2') - cy.get('[title="Save changes"] button').click() - - cy.checkProgramUpdateCompleted() - cy.reload() - - cy.contains('button', 'Beneficiaries').click() - cy.contains('button', 'Active').click() - - cy.assertMuiSelectValue('Field', 'Number of children') - cy.assertMuiSelectValue('Confirm Filters', 'Greater than or equal to') - cy.assertMuiInput('Value', '2') - - cy.ensureSufficientHouseholds(20) - - cy.visit('/front/groups') - cy.contains('a', 'ENROLLMENT').click() - cy.chooseMuiAutocomplete('BenefitPlan', programName) - cy.chooseMuiSelect('Status', 'ACTIVE') - - cy.assertMuiSelectValue('Field', 'Number of children') - cy.assertMuiSelectValue('Confirm Filters', 'Greater than or equal to') - cy.assertMuiInput('Value', '2') - - cy.contains('button', 'Preview Enrollment Process').click() - cy.contains('h6', 'Number Of Selected Groups') - .next('p') - .invoke('text') - .then((text) => { - const num = Number(text.trim()); - cy.wrap(num).as('numGroupsEnrolled'); - expect(num).to.be.greaterThan(0); - }); - - cy.contains('button', 'Confirm Enrollment Process').click() - - // confirmation dialog - cy.contains('h2', 'Confirm Enrollment Process') - cy.contains('button', 'Ok').click() - - // The enrollment page doesn't trigger journal update correctly - // so we'd have to reload the page here - cy.reload() - - // Verify enrollment in expanded journal drawer - cy.get('.MuiDrawer-paperAnchorRight button') - .first() - .click(); - - cy.get('ul.MuiList-root li') - .first() - .should('contain', 'Enrollment has been confirmed'); - - // maker-checker approves enrollment - cy.ensurePermissiveTaskGroup() - cy.visit('/front/AllTasks') - cy.contains('tfoot', 'Rows Per Page') - cy.get('tr') - .filter((_, tr) => ( - Cypress.$(tr).find('td:contains("import_valid_items")').length > 0 && - Cypress.$(tr).find('td:contains("RECEIVED")').length > 0 - )) - .first() - .within(() => { - cy.get('td') - .contains(new RegExp(`^${programCode}\\b`)) - .should('exist'); - - cy.get('button[title="View details"]').click(); - }); - - cy.contains('Import Valid Items Task') - cy.chooseMuiAutocomplete('Task Group', 'any'); - cy.get('[title="Save changes"] button').click(); - - cy.contains('div', 'Accept All') - .find('button') - .click(); - - cy.contains('Beneficiary Upload Confirmation') - cy.contains('button', 'Continue').click() - cy.contains('div', 'Accept All') - .find('button').should('be.disabled') - - cy.visit('/front/AllTasks') - cy.get('tr') - .filter((_, tr) => ( - Cypress.$(tr).find('td:contains("import_valid_items")').length > 0 && - Cypress.$(tr).find(`td:contains("${programCode}")`).length > 0 - )) - .first() - .within(() => { - cy.contains('td', 'COMPLETED') - }); - - cy.visit('/front/benefitPlans'); - cy.openProgramForEditFromList(programName) - cy.contains('button', 'Beneficiaries').click() - cy.contains('button', 'Active').click() + cy.configureDefaultEnrollmentCriteria( + programName, + 'Active', + criterionField, + criterionFilter, + criterionValue, + ) - cy.get('@numGroupsEnrolled').then((count) => { - cy.contains(`${count} Group Beneficiaries`) - }); + cy.enrollGroupBeneficiariesIntoProgram( + programName, + programCode, + criterionField, + criterionFilter, + criterionValue, + ) }) it('Creates a project under a household program', function () { @@ -477,8 +374,7 @@ describe('Cash transfer program update workflows', () => { describe('Given a project and a household program', function () { const projectName = `E2E Existing Group Project - ${getTimestamp()}` - const regionName = 'R2 Tahida' - const districtName = 'R2D1 Rajo' + const regionName = 'R1 Region 1' const targetBeneficiaries = "20" const workingDays = "10" let projectPath = null @@ -487,7 +383,7 @@ describe('Cash transfer program update workflows', () => { cy.login() cy.createProject( programName, projectName, treePlantingActivity, - regionName, districtName, targetBeneficiaries, workingDays, + regionName, null, targetBeneficiaries, workingDays, ) cy.url().then((currentUrl) => { projectPath = new URL(currentUrl).pathname; @@ -495,6 +391,118 @@ describe('Cash transfer program update workflows', () => { cy.logout() }) + it('Enrolls active households into a project', function () { + const criterionField = 'Number of children' + const criterionFilter = 'Less than' + const criterionValue = '6' + + cy.configureDefaultEnrollmentCriteria( + programName, + 'Active', + criterionField, + criterionFilter, + criterionValue, + ) + + cy.enrollGroupBeneficiariesIntoProgram( + programName, + programCode, + criterionField, + criterionFilter, + criterionValue, + ) + + cy.contains('tfoot', 'Rows Per Page').should('be.visible') + cy.chooseMuiSelect('Region', regionName) + cy.contains('button', 'Search').click() + cy.contains('Loading...') + cy.contains('tfoot', 'Rows Per Page').should('be.visible') + cy.get('table').within(() => { + cy.get('th').then(($ths) => { + const districtIndex = [...$ths].findIndex( + (th) => th.innerText.trim() === 'District' + ); + cy.get(`tbody tr:first td:nth-child(${districtIndex + 1})`) + .invoke('text') + .then((districtValue) => { + cy.wrap(districtValue.trim()).as('activeDistrict'); + }); + }); + }); + + cy.visit(projectPath) + cy.contains('h6', '0 Beneficiaries Assigned') + cy.contains('button', 'Assign Beneficiaries').click() + cy.get('[role="progressbar"]').should('exist') + cy.get('[role="progressbar"]').should('not.exist') + + cy.contains('h2', 'Assign Active Beneficiaries') + cy.contains('h6', '0 active beneficiaries selected') + cy.get('[role="dialog"] [title="Previous Page"]') + .next('span') + .invoke('text') + .then((text) => { + // extract 304 from text e.g. 1-10 of 304 + const match = text.match(/of\s+(\d+)/); + cy.wrap(Number(match[1])).as('numCandidates'); + }); + + // filter beneficiaries by district + cy.get('@activeDistrict').then((district) => { + cy.get('[role="dialog"] input[aria-label="filter data by District"]') + .clear() + .type(district) + cy.get('[role="progressbar"]').should('exist') + cy.get('[role="progressbar"]').should('not.exist') + }); + + cy.get('@numCandidates').then((prevTotal) => { + cy.get('[role="dialog"] [title="Previous Page"]') + .next('span') + .invoke('text') + .then((text) => { + const match = text.match(/of\s+(\d+)/); + const total = Number(match[1]); + expect(prevTotal - total).to.gt(0); + }); + }); + + // assign all matching beneficiaries from the first page + cy.get('th input[type="checkbox"]').check() + cy.contains('h6', '0 active beneficiaries selected').should('not.exist') + cy.get('[role="dialog"] h6') + .invoke('text') + .then((text) => { + // 1 active beneficiaries selected + const match = text.match(/(\d+) active beneficiaries selected/); + cy.wrap(match[1]).as('assignedProjectBeneficiaries'); + }); + + cy.contains('button', 'Save').click() + cy.get('[role="progressbar"]').should('exist') + cy.get('[role="progressbar"]').should('not.exist') + + cy.get('@assignedProjectBeneficiaries').then((expectedNumAssigned) => { + cy.contains('h6', `${expectedNumAssigned} Beneficiaries Assigned`) + }) + + // Check that all assigned beneficiaries share the same district + cy.get('table').first().within(() => { + cy.get('th').then(($ths) => { + const districtIndex = [...$ths].findIndex( + (th) => th.innerText.trim().includes('District') + ); + cy.get('@activeDistrict').then((activeDistrict) => { + cy.get(`tbody tr td:nth-child(${districtIndex + 1})`) + .each(($td, i) => { + // skip the filter row + if (i>0) expect($td.text().trim()).to.eq(activeDistrict); + }); + }); + }); + }); + }) + it('Updates a project', function () { cy.visit(projectPath) cy.assertMuiInput('Name', projectName) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 9341385..96140c9 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -359,7 +359,7 @@ Cypress.Commands.add('ensureSufficientHouseholds', (expectedNumGroups) => { cy.visit('/front/individuals') cy.uploadIndividualsCSV(numIndividualsToAdd) - cy.wait(100*numGroupsToAdd) // group creation takes time + cy.wait(100*numIndividualsToAdd) // group creation takes time cy.visit('/front/groups') cy.getItemCount("Group").then(newCount => { @@ -371,7 +371,7 @@ Cypress.Commands.add('ensureSufficientHouseholds', (expectedNumGroups) => { Cypress.Commands.add('ensurePermissiveTaskGroup', () => { cy.visit('/front/tasks/groups'); - cy.contains('tfoot', 'Rows Per Page') + cy.contains('Task Groups Found') cy.get('table').then(($table) => { const hasAnyRow = $table.find('tbody tr td:first-child') .toArray() @@ -399,6 +399,130 @@ Cypress.Commands.add('ensurePermissiveTaskGroup', () => { }); }); +Cypress.Commands.add('configureDefaultEnrollmentCriteria', ( + programName, status, criterionField, criterionFilter, criterionValue, +) => { + cy.visit('/front/benefitPlans'); + cy.openProgramForEditFromList(programName) + + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', status).click() + + cy.contains(`${status} Beneficiary Enrollment Criteria`) + cy.contains('button', 'Add Filters').click() + + cy.chooseMuiSelect('Field', criterionField) + cy.chooseMuiSelect('Confirm Filters', criterionFilter) + cy.enterMuiInput('Value', criterionValue) + cy.get('[title="Save changes"] button').click() + + cy.checkProgramUpdateCompleted() + cy.reload() + + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', status).click() + + cy.assertMuiSelectValue('Field', criterionField) + cy.assertMuiSelectValue('Confirm Filters', criterionFilter) + cy.assertMuiInput('Value', criterionValue) +}); + +Cypress.Commands.add('enrollGroupBeneficiariesIntoProgram', ( + programName, programCode, criterionField, criterionFilter, criterionValue, +) => { + cy.ensureSufficientHouseholds(20) + + cy.visit('/front/groups') + cy.contains('a', 'ENROLLMENT').click() + cy.chooseMuiAutocomplete('BenefitPlan', programName) + cy.chooseMuiSelect('Status', 'ACTIVE') + + cy.assertMuiSelectValue('Field', criterionField) + cy.assertMuiSelectValue('Confirm Filters', criterionFilter) + cy.assertMuiInput('Value', criterionValue) + + cy.contains('button', 'Preview Enrollment Process').click() + cy.contains('h6', 'Number Of Selected Groups') + .next('p') + .invoke('text') + .then((text) => { + const num = Number(text.trim()); + cy.wrap(num).as('numGroupsEnrolled'); + expect(num).to.be.greaterThan(0); + }); + + cy.contains('button', 'Confirm Enrollment Process').click() + + // confirmation dialog + cy.contains('h2', 'Confirm Enrollment Process') + cy.contains('button', 'Ok').click() + + // The enrollment page doesn't trigger journal update correctly + // so we'd have to reload the page here + cy.reload() + + // Verify enrollment in expanded journal drawer + cy.get('.MuiDrawer-paperAnchorRight button') + .first() + .click(); + + cy.get('ul.MuiList-root li') + .first() + .should('contain', 'Enrollment has been confirmed'); + + // maker-checker approves enrollment + cy.ensurePermissiveTaskGroup() + cy.visit('/front/AllTasks') + cy.contains('tfoot', 'Rows Per Page') + cy.get('tr') + .filter((_, tr) => ( + Cypress.$(tr).find('td:contains("import_valid_items")').length > 0 && + Cypress.$(tr).find('td:contains("RECEIVED")').length > 0 + )) + .first() + .within(() => { + cy.get('td') + .contains(new RegExp(`^${programCode}\\b`)) + .should('exist'); + + cy.get('button[title="View details"]').click(); + }); + + cy.contains('Import Valid Items Task') + cy.chooseMuiAutocomplete('Task Group', 'any'); + cy.get('[title="Save changes"] button').click(); + + cy.contains('div', 'Accept All') + .find('button') + .click(); + + cy.contains('Beneficiary Upload Confirmation') + cy.contains('button', 'Continue').click() + cy.contains('div', 'Accept All') + .find('button').should('be.disabled') + + cy.visit('/front/AllTasks') + cy.get('tr') + .filter((_, tr) => ( + Cypress.$(tr).find('td:contains("import_valid_items")').length > 0 && + Cypress.$(tr).find(`td:contains("${programCode}")`).length > 0 + )) + .first() + .within(() => { + cy.contains('td', 'COMPLETED') + }); + + cy.visit('/front/benefitPlans'); + cy.openProgramForEditFromList(programName) + cy.contains('button', 'Beneficiaries').click() + cy.contains('button', 'Active').click() + + cy.get('@numGroupsEnrolled').then((count) => { + cy.contains(`${count} Group Beneficiaries`) + }); +}) + + Cypress.Commands.add('enterMuiInput', (label, value, inputTag='input') => { cy.contains('label', label) .siblings('.MuiInputBase-root') @@ -414,7 +538,8 @@ Cypress.Commands.add('chooseMuiSelect', (label, value) => { .find('[role="button"]') .click() - cy.contains('li', value).click() + cy.contains('[role="listbox"] li', value).as('option') + cy.get('@option').click() }) Cypress.Commands.add('assertMuiInput', (label, value, inputTag='input') => { @@ -448,7 +573,7 @@ Cypress.Commands.add('chooseMuiAutocomplete', (label, value) => { .find('input') .click() - cy.contains('.MuiAutocomplete-popper li', value).click() + cy.contains('[role="menu"] li, [role="presentation"] li', value).click(); }) Cypress.Commands.add('setModuleConfig', (moduleName, configFixtureFile) => { From b669d257e0238fdbd6c9c70c387a5b0ba8b61b6b Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Sat, 18 Oct 2025 07:30:57 -0400 Subject: [PATCH 31/33] Add E2E test for individual enrollment into a program --- cypress/e2e/cash-trasfer.cy.js | 13 ++++- cypress/support/commands.js | 93 +++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index f332bd7..07f586e 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -127,7 +127,7 @@ describe('Cash transfer program update workflows', () => { ) }) - it('configures default enrollment criteria for an individual program', function () { + it('configures default enrollment criteria for an individual program and enrolls individuals', function () { const criterionField = 'Educated level' const criterionFilter = 'Contains' const criterionValue = 'prim' @@ -139,6 +139,15 @@ describe('Cash transfer program update workflows', () => { criterionFilter, criterionValue, ) + + cy.enrollIndividualBeneficiariesIntoProgram( + programName, + programCode, + 'Potential', + criterionField, + criterionFilter, + criterionValue, + ) }) it('Creates a project under an individual program', function () { @@ -345,6 +354,7 @@ describe('Cash transfer program update workflows', () => { cy.enrollGroupBeneficiariesIntoProgram( programName, programCode, + 'Active', criterionField, criterionFilter, criterionValue, @@ -407,6 +417,7 @@ describe('Cash transfer program update workflows', () => { cy.enrollGroupBeneficiariesIntoProgram( programName, programCode, + 'Active', criterionField, criterionFilter, criterionValue, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 96140c9..be1a9b2 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -343,6 +343,30 @@ Cypress.Commands.add('uploadIndividualsCSV', (numIndividuals) => { }) }) +Cypress.Commands.add('ensureSufficientIndividuals', (expectedNumIndividuals) => { + cy.visit('/front/individuals') + cy.getItemCount('Individual').then(count => { + const numToAdd = expectedNumIndividuals - count; + if (numToAdd <= 0) { + Cypress.log({ + name: 'ensureSufficientIndividuals', + message: `Found ${count} which is more than ${expectedNumIndividuals}, no need to add additional`, + }); + return + } + + cy.visit('/front/individuals') + cy.uploadIndividualsCSV(numToAdd) + + cy.wait(100*numToAdd) // group creation takes time + + cy.visit('/front/individuals') + cy.getItemCount("Individual").then(newCount => { + expect(newCount).to.be.gte(expectedNumIndividuals); + }); + }) +}) + Cypress.Commands.add('ensureSufficientHouseholds', (expectedNumGroups) => { cy.visit('/front/groups') cy.getItemCount('Group').then(numGroups => { @@ -427,27 +451,30 @@ Cypress.Commands.add('configureDefaultEnrollmentCriteria', ( cy.assertMuiInput('Value', criterionValue) }); -Cypress.Commands.add('enrollGroupBeneficiariesIntoProgram', ( - programName, programCode, criterionField, criterionFilter, criterionValue, +Cypress.Commands.add('enrollBeneficiariesIntoProgram', ( + programName, + programCode, + status, // Active, Potential etc. + criterionField, + criterionFilter, + criterionValue, + entityName, ) => { - cy.ensureSufficientHouseholds(20) - - cy.visit('/front/groups') - cy.contains('a', 'ENROLLMENT').click() cy.chooseMuiAutocomplete('BenefitPlan', programName) - cy.chooseMuiSelect('Status', 'ACTIVE') + cy.chooseMuiSelect('Status', status.toUpperCase()) cy.assertMuiSelectValue('Field', criterionField) cy.assertMuiSelectValue('Confirm Filters', criterionFilter) cy.assertMuiInput('Value', criterionValue) cy.contains('button', 'Preview Enrollment Process').click() - cy.contains('h6', 'Number Of Selected Groups') + + cy.contains('h6', `Number Of Selected ${entityName}`) .next('p') .invoke('text') .then((text) => { const num = Number(text.trim()); - cy.wrap(num).as('numGroupsEnrolled'); + cy.wrap(num).as('numEnrolled'); expect(num).to.be.greaterThan(0); }); @@ -515,13 +542,55 @@ Cypress.Commands.add('enrollGroupBeneficiariesIntoProgram', ( cy.visit('/front/benefitPlans'); cy.openProgramForEditFromList(programName) cy.contains('button', 'Beneficiaries').click() - cy.contains('button', 'Active').click() + cy.contains('button', status).click() - cy.get('@numGroupsEnrolled').then((count) => { - cy.contains(`${count} Group Beneficiaries`) + cy.get('@numEnrolled').then((count) => { + if (entityName === 'Groups') { + cy.contains(`${count} Group Beneficiaries`) + } else { + cy.contains(`${count} Beneficiaries`) + } }); }) +Cypress.Commands.add('enrollIndividualBeneficiariesIntoProgram', ( + programName, + programCode, + status, + criterionField, + criterionFilter, + criterionValue, +) => { + cy.ensureSufficientIndividuals(100) + + cy.visit('/front/individuals') + cy.contains('a', 'ENROLLMENT').click() + + cy.enrollBeneficiariesIntoProgram( + programName, programCode, status, + criterionField, criterionFilter, criterionValue, 'Individuals' + ) +}) + +Cypress.Commands.add('enrollGroupBeneficiariesIntoProgram', ( + programName, + programCode, + status, + criterionField, + criterionFilter, + criterionValue, +) => { + cy.ensureSufficientHouseholds(20) + + cy.visit('/front/groups') + cy.contains('a', 'ENROLLMENT').click() + + cy.enrollBeneficiariesIntoProgram( + programName, programCode, status, + criterionField, criterionFilter, criterionValue, 'Groups' + ) +}) + Cypress.Commands.add('enterMuiInput', (label, value, inputTag='input') => { cy.contains('label', label) From cd9f6df627565d73566cec0b308bf6fa5e99a652 Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Tue, 21 Oct 2025 12:39:11 -0400 Subject: [PATCH 32/33] E2E test for individual project enrollment & filter --- cypress/e2e/cash-trasfer.cy.js | 112 ++++++++++++++++++++++++++++++++- cypress/support/commands.js | 23 ++++--- 2 files changed, 126 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/cash-trasfer.cy.js b/cypress/e2e/cash-trasfer.cy.js index 07f586e..262c310 100644 --- a/cypress/e2e/cash-trasfer.cy.js +++ b/cypress/e2e/cash-trasfer.cy.js @@ -172,8 +172,8 @@ describe('Cash transfer program update workflows', () => { describe('Given a project and an individual program', function () { const projectName = `E2E Existing Individual Project - ${getTimestamp()}` - const regionName = 'R2 Tahida' - const districtName = 'R2D1 Rajo' + const regionName = 'R1 Region 1' + const districtName = 'R1D2 Jambero' const targetBeneficiaries = "20" const workingDays = "10" let projectPath = null @@ -190,6 +190,114 @@ describe('Cash transfer program update workflows', () => { cy.logout() }) + it('Enrolls active individuals into a project', function () { + const criterionField = 'Able bodied' + const criterionFilter = 'Exact' + const criterionValue = 'False' + + cy.configureDefaultEnrollmentCriteria( + programName, + 'Active', + criterionField, + criterionFilter, + criterionValue, + ) + + cy.enrollIndividualBeneficiariesIntoProgram( + programName, + programCode, + 'Active', + criterionField, + criterionFilter, + criterionValue, + ) + + cy.visit(projectPath) + cy.contains('h6', '0 Beneficiaries Assigned') + cy.contains('button', 'Assign Beneficiaries').click() + cy.get('[role="progressbar"]').should('exist') + cy.get('[role="progressbar"]').should('not.exist') + + cy.contains('h2', 'Assign Active Beneficiaries') + cy.contains('h6', '0 active beneficiaries selected') + cy.get('[role="dialog"] [title="Previous Page"]') + .next('span') + .invoke('text') + .then((text) => { + // extract 304 from text e.g. 1-10 of 304 + const match = text.match(/of\s+(\d+)/); + cy.wrap(Number(match[1])).as('numCandidates'); + }); + + // filter beneficiaries by number of children + cy.contains('[role="dialog"] button', '=').trigger('mouseover'); + cy.contains('li', '<').click(); + cy.get('[role="dialog"] input[type="number"]').clear().type('5'); + + cy.get('[role="progressbar"]').should('exist') + cy.get('[role="progressbar"]').should('not.exist') + + cy.get('@numCandidates').then((prevTotal) => { + cy.get('[role="dialog"] [title="Previous Page"]') + .next('span') + .invoke('text') + .then((text) => { + const match = text.match(/of\s+(\d+)/); + const total = Number(match[1]); + expect(prevTotal - total).to.gt(0); + }); + }); + + // assign all matching beneficiaries from the first page + cy.get('th input[type="checkbox"]').check() + cy.contains('h6', '0 active beneficiaries selected').should('not.exist') + cy.get('[role="dialog"] h6') + .invoke('text') + .then((text) => { + // 1 active beneficiaries selected + const match = text.match(/(\d+) active beneficiaries selected/); + cy.wrap(match[1]).as('assignedProjectBeneficiaries'); + }); + + cy.contains('button', 'Save').click() + cy.get('[role="progressbar"]').should('exist') + cy.get('[role="progressbar"]').should('not.exist') + + cy.get('@assignedProjectBeneficiaries').then((expectedNumAssigned) => { + cy.contains('h6', `${expectedNumAssigned} Beneficiaries Assigned`) + }) + + // Check that all assigned beneficiaries share the project's district + cy.get('table').first().within(() => { + cy.get('th').then(($ths) => { + const districtIndex = [...$ths].findIndex( + (th) => th.innerText.trim().includes('District') + ); + cy.get(`tbody tr td:nth-child(${districtIndex + 1})`) + .each(($td, i) => { + // skip the filter row + if (i>0) expect($td.text().trim()).to.eq('Jambero'); + }); + }); + }); + + // Assigned beneficiaries can be filtered + cy.get('td input[aria-label="filter data by Educated Level"]') + .clear() + .type('Ter') + cy.wait(500) // give the filter results to settle down + cy.get('@assignedProjectBeneficiaries').then((assignedProjectBeneficiaries) => { + cy.get('[title="Previous Page"]') + .next('span') + .invoke('text') + .then((text) => { + const match = text.match(/of\s+(\d+)/); + const filteredTotal = Number(match[1]); + expect(assignedProjectBeneficiaries - filteredTotal).to.gt(0); + }); + }); + }) + it('Updates a project', function () { cy.visit(projectPath) cy.assertMuiInput('Name', projectName) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index be1a9b2..72b810d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -437,7 +437,12 @@ Cypress.Commands.add('configureDefaultEnrollmentCriteria', ( cy.chooseMuiSelect('Field', criterionField) cy.chooseMuiSelect('Confirm Filters', criterionFilter) - cy.enterMuiInput('Value', criterionValue) + + const isValueSelect = /^(True|False)$/.test(criterionValue); + isValueSelect + ? cy.chooseMuiSelect('Value', criterionValue) + : cy.enterMuiInput('Value', criterionValue); + cy.get('[title="Save changes"] button').click() cy.checkProgramUpdateCompleted() @@ -446,9 +451,11 @@ Cypress.Commands.add('configureDefaultEnrollmentCriteria', ( cy.contains('button', 'Beneficiaries').click() cy.contains('button', status).click() - cy.assertMuiSelectValue('Field', criterionField) - cy.assertMuiSelectValue('Confirm Filters', criterionFilter) - cy.assertMuiInput('Value', criterionValue) + cy.assertMuiSelectValue('Field', criterionField); + cy.assertMuiSelectValue('Confirm Filters', criterionFilter); + isValueSelect + ? cy.assertMuiSelectValue('Value', criterionValue) + : cy.assertMuiInput('Value', criterionValue); }); Cypress.Commands.add('enrollBeneficiariesIntoProgram', ( @@ -463,9 +470,11 @@ Cypress.Commands.add('enrollBeneficiariesIntoProgram', ( cy.chooseMuiAutocomplete('BenefitPlan', programName) cy.chooseMuiSelect('Status', status.toUpperCase()) - cy.assertMuiSelectValue('Field', criterionField) - cy.assertMuiSelectValue('Confirm Filters', criterionFilter) - cy.assertMuiInput('Value', criterionValue) + cy.assertMuiSelectValue('Field', criterionField); + cy.assertMuiSelectValue('Confirm Filters', criterionFilter); + /^(True|False)$/.test(criterionValue) + ? cy.assertMuiSelectValue('Value', criterionValue) + : cy.assertMuiInput('Value', criterionValue); cy.contains('button', 'Preview Enrollment Process').click() From 04cb15c972ecc82bdce9ab7c7efcc29ca716257e Mon Sep 17 00:00:00 2001 From: Wei Lu Date: Fri, 28 Nov 2025 09:58:18 -0500 Subject: [PATCH 33/33] Fix merge issue in GitHub action CI file --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4280e02..33d3830 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -46,7 +46,6 @@ jobs: echo 'FE_TAG=develop' >> .env echo 'DOMAIN=localhost' >> .env echo 'HOSTS=localhost' >> .env - cp .env.cache.example .env.cache cp .env.openSearch.example .env.openSearch docker compose up -d