Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8907f4e
Add e2e test case covering individual json schema config workflow
weilu Aug 6, 2025
ae77ce2
Add activity admin CRUD E2E tests
weilu Aug 7, 2025
8deb236
Extract deleteModuleConfig e2e command for reuse
weilu Aug 7, 2025
1d712af
Add E2E test for menu config
weilu Aug 7, 2025
6b3be4d
Add E2E tests for program creation & deletion
weilu Aug 7, 2025
3a5ed94
fix: baseUrl setup to offer flexibility
weilu Aug 7, 2025
ebd7c32
Add E2E tests for group programs creation and deletion
weilu Aug 19, 2025
c26e35c
Add individual program update E2E test
weilu Aug 21, 2025
a48042c
Add group program update E2E test
weilu Aug 21, 2025
ff99223
Set HOSTS to fix the CSRF issue
weilu Aug 21, 2025
d214d82
Adjust E2E tests and config such that it can be used with or without …
weilu Sep 4, 2025
98b734f
Disable sonar coverage check on this repo
weilu Sep 5, 2025
fd6344d
Exclude test code from duplication analysis
weilu Sep 5, 2025
6e91d12
Move to the maintained version of sonar action
weilu Sep 5, 2025
ea1db8f
Extract language pack check into util func & use it in E2E tests
weilu Sep 5, 2025
cf3f696
Add individual & household csv import E2E test
weilu Sep 5, 2025
039cc2e
Pin down 3rd party GitHub action versions
weilu Sep 5, 2025
5f2f7df
Increase server check timeout & log non-200 response code
weilu Sep 5, 2025
6f9d1b9
Add domain config to avoid 400 when request is redirected to https
weilu Sep 5, 2025
bf0b0cc
Reorganize cash transfer tests to share setup steps
weilu Sep 11, 2025
185d9ee
Trigger admin user auto provision of DB core user before admin tests
weilu Sep 11, 2025
d07c4c3
Add failed login E2E tests
weilu Sep 11, 2025
078a161
Add E2E tests on default enrollment criteria config on programs
weilu Sep 11, 2025
2dad2ec
E2E test for creating and deleting a project under a household program
weilu Sep 20, 2025
8b638ca
E2E tests for update a project and restore a deleted project
weilu Sep 20, 2025
8f9b022
Fix E2E login before each tests so the entire suite runs correctly
weilu Sep 21, 2025
a75b81e
Individual program’s Project create/update/delete/restore E2E tests
weilu Oct 2, 2025
60320a7
Ensure individual csv group codes are different across E2E runs
weilu Oct 15, 2025
7d34392
Add E2E test for group program enrollment
weilu Oct 16, 2025
f5cff20
E2E test for household enrollment into a project
weilu Oct 17, 2025
b669d25
Add E2E test for individual enrollment into a program
weilu Oct 18, 2025
cd9f6df
E2E test for individual project enrollment & filter
weilu Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: 0
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
uses: SonarSource/sonarqube-scan-action@1a6d90ebcb0e6a6b1d87e37ba693fe453195ae25
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand All @@ -44,6 +44,8 @@ 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
cp .env.database.example .env.database
Expand All @@ -68,7 +70,7 @@ jobs:
exit 1

- name: Cypress run
uses: cypress-io/github-action@v6
uses: cypress-io/github-action@b8ba51a856ba5f4c15cf39007636d4ab04f23e3c
with:
record: false
parallel: false
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ data/*
openimis-dist_dkr.code-workspace
node_modules/
cypress/screenshots/
cypress/downloads/
cypress/fixtures/tmp_individuals.csv
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

109 changes: 79 additions & 30 deletions cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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...')
Expand All @@ -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)
}
})
}
Expand All @@ -44,36 +46,83 @@ function waitForServerToStart(url) {
}

module.exports = defineConfig({
viewportWidth: 1280,
viewportHeight: 670,
e2e: {
projectId: "q6gc25", // Cypress Cloud, needed for recording
baseUrl: 'http://localhost/front',
defaultCommandTimeout: 10000,
taskTimeout: 300000,
baseUrl: 'http://localhost',
defaultCommandTimeout: 15000,
taskTimeout: timeoutMinutes * 60 * 1000 + 10,
downloadsFolder: 'cypress/downloads',
setupNodeEvents(on, config) {
on('task', {
checkSetup() {
return fs.existsSync(serverFlagPath)
},
completeSetup() {
const rootUrl = config.baseUrl.replace("/front", "")
const url = `${rootUrl}/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:', error)
return null
})
},
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({ 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];
const rows = lines.slice(1);
const headerCols = header.split(',');
const groupCodeIdx = headerCols.indexOf('group_code');

// map old group_code → new group_code
const groupMap = {};

// 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));
}
return null
} else {
adjustedRows = rows;
}

const newLines = [header];
for (const line of adjustedRows) {
const row = line.split(',');
const oldCode = row[groupCodeIdx];

if (!groupMap[oldCode]) {
groupMap[oldCode] = Math.random().toString(36).substring(2, 8).toUpperCase();
}

row[groupCodeIdx] = groupMap[oldCode];
newLines.push(row.join(','));
}

fs.writeFileSync(tmpPath, newLines.join('\n'));
return null;
}
})
},
},
Expand Down
159 changes: 159 additions & 0 deletions cypress/e2e/admin.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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()
});

it('Configures menu', function () {
cy.deleteModuleConfig("fe-core")
cy.visit('/front')
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('/front')
cy.get('div.MuiDrawer-root').should('exist') // left drawer menu

const expectedMenuItems = [
'Social Protection',
'Dashboards',
'Payments',
'Grievance',
'Tasks Management',
'Administration',
]
const programMenuText = capitalizeWords(getProgramTerm()) + 's'
const expectedSubMenuItems = [
'Individuals',
'Groups',
'Import Data - API',
programMenuText,
]
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"]', 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"]', programMenuText);
})
});
})
})

it('Configuring individual json schema reflects in advanced filters and upload template', function () {
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')
})

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');
})
})

26 changes: 23 additions & 3 deletions cypress/e2e/auth.cy.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
describe('Unauthenticated', () => {
it('Shows the login screen', () => {
cy.visit('')
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')
})
})

Expand All @@ -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()
Expand All @@ -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')
})
})

Loading
Loading