diff --git a/.github/workflows/e2e-dev.yml b/.github/workflows/e2e-dev.yml new file mode 100644 index 000000000..832a328bf --- /dev/null +++ b/.github/workflows/e2e-dev.yml @@ -0,0 +1,70 @@ +name: 'e2e-dev' + +on: + workflow_call: + secrets: + username: + required: true + password: + required: true + recordkey: + required: true + +concurrency: + group: e2e-dev-${{ github.workflow}}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + compute-dev-version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.instance-version.outputs.version }} + steps: + - name: Output dev version + id: instance-version + uses: dhis2/action-instance-version@v1 + with: + instance-url: https://test.e2e.dhis2.org/analytics-2.41d + username: ${{ secrets.username }} + password: ${{ secrets.password }} + + e2e-dev: + needs: compute-dev-version + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3, 4] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Run e2e tests + uses: cypress-io/github-action@v5 + with: + start: yarn d2-app-scripts start + wait-on: 'http://localhost:3000' + wait-on-timeout: 300 + record: true + parallel: true + browser: chrome + group: e2e-chrome-parallel-dev + env: + BROWSER: none + CYPRESS_RECORD_KEY: ${{ secrets.recordkey }} + CYPRESS_dhis2BaseUrl: https://test.e2e.dhis2.org/analytics-2.41d + CYPRESS_dhis2InstanceVersion: ${{ needs.compute-dev-version.outputs.version }} + CYPRESS_dhis2Username: ${{ secrets.username }} + CYPRESS_dhis2Password: ${{ secrets.password }} + CYPRESS_networkMode: live + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cypress/helpers/dimensions.js b/cypress/helpers/dimensions.js index 584bf857f..efcef8233 100644 --- a/cypress/helpers/dimensions.js +++ b/cypress/helpers/dimensions.js @@ -24,6 +24,11 @@ const selectProgramAndStage = ({ inputType, programName, stageName }) => { } } +export const selectProgramForTE = (programName) => { + cy.getBySel('accessory-sidebar').contains('Choose a program').click() + cy.contains(programName).click() +} + export const selectEventWithProgram = ({ programName, stageName }) => selectProgramAndStage({ inputType: INPUT_EVENT, programName, stageName }) @@ -33,6 +38,12 @@ export const selectEnrollmentWithProgram = ({ programName }) => programName, }) +export const selectTrackedEntityWithType = (typeName) => { + cy.getBySel('input-tracked-entity').click() + cy.getBySel('accessory-sidebar').contains('Choose a type').click() + cy.contains(typeName).click() +} + export const openInputSidebar = () => { cy.getBySel('main-sidebar').contains('Input:').click() cy.getBySel('input-panel').should('be.visible') @@ -40,7 +51,6 @@ export const openInputSidebar = () => { export const openProgramDimensionsSidebar = () => { cy.getBySel('main-sidebar').contains('Program dimensions').click() - cy.getBySel('program-dimensions').should('be.visible') } export const openDimension = (dimensionName) => { @@ -67,6 +77,9 @@ export const clickAddRemoveProgramDimension = (label) => export const clickAddRemoveProgramDataDimension = (label) => clickAddRemoveDimension('program-dimensions-list', label) +export const clickAddRemoveTrackedEntityTypeDimensions = (label) => + clickAddRemoveDimension('tracked-entity-dimensions-list', label) + const selectProgramDimensions = ({ inputType, programName, @@ -105,6 +118,19 @@ export const selectEnrollmentWithProgramDimensions = ({ dimensions, }) +export const selectTrackedEntityWithTypeAndProgramDimensions = ({ + typeName, + programName, + dimensions, +}) => { + selectTrackedEntityWithType(typeName) + openProgramDimensionsSidebar() + selectProgramForTE(programName) + dimensions.forEach((dimensionName) => { + clickAddRemoveProgramDataDimension(dimensionName) + }) +} + const disabledOpacity = { prop: 'opacity', value: '0.5' } const disabledCursor = { prop: 'cursor', value: 'not-allowed' } diff --git a/cypress/helpers/fileMenu.js b/cypress/helpers/fileMenu.js index eb68ea0c3..a2415e8f6 100644 --- a/cypress/helpers/fileMenu.js +++ b/cypress/helpers/fileMenu.js @@ -1,3 +1,4 @@ +import { EXTENDED_TIMEOUT } from '../support/util.js' import { clearInput, typeInput, clearTextarea, typeTextarea } from './common.js' export const ITEM_NEW = 'file-menu-new' @@ -68,3 +69,15 @@ export const renameVisualization = (name, description) => { cy.getBySel('file-menu-rename-modal-rename').click() } + +export const openAOByName = (name) => { + cy.getBySel('dhis2-analytics-hovermenubar').contains('File').click() + + cy.getBySel(ITEM_OPEN).click() + + typeInput('open-file-dialog-modal-name-filter', name) + + cy.getBySel('open-file-dialog-modal', EXTENDED_TIMEOUT) + .contains(name, EXTENDED_TIMEOUT) + .click() +} diff --git a/cypress/helpers/layout.js b/cypress/helpers/layout.js index 443947146..4bf160bfd 100644 --- a/cypress/helpers/layout.js +++ b/cypress/helpers/layout.js @@ -27,21 +27,29 @@ export const expectAxisToNotHaveDimension = (axisId, dimensionId) => { export const assertTooltipContainsEntries = (entries) => entries.forEach((entry) => cy.getBySel('tooltip-content').contains(entry)) -export const assertChipContainsText = (dimensionName, suffix) => { - if (suffix) { +export const assertChipContainsText = (primary, items, secondary) => { + if (items) { cy.getBySelLike('layout-chip') - .containsExact(dimensionName, EXTENDED_TIMEOUT) + .containsExact(primary, EXTENDED_TIMEOUT) .parent() - .findBySelLike('chip-suffix') - .contains(suffix, EXTENDED_TIMEOUT) + .parent() + .findBySelLike('chip-items') + .contains(items, EXTENDED_TIMEOUT) } else { cy.getBySelLike('layout-chip') - .containsExact(dimensionName, EXTENDED_TIMEOUT) + .containsExact(primary, EXTENDED_TIMEOUT) + .parent() .parent() - .findBySelLike('chip-suffix') + .findBySelLike('chip-items') .should('not.exist') } + if (secondary) { + cy.getBySelLike('layout-chip').containsExact( + secondary, + EXTENDED_TIMEOUT + ) + } cy.getBySelLike('layout-chip') - .containsExact(dimensionName, EXTENDED_TIMEOUT) + .containsExact(primary, EXTENDED_TIMEOUT) .trigger('mouseover') } diff --git a/cypress/helpers/menubar.js b/cypress/helpers/menubar.js index b490ea75c..452fccd37 100644 --- a/cypress/helpers/menubar.js +++ b/cypress/helpers/menubar.js @@ -20,17 +20,17 @@ export const clickMenubarOptionsButton = () => export const openDataOptionsModal = () => { clickMenubarOptionsButton() - return cy.getBySel('options-menu-list').contains('Data').click() + cy.getBySel('options-menu-list').contains('Data').click() } export const openStyleOptionsModal = () => { clickMenubarOptionsButton() - return cy.getBySel('options-menu-list').contains('Style').click() + cy.getBySel('options-menu-list').contains('Style').click() } export const openLegendOptionsModal = () => { clickMenubarOptionsButton() - return cy.getBySel('options-menu-list').contains('Legend').click() + cy.getBySel('options-menu-list').contains('Legend').click() } export const clickMenubarInterpretationsButton = () => diff --git a/cypress/helpers/orgUnit.js b/cypress/helpers/orgUnit.js index aa2a7bf7b..d20c2783a 100644 --- a/cypress/helpers/orgUnit.js +++ b/cypress/helpers/orgUnit.js @@ -1,6 +1,6 @@ import { EXTENDED_TIMEOUT } from '../support/util.js' -const orgUnitModalEl = 'fixed-dimension-ou-modal' +const orgUnitModalEl = 'ou-modal' const levelSelectEl = 'org-unit-level-select' const levelSelectOptionEl = 'org-unit-level-select-option' const groupSelectEl = 'org-unit-group-select' @@ -12,12 +12,12 @@ const orgUnitTreeNodeSelectEl = '[type="checkbox"]' const orgUnitTreeNodeToggleEl = 'org-unit-tree-node-toggle' export const clickOrgUnitDimensionModalUpdateButton = () => - cy.getBySel(`${orgUnitModalEl}-action-confirm`).click() + cy.getBySelLike(`${orgUnitModalEl}-action-confirm`).click() export const openOuDimension = () => cy.getBySelLike('layout-chip-ou').click() export const expectOrgUnitDimensionModalToBeVisible = () => - cy.getBySel(orgUnitModalEl).should('be.visible') + cy.getBySelLike(orgUnitModalEl).should('be.visible') export const expectOrgUnitDimensionToNotBeLoading = () => cy @@ -84,24 +84,24 @@ export const toggleOrgUnitGroup = (name) => { } export const selectUserOrgUnit = (name) => { - cy.getBySel(orgUnitModalEl) + cy.getBySelLike(orgUnitModalEl) .contains(name) .find('[type="checkbox"]') .should('not.be.checked') - cy.getBySel(orgUnitModalEl).contains(name).click() - cy.getBySel(orgUnitModalEl) + cy.getBySelLike(orgUnitModalEl).contains(name).click() + cy.getBySelLike(orgUnitModalEl) .contains(name) .find('[type="checkbox"]') .should('be.checked') } export const deselectUserOrgUnit = (name) => { - cy.getBySel(orgUnitModalEl) + cy.getBySelLike(orgUnitModalEl) .contains(name) .find('[type="checkbox"]') .should('be.checked') - cy.getBySel(orgUnitModalEl).contains(name).click() - cy.getBySel(orgUnitModalEl) + cy.getBySelLike(orgUnitModalEl).contains(name).click() + cy.getBySelLike(orgUnitModalEl) .contains(name) .find('[type="checkbox"]') .should('not.be.checked') diff --git a/cypress/helpers/period.js b/cypress/helpers/period.js index 562b1b64c..c99a69437 100644 --- a/cypress/helpers/period.js +++ b/cypress/helpers/period.js @@ -7,11 +7,15 @@ const openPeriod = (label) => { } } -const selectFixedPeriod = ({ label, period }) => { +const DEFAULT_FIXED_PERIOD_TYPE = 'Monthly' +const selectFixedPeriod = ({ label, period, selected }) => { + // open the period modal in the fixed period view openPeriod(label) cy.contains('Choose from presets').click() cy.contains('Fixed periods').click() - if (period.type) { + + // change period type if applicable + if (period.type && period.type !== DEFAULT_FIXED_PERIOD_TYPE) { cy.getBySel( 'period-dimension-fixed-period-filter-period-type-content' ).click() @@ -21,12 +25,30 @@ const selectFixedPeriod = ({ label, period }) => { .contains(period.type) .click() } + + // select the year and the period cy.getBySel('period-dimension-fixed-period-filter-year-content') .clear() .type(period.year) - cy.getBySel('period-dimension-transfer-option-content') + cy.getBySel('period-dimension-transfer-sourceoptions') + .findBySel('period-dimension-transfer-option-content') .contains(period.name) .dblclick() + + cy.getBySel('period-dimension-transfer-pickedoptions').contains(period.name) + cy.getBySel('period-dimension-transfer-sourceoptions') + .contains(period.name) + .should('not.exist') + + // verify that a previously selected period is visible in the picked options and not in the source options + if (selected) { + cy.getBySel('period-dimension-transfer-pickedoptions') + .containsExact(selected.name) + .should('be.visible') + cy.getBySel('period-dimension-transfer-sourceoptions') + .contains(selected.name) + .should('not.exist') + } cy.getBySel('period-dimension-modal-action-confirm').click() } diff --git a/cypress/integration/conditions/alphanumericConditions.cy.js b/cypress/integration/conditions/alphanumericConditions.cy.js index c22f86280..44655fee1 100644 --- a/cypress/integration/conditions/alphanumericConditions.cy.js +++ b/cypress/integration/conditions/alphanumericConditions.cy.js @@ -1,3 +1,4 @@ +import { DIMENSION_ID_ORGUNIT } from '@dhis2/analytics' import { DIMENSION_ID_EVENT_DATE } from '../../../src/modules/dimensionConstants.js' import { E2E_PROGRAM, @@ -11,16 +12,27 @@ import { TEST_REL_PE_THIS_YEAR, } from '../../data/index.js' import { + clickAddRemoveTrackedEntityTypeDimensions, openDimension, openProgramDimensionsSidebar, selectEventWithProgram, selectEventWithProgramDimensions, + selectTrackedEntityWithType, } from '../../helpers/dimensions.js' import { assertChipContainsText, assertTooltipContainsEntries, } from '../../helpers/layout.js' import { clickMenubarUpdateButton } from '../../helpers/menubar.js' +import { + clickOrgUnitDimensionModalUpdateButton, + deselectUserOrgUnit, + expectOrgUnitDimensionModalToBeVisible, + expectOrgUnitDimensionToNotBeLoading, + openOrgUnitTreeItem, + openOuDimension, + selectOrgUnitTreeItem, +} from '../../helpers/orgUnit.js' import { getCurrentYearStr, selectRelativePeriod, @@ -33,27 +45,25 @@ import { } from '../../helpers/table.js' const event = E2E_PROGRAM -const dimensionName = TEST_DIM_TEXT const periodLabel = event[DIMENSION_ID_EVENT_DATE] -const stageName = 'Stage 1 - Repeatable' -const currentYear = getCurrentYearStr() -const setUpTable = () => { - selectEventWithProgramDimensions({ ...event, dimensions: [dimensionName] }) +const currentYear = getCurrentYearStr() - selectRelativePeriod({ - label: periodLabel, - period: TEST_REL_PE_THIS_YEAR, +/* + // one way to make sure that conditions work for TE is to simply duplicate the tests we have today and adapt them to TE, here's a quick plan for that: + // TODO: make a copy of the "describe('text conditions..." below + // TODO: change the beforeEach to include this instead: + selectTrackedEntityWithTypeAndProgramDimensions({ + typeName: 'Person', + ...event, + dimensions: [dimensionName], }) + // TODO: add period selection here once it's supported / before merging this to master! + // TODO: adapt the results of each test to match the result from tracked entity - clickMenubarUpdateButton() - - expectTableToBeVisible() - - assertChipContainsText(dimensionName, 'all') -} - -const addConditions = (conditions) => { + // TODO: do all of the above for the other types of conditions +*/ +const addConditions = (conditions, dimensionName) => { cy.getBySelLike('layout-chip').contains(dimensionName).click() conditions.forEach(({ conditionName, value, useCaseSensitive }, index) => { cy.getBySel('button-add-condition').click() @@ -79,19 +89,39 @@ const addConditions = (conditions) => { /* This test doesn't look like it needs `testIsolation: false` * but start failing once this is removed */ -describe('text conditions', { testIsolation: false }, () => { +describe('text conditions (event)', { testIsolation: false }, () => { + const dimensionName = TEST_DIM_TEXT + const stageName = 'Stage 1 - Repeatable' const LONG_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' beforeEach(() => { goToStartPage() - setUpTable() + + selectEventWithProgramDimensions({ + ...event, + dimensions: [dimensionName], + }) + + selectRelativePeriod({ + label: periodLabel, + period: TEST_REL_PE_THIS_YEAR, + }) + + clickMenubarUpdateButton() + + expectTableToBeVisible() + + assertChipContainsText(dimensionName, 'all') }) it('exactly', () => { const TEST_TEXT = 'Text A' - addConditions([{ conditionName: 'exactly', value: TEST_TEXT }]) + addConditions( + [{ conditionName: 'exactly', value: TEST_TEXT }], + dimensionName + ) expectTableToMatchRows([TEST_TEXT]) @@ -103,7 +133,10 @@ describe('text conditions', { testIsolation: false }, () => { it('is not', () => { const TEST_TEXT = 'Text A' - addConditions([{ conditionName: 'is not', value: TEST_TEXT }]) + addConditions( + [{ conditionName: 'is not', value: TEST_TEXT }], + dimensionName + ) expectTableToMatchRows([ LONG_TEXT, @@ -122,12 +155,15 @@ describe('text conditions', { testIsolation: false }, () => { it('contains', () => { const TEST_TEXT = 'T' - addConditions([ - { - conditionName: 'contains', - value: TEST_TEXT, - }, - ]) + addConditions( + [ + { + conditionName: 'contains', + value: TEST_TEXT, + }, + ], + dimensionName + ) expectTableToMatchRows([LONG_TEXT, 'Text A', 'Text A-2', 'Text E']) @@ -139,13 +175,16 @@ describe('text conditions', { testIsolation: false }, () => { it('contains (case-sensitive)', () => { const TEST_TEXT = 'T' - addConditions([ - { - conditionName: 'contains', - value: TEST_TEXT, - useCaseSensitive: true, - }, - ]) + addConditions( + [ + { + conditionName: 'contains', + value: TEST_TEXT, + useCaseSensitive: true, + }, + ], + dimensionName + ) expectTableToMatchRows(['Text A', 'Text A-2', 'Text E']) @@ -157,12 +196,15 @@ describe('text conditions', { testIsolation: false }, () => { it('does not contain', () => { const TEST_TEXT = 'T' - addConditions([ - { - conditionName: 'does not contain', - value: TEST_TEXT, - }, - ]) + addConditions( + [ + { + conditionName: 'does not contain', + value: TEST_TEXT, + }, + ], + dimensionName + ) expectTableToMatchRows([ `${currentYear}-03-01`, // empty row, use value in date column @@ -179,11 +221,14 @@ describe('text conditions', { testIsolation: false }, () => { }) it('is empty / null', () => { - addConditions([ - { - conditionName: 'is empty / null', - }, - ]) + addConditions( + [ + { + conditionName: 'is empty / null', + }, + ], + dimensionName + ) expectTableToMatchRows([`${currentYear}-03-01`, `${currentYear}-02-01`]) // empty row, use value in date column @@ -193,11 +238,14 @@ describe('text conditions', { testIsolation: false }, () => { }) it('is not empty / not null', () => { - addConditions([ - { - conditionName: 'is not empty / not null', - }, - ]) + addConditions( + [ + { + conditionName: 'is not empty / not null', + }, + ], + dimensionName + ) expectTableToMatchRows([ LONG_TEXT, @@ -213,10 +261,13 @@ describe('text conditions', { testIsolation: false }, () => { }) it('2 conditions: contains + is not', () => { - addConditions([ - { conditionName: 'contains', value: 'T' }, - { conditionName: 'is not', value: 'Text A-2' }, - ]) + addConditions( + [ + { conditionName: 'contains', value: 'T' }, + { conditionName: 'is not', value: 'Text A-2' }, + ], + dimensionName + ) expectTableToMatchRows([LONG_TEXT, 'Text A', 'Text E']) @@ -225,6 +276,185 @@ describe('text conditions', { testIsolation: false }, () => { assertTooltipContainsEntries([stageName, 'Contains: ', 'Is not: ']) }) }) +describe(['>=41'], 'text conditions (TE)', { testIsolation: false }, () => { + const dimensionName = 'First Name' + + beforeEach(() => { + // set up a TE LL with some dimensions + goToStartPage() + selectTrackedEntityWithType('Malaria Entity') + cy.getBySel('main-sidebar') + .contains('Malaria Entity dimensions') + .click() + clickAddRemoveTrackedEntityTypeDimensions(dimensionName) + // select a second dimension that can be verified if the first dimension has an emtpy value + clickAddRemoveTrackedEntityTypeDimensions('System Case ID') + + // change org unit to limit the data + openOuDimension(DIMENSION_ID_ORGUNIT) + expectOrgUnitDimensionModalToBeVisible() + expectOrgUnitDimensionToNotBeLoading() + deselectUserOrgUnit('User organisation unit') + openOrgUnitTreeItem('Bo') + openOrgUnitTreeItem('Badjia') + selectOrgUnitTreeItem('Njandama MCHP') + clickOrgUnitDimensionModalUpdateButton() + + // assert that the table is visible and that the layout chip is present + expectTableToBeVisible() + assertChipContainsText(dimensionName, 'all') + }) + + it('exactly', () => { + const TEST_TEXT = 'Angus' + + addConditions( + [{ conditionName: 'exactly', value: TEST_TEXT }], + dimensionName + ) + + expectTableToMatchRows([TEST_TEXT]) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries([`Exactly: ${TEST_TEXT}`]) + }) + + it('is not', () => { + const TEST_TEXT = 'Angus' + + addConditions( + [{ conditionName: 'is not', value: TEST_TEXT }], + dimensionName + ) + + expectTableToMatchRows([ + 'Mark', + 'YYX928443', // empty row, use value in another column + 'BGD242352', // empty row, use value in another column + 'beleb', + ]) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries([`Is not: ${TEST_TEXT}`]) + }) + + it('contains', () => { + const TEST_TEXT = 'A' + + addConditions( + [ + { + conditionName: 'contains', + value: TEST_TEXT, + }, + ], + dimensionName + ) + + expectTableToMatchRows(['Angus', 'Mark']) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries([`Contains: ${TEST_TEXT}`]) + }) + + it('contains (case-sensitive)', () => { + const TEST_TEXT = 'a' + + addConditions( + [ + { + conditionName: 'contains', + value: TEST_TEXT, + useCaseSensitive: true, + }, + ], + dimensionName + ) + + expectTableToMatchRows(['Mark']) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries([`Contains: ${TEST_TEXT}`]) + }) + + it('does not contain', () => { + const TEST_TEXT = 'A' + + addConditions( + [ + { + conditionName: 'does not contain', + value: TEST_TEXT, + }, + ], + dimensionName + ) + + expectTableToMatchRows([ + 'beleb', + 'YYX928443', // empty row, use value in another column + 'BGD242352', // empty row, use value in another column + ]) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries([`Does not contain: ${TEST_TEXT}`]) + }) + + it('is empty / null', () => { + addConditions( + [ + { + conditionName: 'is empty / null', + }, + ], + dimensionName + ) + + expectTableToMatchRows(['YYX928443', 'BGD242352']) //// empty row, use value in another column + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries([`Is empty / null`]) + }) + + it('is not empty / not null', () => { + addConditions( + [ + { + conditionName: 'is not empty / not null', + }, + ], + dimensionName + ) + + expectTableToMatchRows(['Angus', 'Mark', 'beleb']) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries([`Is not empty / not null`]) + }) + + it('2 conditions: contains + is not', () => { + addConditions( + [ + { conditionName: 'contains', value: 'A' }, + { conditionName: 'is not', value: 'Mark' }, + ], + dimensionName + ) + + expectTableToMatchRows(['Angus']) + + assertChipContainsText(dimensionName, 2) + + assertTooltipContainsEntries(['Contains: ', 'Is not: ']) + }) +}) /* This test doesn't look like it needs `testIsolation: false` * but start failing once this is removed */ describe('alphanumeric types', { testIsolation: false }, () => { diff --git a/cypress/integration/conditions/booleanConditions.cy.js b/cypress/integration/conditions/booleanConditions.cy.js index 461562bd9..617ef1a02 100644 --- a/cypress/integration/conditions/booleanConditions.cy.js +++ b/cypress/integration/conditions/booleanConditions.cy.js @@ -27,6 +27,8 @@ const event = E2E_PROGRAM const periodLabel = event[DIMENSION_ID_EVENT_DATE] const stageName = 'Stage 1 - Repeatable' +// TODO: implement these tests for TE as soon as the backend returns 1 / 0 instead of "true" / "false" + const setUpTable = (dimensionName) => { selectEventWithProgramDimensions({ ...event, dimensions: [dimensionName] }) diff --git a/cypress/integration/conditions/numericConditions.cy.js b/cypress/integration/conditions/numericConditions.cy.js index eab79fa8e..b4a2ce17e 100644 --- a/cypress/integration/conditions/numericConditions.cy.js +++ b/cypress/integration/conditions/numericConditions.cy.js @@ -1,3 +1,4 @@ +import { DIMENSION_ID_ORGUNIT } from '@dhis2/analytics' import { DIMENSION_ID_EVENT_DATE } from '../../../src/modules/dimensionConstants.js' import { E2E_PROGRAM, @@ -13,16 +14,27 @@ import { TEST_DIM_WITH_PRESET, } from '../../data/index.js' import { + clickAddRemoveTrackedEntityTypeDimensions, openDimension, openProgramDimensionsSidebar, selectEventWithProgram, selectEventWithProgramDimensions, + selectTrackedEntityWithType, } from '../../helpers/dimensions.js' import { assertChipContainsText, assertTooltipContainsEntries, } from '../../helpers/layout.js' import { clickMenubarUpdateButton } from '../../helpers/menubar.js' +import { + clickOrgUnitDimensionModalUpdateButton, + deselectUserOrgUnit, + expectOrgUnitDimensionModalToBeVisible, + expectOrgUnitDimensionToNotBeLoading, + openOrgUnitTreeItem, + openOuDimension, + selectOrgUnitTreeItem, +} from '../../helpers/orgUnit.js' import { getPreviousYearStr, getCurrentYearStr, @@ -44,21 +56,6 @@ const event = E2E_PROGRAM const periodLabel = event[DIMENSION_ID_EVENT_DATE] const stageName = 'Stage 1 - Repeatable' -const setUpTable = (dimensionName, period) => { - selectEventWithProgramDimensions({ ...event, dimensions: [dimensionName] }) - - selectRelativePeriod({ - label: periodLabel, - period, - }) - - clickMenubarUpdateButton() - - expectTableToBeVisible() - - assertChipContainsText(dimensionName, 'all') -} - const addConditions = (conditions, dimensionName) => { cy.getBySelLike('layout-chip').contains(dimensionName).click() conditions.forEach(({ conditionName, value }) => { @@ -74,15 +71,8 @@ const addConditions = (conditions, dimensionName) => { cy.getBySel('conditions-modal').contains('Update').click() } -/* This test doesn't look like it needs `testIsolation: false` - * but start failing once this is removed */ -describe('number conditions', { testIsolation: false }, () => { - const dimensionName = TEST_DIM_NUMBER - - beforeEach(() => { - goToStartPage() - setUpTable(dimensionName, TEST_REL_PE_THIS_YEAR) - }) +const testNumberConditions = (dimensionName, version) => { + const decimalNumber = version >= 41 ? '3.12' : '3.1' it('equal to', () => { addConditions( @@ -136,7 +126,7 @@ describe('number conditions', { testIsolation: false }, () => { dimensionName ) - expectTableToMatchRows(['11', '3.7']) + expectTableToMatchRows(['11', decimalNumber]) assertChipContainsText(dimensionName, 1) @@ -149,7 +139,7 @@ describe('number conditions', { testIsolation: false }, () => { dimensionName ) - expectTableToMatchRows(['11', '12', '3.7']) + expectTableToMatchRows(['11', '12', decimalNumber]) assertChipContainsText(dimensionName, 1) @@ -166,7 +156,7 @@ describe('number conditions', { testIsolation: false }, () => { ) expectTableToMatchRows([ - '3.7', + decimalNumber, '11', `${currentYear}-01-01`, // empty row, use value in date column '2 000 000', @@ -202,7 +192,7 @@ describe('number conditions', { testIsolation: false }, () => { ) expectTableToMatchRows([ - '3.7', + decimalNumber, '11', '12', '2 000 000', @@ -234,6 +224,240 @@ describe('number conditions', { testIsolation: false }, () => { 'Less than (<): 13', ]) }) +} + +const prepareNumberConditions = (dimensionName) => { + goToStartPage() + selectEventWithProgramDimensions({ + ...event, + dimensions: [dimensionName], + }) + + selectRelativePeriod({ + label: periodLabel, + period: TEST_REL_PE_THIS_YEAR, + }) + + clickMenubarUpdateButton() + + expectTableToBeVisible() + + assertChipContainsText(dimensionName, 'all') +} + +/* This test doesn't look like it needs `testIsolation: false` + * but start failing once this is removed */ +describe(['<41'], 'number conditions (event)', { testIsolation: false }, () => { + const dimensionName = TEST_DIM_NUMBER + + beforeEach(() => { + prepareNumberConditions(dimensionName) + }) + + testNumberConditions(dimensionName, 40) +}) +describe( + ['>=41'], + 'number conditions (event)', + { testIsolation: false }, + () => { + const dimensionName = TEST_DIM_NUMBER + + beforeEach(() => { + prepareNumberConditions(dimensionName) + }) + + testNumberConditions(dimensionName, 41) + } +) + +describe(['>=41'], 'number conditions (TE)', { testIsolation: false }, () => { + const dimensionName = 'Age (years)' + + beforeEach(() => { + // set up a TE LL with a dimension + goToStartPage() + selectTrackedEntityWithType('Malaria Entity') + cy.getBySel('main-sidebar') + .contains('Malaria Entity dimensions') + .click() + clickAddRemoveTrackedEntityTypeDimensions(dimensionName) + clickMenubarUpdateButton() + + // expect the table to be visible and the dimension chip to be present + expectTableToBeVisible() + assertChipContainsText(dimensionName, 'all') + }) + + it('equal to', () => { + addConditions( + [{ conditionName: 'equal to (=)', value: '15' }], + dimensionName + ) + + expectTableToMatchRows(['15']) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Equal to (=): 15']) + }) + + it('greater than', () => { + addConditions( + [{ conditionName: 'greater than (>)', value: '43' }], + dimensionName + ) + + expectTableToMatchRows(['46', '64']) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Greater than (>): 43']) + }) + + it('greater than or equal to', () => { + addConditions( + [{ conditionName: 'greater than or equal to', value: '43' }], + dimensionName + ) + expectTableToMatchRows(['43', '46', '64']) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Greater than or equal to (≥): 43']) + }) + + it('less than', () => { + addConditions( + [{ conditionName: 'less than (<)', value: '11' }], + dimensionName + ) + + expectTableToMatchRows([ + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '6', + '9', + '9', + ]) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Less than (<): 11']) + }) + + it('less than or equal to', () => { + addConditions( + [{ conditionName: 'less than or equal to', value: '11' }], + dimensionName + ) + + expectTableToMatchRows([ + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '6', + '9', + '9', + '11', + ]) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Less than or equal to (≤): 11']) + }) + + it('not equal to', () => { + openOuDimension(DIMENSION_ID_ORGUNIT) + expectOrgUnitDimensionModalToBeVisible() + expectOrgUnitDimensionToNotBeLoading() + deselectUserOrgUnit('User organisation unit') + openOrgUnitTreeItem('Bo') + openOrgUnitTreeItem('Badjia') + selectOrgUnitTreeItem('Njandama MCHP') + clickOrgUnitDimensionModalUpdateButton() + + addConditions( + [{ conditionName: 'not equal to', value: '11' }], + dimensionName + ) + + expectTableToMatchRows(['0', '0', '26', '36']) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Not equal to (≠): 11']) + }) + + it('is empty / null', () => { + clickAddRemoveTrackedEntityTypeDimensions('System Case ID') + + addConditions([{ conditionName: 'is empty / null' }], dimensionName) + + expectTableToMatchRows(['GFS397135', 'VCA989272', 'PVZ270497']) + + getTableDataCells() + .eq(1) + .invoke('text') + .invoke('trim') + .should('equal', '') + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Is empty / null']) + }) + + it('is not empty / not null', () => { + openOuDimension(DIMENSION_ID_ORGUNIT) + expectOrgUnitDimensionModalToBeVisible() + expectOrgUnitDimensionToNotBeLoading() + deselectUserOrgUnit('User organisation unit') + openOrgUnitTreeItem('Bo') + openOrgUnitTreeItem('Badjia') + selectOrgUnitTreeItem('Ngelehun CHC') + clickOrgUnitDimensionModalUpdateButton() + + getTableRows().should('have.length', 32) + + addConditions( + [{ conditionName: 'is not empty / not null' }], + dimensionName + ) + + getTableRows().should('have.length', 29) + + assertChipContainsText(dimensionName, 1) + + assertTooltipContainsEntries(['Is not empty / not null']) + }) + + it('2 conditions: greater than + less than', () => { + addConditions( + [ + { conditionName: 'greater than (>)', value: '21' }, + { conditionName: 'less than (<)', value: '24' }, + ], + dimensionName + ) + + expectTableToMatchRows(['23']) + + assertChipContainsText(dimensionName, 2) + + assertTooltipContainsEntries([ + 'Greater than (>): 21', + 'Less than (<): 24', + ]) + }) }) /* This test doesn't look like it needs `testIsolation: false` @@ -243,7 +467,22 @@ describe('integer', { testIsolation: false }, () => { beforeEach(() => { goToStartPage() - setUpTable(dimensionName, TEST_REL_PE_LAST_YEAR) + + selectEventWithProgramDimensions({ + ...event, + dimensions: [dimensionName], + }) + + selectRelativePeriod({ + label: periodLabel, + period: TEST_REL_PE_LAST_YEAR, + }) + + clickMenubarUpdateButton() + + expectTableToBeVisible() + + assertChipContainsText(dimensionName, 'all') }) it('integer with negative value', () => { diff --git a/cypress/integration/createTrackedEntity.cy.js b/cypress/integration/createTrackedEntity.cy.js new file mode 100644 index 000000000..d4ef97a1f --- /dev/null +++ b/cypress/integration/createTrackedEntity.cy.js @@ -0,0 +1,246 @@ +import { + DIMENSION_ID_ENROLLMENT_DATE, + DIMENSION_ID_INCIDENT_DATE, + DIMENSION_ID_LAST_UPDATED, +} from '../../src/modules/dimensionConstants.js' +import { + clickAddRemoveProgramDataDimension, + clickAddRemoveTrackedEntityTypeDimensions, + openProgramDimensionsSidebar, + selectProgramForTE, + selectTrackedEntityWithType, +} from '../helpers/dimensions.js' +import { assertChipContainsText } from '../helpers/layout.js' +import { clickMenubarUpdateButton } from '../helpers/menubar.js' +import { selectFixedPeriod, getCurrentYearStr } from '../helpers/period.js' +import { goToStartPage } from '../helpers/startScreen.js' +import { expectTableToBeVisible } from '../helpers/table.js' + +describe(['>=41'], 'tracked entity', () => { + beforeEach(() => { + goToStartPage() + }) + it('creates a line list with dimensions', () => { + const program = { + programName: 'Child Programme', + [DIMENSION_ID_ENROLLMENT_DATE]: 'Date of enrollment', + [DIMENSION_ID_INCIDENT_DATE]: 'Date of birth', + [DIMENSION_ID_LAST_UPDATED]: 'Last updated on', + id: 'IpHINAT79UW', + } + const entityDimensionName = 'City' + const programDataDimensionName = 'MCH Infant Weight (g)' + const periodLabel = program[DIMENSION_ID_ENROLLMENT_DATE] + + // verify that e.g. the "Person dimensions" button is hidden + cy.getBySel('tracked-entity-button').should('not.exist') + cy.getBySel('main-sidebar') + .contains('Person dimensions') + .should('not.exist') + + // TODO: check that reg ou and reg date aren't shown ! + + // switch to Tracked entity and select a type + selectTrackedEntityWithType('Person') + + cy.getBySel('input-panel-button-subtitle').contains('Person') + + // TODO: check that reg ou and reg date are shown ! + + // add a TET dimension + cy.getBySel('main-sidebar').contains('Person dimensions').click() + cy.getBySel('tracked-entity-dimensions-list') + .children() + .should('have.length', 50) + clickAddRemoveTrackedEntityTypeDimensions(entityDimensionName) + + // select a program and add program dimensions + openProgramDimensionsSidebar() + + selectProgramForTE(program.programName) + + // Check the correct time dimensions are displayed, and with program/stage specific name + cy.getBySel(`dimension-item-${program.id}.enrollmentDate`).contains( + program[DIMENSION_ID_ENROLLMENT_DATE] + ) + cy.getBySel(`dimension-item-${program.id}.incidentDate`).contains( + program[DIMENSION_ID_INCIDENT_DATE] + ) + cy.getBySel('dimension-item-lastUpdated').contains( + program[DIMENSION_ID_LAST_UPDATED] + ) + cy.getBySel('dimension-item-eventDate').should('not.exist') + cy.getBySel('dimension-item-scheduledDate').should('not.exist') + + // Add a program data dimension + clickAddRemoveProgramDataDimension(programDataDimensionName) + + // Adding time dimensions + const firstQuarterThisYear = { + type: 'Quarterly', + year: getCurrentYearStr(), + name: `January - March ${getCurrentYearStr()}`, + } + const secondQuarterThisYear = { + type: 'Quarterly', + year: getCurrentYearStr(), + name: `April - June ${getCurrentYearStr()}`, + } + + // Add the first time dimension + selectFixedPeriod({ + label: periodLabel, + period: firstQuarterThisYear, + }) + + // Check the time dimension chip and tooltip content + cy.getBySelLike('layout-chip') + .contains(periodLabel) + .trigger('mouseover') + cy.getBySel('layout-chip-tooltip-content').contains( + firstQuarterThisYear.name + ) + cy.getBySelLike('layout-chip').contains(periodLabel).trigger('mouseout') + cy.getBySel('layout-chip-tooltip-content').should('not.exist') + + // Add another time dimension + selectFixedPeriod({ + label: periodLabel, + period: secondQuarterThisYear, + selected: firstQuarterThisYear, + }) + + // Check the time dimension chip and tooltip content for the new content + cy.getBySelLike('layout-chip') + .contains(periodLabel) + .trigger('mouseover') + cy.getBySel('layout-chip-tooltip-content').contains( + `Program: ${program.programName}` + ) + cy.getBySel('layout-chip-tooltip-content').contains( + firstQuarterThisYear.name + ) + cy.getBySel('layout-chip-tooltip-content').contains( + secondQuarterThisYear.name + ) + cy.getBySelLike('layout-chip').contains(periodLabel).trigger('mouseout') + cy.getBySel('layout-chip-tooltip-content').should('not.exist') + + // Go back to person dimensions to verify that they're still listed properly + cy.getBySel('main-sidebar').contains('Person dimensions').click() + cy.getBySel('tracked-entity-dimensions-list') + .children() + .should('have.length', 50) + + clickMenubarUpdateButton() + + expectTableToBeVisible() + + assertChipContainsText(entityDimensionName, 'all') + }) + it('creates a line list without dimensions', () => { + // verify that a TE line list can be created without making any program related selections (or adding any dimensions) + selectTrackedEntityWithType('Person') + clickMenubarUpdateButton() + expectTableToBeVisible() + }) + it('displays correct dimension names and suffixes', () => { + // TODO: Test the following: + /* + Registration org.unit - always has the same name, no suffix + Person dimensions - no suffix + Your dimensions - no suffix + Organisation unit - always suffixed with program name + Program status - always suffixed with program name + Time dimensions - only suffixed with program name if there are duplicates + Program indicator - no suffix + Data elements + no duplicates - no suffix + duplicates within the program - suffixed with stage name + duplicates between different programs - suffixed with the program name + + */ + }) + //runTests() +}) + +describe(['<41'], 'tracked entity', () => { + it("is hidden and doesn't show up", () => { + goToStartPage() + // verify that e.g. the "Person dimensions" button doesn't exist + cy.getBySel('tracked-entity-button').should('not.exist') + // verify that the TE input option doesn't exist + cy.getBySel('input-tracked-entity').should('not.exist') + }) +}) + +// const runTests = () => { +// it('creates an tracked entity line list', () => { +// // check the number of columns +// getTableHeaderCells().its('length').should('equal', 3) + +// // check that there is at least 1 row +// getTableRows().its('length').should('be.gte', 1) + +// // check the column headers in the table +// getTableHeaderCells().contains('Organisation unit').should('be.visible') +// getTableHeaderCells().contains(entityDimensionName).should('be.visible') +// getTableHeaderCells().contains(periodLabel).should('be.visible') + +// //check the chips in the layout +// assertChipContainsText('Organisation unit', 1) + +// assertChipContainsText(entityDimensionName, 'all') + +// assertChipContainsText(periodLabel, 1) +// }) + +// it('moves a dimension to filter', () => { +// cy.intercept('**/api/*/analytics/**').as('getAnalytics') + +// // sort on enrollment date column +// getTableHeaderCells().find(`button[title*="${periodLabel}"]`).click() + +// // verify that the analytics request contains "asc" for enrollment date field +// cy.wait('@getAnalytics').then(({ request }) => { +// const url = new URL(request.url) + +// expect(url.searchParams.has('asc')).to.be.true +// expect(url.searchParams.get('asc')).to.equal( +// DIMENSION_ID_ENROLLMENT_DATE.toLowerCase() +// ) +// }) + +// // move date from "Columns" to "Filter" +// cy.getBySel('columns-axis') +// .findBySel('dimension-menu-button-enrollmentDate') +// .click() +// cy.contains('Move to Filter').click() + +// clickMenubarUpdateButton() + +// // verify that the analytics request does not contain "asc" +// // the sorting needs to be reset when a dimension used to sort is removed from "Columns" +// cy.wait('@getAnalytics').then(({ request }) => { +// const url = new URL(request.url) + +// expect(url.searchParams.has('asc')).to.be.false +// }) + +// // check the number of columns +// getTableHeaderCells().its('length').should('equal', 2) + +// // check that there is at least 1 row in the table +// getTableRows().its('length').should('be.gte', 1) + +// // check the column headers in the table +// getTableHeaderCells().contains('Organisation unit').should('be.visible') +// getTableHeaderCells().contains(entityDimensionName).should('be.visible') +// getTableHeaderCells().contains(periodLabel).should('not.exist') + +// //check the chips in the layout +// assertChipContainsText('Organisation unit', 1) +// assertChipContainsText(entityDimensionName, 'all') +// assertChipContainsText(periodLabel, 1) +// }) +// } diff --git a/cypress/integration/download.cy.js b/cypress/integration/download.cy.js index ecd4dc198..fabcc9eeb 100644 --- a/cypress/integration/download.cy.js +++ b/cypress/integration/download.cy.js @@ -2,6 +2,7 @@ import { E2E_PROGRAM } from '../data/index.js' import { selectEnrollmentWithProgram, selectEventWithProgram, + selectTrackedEntityWithType, } from '../helpers/dimensions.js' import { clickMenubarUpdateButton } from '../helpers/menubar.js' import { goToStartPage } from '../helpers/startScreen.js' @@ -47,4 +48,20 @@ describe('download', () => { downloadIsEnabled() }) + + it( + ['>=41'], + 'download button enables when required dimensions are selected (TE)', + () => { + goToStartPage() + + downloadIsDisabled() + + selectTrackedEntityWithType('Person') + + clickMenubarUpdateButton() + + downloadIsEnabled() + } + ) }) diff --git a/cypress/integration/eventStatus.cy.js b/cypress/integration/eventStatus.cy.js index 574bd2bdc..339573afd 100644 --- a/cypress/integration/eventStatus.cy.js +++ b/cypress/integration/eventStatus.cy.js @@ -23,7 +23,7 @@ import { const currentYear = getCurrentYearStr() -describe('event status', () => { +describe('event status (event)', () => { const event = E2E_PROGRAM const dimensionName = 'Event status' diff --git a/cypress/integration/layoutValidation.cy.js b/cypress/integration/layoutValidation.cy.js index 358296546..4f41b46a0 100644 --- a/cypress/integration/layoutValidation.cy.js +++ b/cypress/integration/layoutValidation.cy.js @@ -1,55 +1,121 @@ -import { CHILD_PROGRAM } from '../data/index.js' +import { DIMENSION_ID_ORGUNIT } from '@dhis2/analytics' +import { E2E_PROGRAM } from '../data/index.js' import { clickAddRemoveMainDimension, clickAddRemoveProgramDimension, openProgramDimensionsSidebar, + selectEnrollmentWithProgram, selectEventWithProgram, + selectTrackedEntityWithType, } from '../helpers/dimensions.js' import { clickMenubarUpdateButton } from '../helpers/menubar.js' +import { + clickOrgUnitDimensionModalUpdateButton, + deselectUserOrgUnit, + openOuDimension, +} from '../helpers/orgUnit.js' import { goToStartPage } from '../helpers/startScreen.js' import { expectTableToBeVisible } from '../helpers/table.js' -/* This files constains sequential tests, which means that some test steps - * depend on a previous step. With test isolation switched on (the default setting) - * each step (`it` block) will start off in a fresh window, and that breaks this kind - * of test. So `testIsolation` was set to false here. */ -describe('layout validation', { testIsolation: false }, () => { - const trackerProgram = CHILD_PROGRAM +describe('layout validation', () => { + const types = ['event', 'enrollment'] + types.forEach((type) => { + it(`validates that program, columns and org unit are required (${type})`, () => { + const trackerProgram = E2E_PROGRAM + + goToStartPage() + + if (type === 'enrollment') { + cy.getBySel('input-enrollment').click() + } + + clickMenubarUpdateButton() + + cy.getBySel('error-container').contains('No program selected') + + if (type === 'event') { + // select a program (without selecting stage, should auto-select) + selectEventWithProgram({ + programName: trackerProgram.programName, + }) + } else { + selectEnrollmentWithProgram({ + programName: trackerProgram.programName, + }) + // this will fail in january since default period is "last 12 months" and the first available enrollment is in january current year + // TODO: add another period if this test is run in january? + // or add a new enrollment in the test data for december last year? + } + + openProgramDimensionsSidebar() + + // remove org unit + clickAddRemoveProgramDimension('Organisation unit') + + clickMenubarUpdateButton() + + // columns is required + cy.getBySel('error-container').contains('Columns is empty') + + // add something other than org unit to columns + clickAddRemoveMainDimension('Last updated by') + + clickMenubarUpdateButton() - it('program is required', () => { + // org unit dimension is required + cy.getBySel('error-container').contains( + 'No organisation unit selected' + ) + + // remove previously added dimension + clickAddRemoveMainDimension('Last updated by') + + // add org unit to columns + clickAddRemoveProgramDimension('Organisation unit') + + clickMenubarUpdateButton() + + // validation succeeds when all above are provided + expectTableToBeVisible() + }) + }) + it(['>=41'], 'validates that type and org unit are required (TE)', () => { goToStartPage() + cy.getBySel('input-tracked-entity').click() + clickMenubarUpdateButton() - cy.getBySel('error-container').contains('No program selected') - }) - it('columns is required', () => { - // select a program (without selecting stage, should auto-select) - selectEventWithProgram({ programName: trackerProgram.programName }) + cy.getBySel('error-container').contains( + 'No tracked entity type selected' + ) - openProgramDimensionsSidebar() + selectTrackedEntityWithType('Person') // remove org unit - clickAddRemoveProgramDimension('Organisation unit') + clickAddRemoveMainDimension('Registration org. unit') clickMenubarUpdateButton() + // columns is required cy.getBySel('error-container').contains('Columns is empty') - }) - it('org unit dimension is required', () => { + // add something other than org unit to columns clickAddRemoveMainDimension('Last updated by') clickMenubarUpdateButton() - cy.getBySel('error-container').contains('No organisation unit selected') - }) - it('validation succeeds when all above are provided', () => { + // org unit isn't required + expectTableToBeVisible() + // remove previously added dimension clickAddRemoveMainDimension('Last updated by') - // add org unit to columns - clickAddRemoveProgramDimension('Organisation unit') + // add org unit to columns without any items selected + clickAddRemoveMainDimension('Registration org. unit') + openOuDimension(DIMENSION_ID_ORGUNIT) + deselectUserOrgUnit('User organisation unit') + clickOrgUnitDimensionModalUpdateButton() clickMenubarUpdateButton() diff --git a/cypress/integration/legendSet.cy.js b/cypress/integration/legendSet.cy.js index 57b6f1e71..01549351b 100644 --- a/cypress/integration/legendSet.cy.js +++ b/cypress/integration/legendSet.cy.js @@ -9,6 +9,7 @@ import { openDimension, openProgramDimensionsSidebar, selectEventWithProgram, + selectTrackedEntityWithTypeAndProgramDimensions, } from '../helpers/dimensions.js' import { deleteVisualization, saveVisualization } from '../helpers/fileMenu.js' import { @@ -44,36 +45,10 @@ const event = E2E_PROGRAM const dimensionName = TEST_DIM_LEGEND_SET const periodLabel = event[DIMENSION_ID_EVENT_DATE] -/* This files constains sequential tests, which means that some test steps - * depend on a previous step. With test isolation switched on (the default setting) - * each step (`it` block) will start off in a fresh window, and that breaks this kind - * of test. So `testIsolation` was set to false here. */ -describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { +describe('Options - Legend', () => { const defaultBackgroundColor = 'rgb(255, 255, 255)' const defaultTextColor = 'rgb(33, 41, 52)' - const TEST_LEGEND_AGE = { - name: 'Age 10y interval', - cells: [ - { value: 10, color: 'rgb(255, 255, 229)' }, - { value: 56, color: 'rgb(120, 198, 121)' }, - ], - } - - const TEST_LEGEND_E2E = { - name: 'E2E legend', - cells: { - positive: [ - { value: 10, color: 'rgb(209, 229, 240)' }, - { value: 56, color: 'rgb(103, 169, 207)' }, - ], - negative: [ - { value: -10, color: 'rgb(253, 219, 199)' }, - { value: -56, color: 'rgb(239, 138, 98)' }, - ], - }, - } - const assertCellsHaveDefaultColors = (selector) => cy .getBySel('table-body') @@ -90,7 +65,30 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { .should('have.css', 'color', defaultTextColor) }) - it('no legend is applied by default', () => { + it(['>=39'], 'apples legend correctly (event)', () => { + const TEST_LEGEND_AGE = { + name: 'Age 10y interval', + cells: [ + { value: 10, color: 'rgb(255, 255, 229)' }, + { value: 56, color: 'rgb(120, 198, 121)' }, + ], + } + + const TEST_LEGEND_E2E = { + name: 'E2E legend', + cells: { + positive: [ + { value: 10, color: 'rgb(209, 229, 240)' }, + { value: 56, color: 'rgb(103, 169, 207)' }, + ], + negative: [ + { value: -10, color: 'rgb(253, 219, 199)' }, + { value: -56, color: 'rgb(239, 138, 98)' }, + ], + }, + } + + // no legend is applied by default goToStartPage() selectEventWithProgram(E2E_PROGRAM) @@ -118,8 +116,8 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { // all cells have default background and text color assertCellsHaveDefaultColors('tr td') - }) - it('background color legend is applied (per data item)', () => { + + // background color legend is applied (per data item) openLegendOptionsModal() cy.getBySel('options-modal-content') @@ -148,8 +146,8 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { // unaffected cells (date column) have default background and text color assertCellsHaveDefaultColors('tr td:nth-child(1)') - }) - it('text color legend is applied (per data item)', () => { + + // text color legend is applied (per data item) openLegendOptionsModal() cy.getBySel('options-modal-content').should('contain', 'Legend style') @@ -180,11 +178,11 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { // unaffected cells (date column) have default background and text color assertCellsHaveDefaultColors('tr td:nth-child(1)') - }) - it('legend key is hidden by default', () => { + + // legend key is hidden by default expectLegendKeyToBeHidden() - }) - it('legend key displays correctly when enabled', () => { + + // legend key displays correctly when enabled openLegendOptionsModal() expectLegendDisplayStrategyToBeByDataItem() @@ -200,8 +198,8 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { expectLegendKeyToBeVisible() expectLegendKeyToMatchLegendSets([TEST_LEGEND_AGE.name]) - }) - it('text color legend is applied (single legend)', () => { + + // text color legend is applied (single legend) openLegendOptionsModal() cy.getBySel('options-modal-content').should('contain', 'Legend style') @@ -246,8 +244,8 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { // unaffected cells (date column) have default background and text color assertCellsHaveDefaultColors('tr td:nth-child(1)') - }) - it('background color legend is applied (single legend)', () => { + + // background color legend is applied (single legend) openLegendOptionsModal() cy.getBySel('options-modal-content').should('contain', 'Legend style') @@ -278,8 +276,8 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { // unaffected cells (date column) have default background and text color assertCellsHaveDefaultColors('tr td:nth-child(1)') - }) - it('options can be saved and loaded', () => { + + // options can be saved and loaded const AO_NAME = `TEST ${new Date().toLocaleString()}` saveVisualization(AO_NAME) expectAOTitleToContain(AO_NAME) @@ -307,8 +305,8 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { cy.getBySel('fixed-legend-set-select-content').contains( TEST_LEGEND_E2E.name ) - }) - it('legend is applied to negative values (per data item)', () => { + + // legend is applied to negative values (per data item) cy.getBySel('options-modal-content') .contains('Use pre-defined legend per data item') .click() @@ -336,16 +334,16 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { // unaffected cells (date column) have default background and text color assertCellsHaveDefaultColors('tr td:nth-child(1)') - }) - it('legend key displays correctly when two items are in use', () => { + + // legend key displays correctly when two items are in use expectLegendKeyToBeVisible() expectLegendKeyToMatchLegendSets([ TEST_LEGEND_AGE.name, TEST_LEGEND_E2E.name, ]) - }) - it('empty values do not display a legend color', () => { + + // empty values do not display a legend color const currentYear = getCurrentYearStr() unselectAllPeriods({ @@ -380,8 +378,225 @@ describe(['>=39'], 'Options - Legend', { testIsolation: false }, () => { .invoke('text') .invoke('trim') .should('not.equal', '') + + // saved AO can be deleted + deleteVisualization() + + expectRouteToBeEmpty() }) - it('saved AO can be deleted', () => { + + it(['>=41'], 'apples legend correctly (TE)', () => { + const TEST_LEGEND_AGE = { + name: 'Age 10y interval', + cells: [{ value: 46, color: 'rgb(173, 221, 142)' }], + } + + const TEST_LEGEND_E2E = { + name: 'E2E legend', + cells: [{ value: 46, color: 'rgb(103, 169, 207)' }], + } + + // no legend is applied by default + goToStartPage() + + selectTrackedEntityWithTypeAndProgramDimensions({ + typeName: 'Person', + programName: E2E_PROGRAM.programName, + dimensions: [dimensionName], + }) + + // add condition to filter out empty values + cy.getBySelLike('layout-chip').contains(dimensionName).click() + cy.getBySel('button-add-condition').click() + cy.contains('Choose a condition type').click() + cy.contains('greater than (>)').click() + cy.getBySel('conditions-modal-content') + .find('input[value=""]') + .type('1') + cy.getBySel('conditions-modal').contains('Update').click() + + expectTableToBeVisible() + + // all cells have default background and text color + assertCellsHaveDefaultColors('tr td') + + // background color legend is applied (per data item) + openLegendOptionsModal() + + cy.getBySel('options-modal-content') + .contains('Use a legend for table cell colors') + .click() + + cy.getBySel('options-modal-content').should('contain', 'Legend style') + + expectLegendDisplayStrategyToBeByDataItem() + + expectLegendDisplayStyleToBeFill() + + clickOptionsModalUpdateButton() + + expectTableToBeVisible() + + // affected cells have default text color and custom background color + TEST_LEGEND_AGE.cells.forEach((cell) => + cy + .getBySel('table-body') + .contains(cell.value) + .should('have.css', 'color', defaultTextColor) + .closest('td') + .should('have.css', 'background-color', cell.color) + ) + + // text color legend is applied (per data item) + openLegendOptionsModal() + + cy.getBySel('options-modal-content').should('contain', 'Legend style') + + expectLegendDisplayStrategyToBeByDataItem() + + expectLegendDisplayStyleToBeFill() + + cy.getBySel('options-modal-content') + .contains('Legend changes text color') + .click() + + expectLegendDisplayStyleToBeText() + + clickOptionsModalUpdateButton() + + expectTableToBeVisible() + + // affected cells have custom text color and default background color + TEST_LEGEND_AGE.cells.forEach((cell) => + cy + .getBySel('table-body') + .contains(cell.value) + .should('have.css', 'color', cell.color) + .closest('td') + .should('have.css', 'background-color', defaultBackgroundColor) + ) + + // legend key is hidden by default + expectLegendKeyToBeHidden() + + // legend key displays correctly when enabled + openLegendOptionsModal() + + expectLegendDisplayStrategyToBeByDataItem() + + cy.getBySel('options-modal-content').contains('Show legend key').click() + + expectLegendKeyOptionToBeEnabled() + + clickOptionsModalUpdateButton() + + expectTableToBeVisible() + + expectLegendKeyToBeVisible() + + expectLegendKeyToMatchLegendSets([TEST_LEGEND_AGE.name]) + + // text color legend is applied (single legend) + openLegendOptionsModal() + + cy.getBySel('options-modal-content').should('contain', 'Legend style') + + expectLegendDisplayStrategyToBeByDataItem() + + expectLegendDisplayStyleToBeText() + + cy.getBySel('options-modal-content') + .contains('Choose a single legend for the entire visualization') + .click() + + cy.getBySel('options-modal-content') + .contains('Select from legends') + .click() + + cy.getBySel('fixed-legend-set-option') + .contains(TEST_LEGEND_E2E.name) + .click() + + cy.getBySel('options-modal-content') + .contains('Legend changes text color') + .click() + + expectLegendDisplayStrategyToBeFixed() + + expectLegendDisplayStyleToBeText() + + clickOptionsModalUpdateButton() + + expectTableToBeVisible() + + // affected cells have fixed text color and default background color + TEST_LEGEND_E2E.cells.forEach((cell) => + cy + .getBySel('table-body') + .contains(cell.value) + .should('have.css', 'color', cell.color) + .closest('td') + .should('have.css', 'background-color', defaultBackgroundColor) + ) + + // background color legend is applied (single legend) + openLegendOptionsModal() + + cy.getBySel('options-modal-content').should('contain', 'Legend style') + + expectLegendDisplayStrategyToBeFixed() + + expectLegendDisplayStyleToBeText() + + cy.getBySel('options-modal-content') + .contains('Legend changes background color') + .click() + + expectLegendDisplayStyleToBeFill() + + clickOptionsModalUpdateButton() + + expectTableToBeVisible() + + // affected cells have default text color and fixed background color + TEST_LEGEND_E2E.cells.forEach((cell) => + cy + .getBySel('table-body') + .contains(cell.value) + .should('have.css', 'color', defaultTextColor) + .closest('td') + .should('have.css', 'background-color', cell.color) + ) + + // options can be saved and loaded + const AO_NAME = `TEST ${new Date().toLocaleString()}` + saveVisualization(AO_NAME) + expectAOTitleToContain(AO_NAME) + expectTableToBeVisible() + + // affected cells have default text color and fixed background color + TEST_LEGEND_E2E.cells.forEach((cell) => + cy + .getBySel('table-body') + .contains(cell.value) + .should('have.css', 'color', defaultTextColor) + .closest('td') + .should('have.css', 'background-color', cell.color) + ) + + openLegendOptionsModal() + + expectLegendDisplayStrategyToBeFixed() + + expectLegendDisplayStyleToBeFill() + + cy.getBySel('fixed-legend-set-select-content').contains( + TEST_LEGEND_E2E.name + ) + + clickOptionsModalUpdateButton() + + // saved AO can be deleted deleteVisualization() expectRouteToBeEmpty() diff --git a/cypress/integration/options.cy.js b/cypress/integration/options.cy.js index e92fd9dfd..00571c75b 100644 --- a/cypress/integration/options.cy.js +++ b/cypress/integration/options.cy.js @@ -14,6 +14,7 @@ import { goToAO } from '../helpers/common.js' import { selectEnrollmentWithProgramDimensions, selectEventWithProgramDimensions, + selectTrackedEntityWithTypeAndProgramDimensions, } from '../helpers/dimensions.js' import { saveVisualization } from '../helpers/fileMenu.js' import { @@ -141,8 +142,8 @@ describe('options', () => { const PHONE_NUMBER = '555-1212' // assert the default dgs space on number but not phone number - getTableRows().eq(0).find('td').eq(1).should('contain', PHONE_NUMBER) - getTableRows().eq(0).find('td').eq(2).should('contain', '333 333 444') + getTableRows().eq(0).find('td').eq(1).should('have.text', PHONE_NUMBER) + getTableRows().eq(0).find('td').eq(2).should('have.text', '333 333 444') // set dgs to comma openStyleOptionsModal() @@ -153,8 +154,8 @@ describe('options', () => { cy.contains('Comma').click() clickOptionsModalUpdateButton() - getTableRows().eq(0).find('td').eq(1).should('contain', PHONE_NUMBER) - getTableRows().eq(0).find('td').eq(2).should('contain', '333,333,444') + getTableRows().eq(0).find('td').eq(1).should('have.text', PHONE_NUMBER) + getTableRows().eq(0).find('td').eq(2).should('have.text', '333,333,444') // set dgs to none openStyleOptionsModal() @@ -165,8 +166,8 @@ describe('options', () => { cy.contains('None').click() clickOptionsModalUpdateButton() - getTableRows().eq(0).find('td').eq(1).should('contain', PHONE_NUMBER) - getTableRows().eq(0).find('td').eq(2).should('contain', '333333444') + getTableRows().eq(0).find('td').eq(1).should('have.text', PHONE_NUMBER) + getTableRows().eq(0).find('td').eq(2).should('have.text', '333333444') // set dgs to space openStyleOptionsModal() @@ -177,8 +178,8 @@ describe('options', () => { cy.contains('Space').click() clickOptionsModalUpdateButton() - getTableRows().eq(0).find('td').eq(1).should('contain', PHONE_NUMBER) - getTableRows().eq(0).find('td').eq(2).should('contain', '333 333 444') + getTableRows().eq(0).find('td').eq(1).should('have.text', PHONE_NUMBER) + getTableRows().eq(0).find('td').eq(2).should('have.text', '333 333 444') }) }) @@ -244,18 +245,92 @@ describe(['>=40'], 'ou hierarchy', () => { }) }) +const testSkipRoundingForEvent = (roundedValue) => { + goToStartPage() + + // set up table + selectEventWithProgramDimensions({ + ...E2E_PROGRAM, + dimensions: [TEST_DIM_NUMBER], + }) + + selectRelativePeriod({ + label: E2E_PROGRAM[DIMENSION_ID_EVENT_DATE], + period: TEST_REL_PE_THIS_YEAR, + }) + + clickMenubarUpdateButton() + + getTableHeaderCells().find(`button[title*="${TEST_DIM_NUMBER}"]`).click() + + expectTableToBeUpdated() + + getTableRows().eq(0).find('td').eq(1).should('have.text', roundedValue) + + openDataOptionsModal() + + cy.getBySel('skip-rounding').click() + clickOptionsModalUpdateButton() + + getTableRows().eq(0).find('td').eq(1).should('have.text', 3.123456) +} + +const testSkipRoundingForEnrollment = (roundedValue) => { + goToStartPage() + + // set up table + selectEnrollmentWithProgramDimensions({ + ...E2E_PROGRAM, + dimensions: [TEST_DIM_NUMBER], + }) + + selectRelativePeriod({ + label: E2E_PROGRAM[DIMENSION_ID_ENROLLMENT_DATE], + period: TEST_REL_PE_THIS_YEAR, + }) + + clickMenubarUpdateButton() + + getTableHeaderCells().find(`button[title*="${TEST_DIM_NUMBER}"]`).click() + + expectTableToBeUpdated() + + getTableRows().eq(0).find('td').eq(1).should('have.text', roundedValue) + + openDataOptionsModal() + + cy.getBySel('skip-rounding').click() + clickOptionsModalUpdateButton() + + getTableRows().eq(0).find('td').eq(1).should('have.text', 3.123456) +} + describe('skip rounding', () => { - it('sets skip rounding', () => { + it(['<41'], 'sets skip rounding for event (below 41)', () => { + testSkipRoundingForEvent('3.1') + }) + it(['>=41'], 'sets skip rounding for event (41 and above)', () => { + testSkipRoundingForEvent('3.12') + }) + // FIXME: Blocked by backend issue https://dhis2.atlassian.net/browse/DHIS2-17027 (currently unsure if this will be backported though) + it.skip(['<41'], 'sets skip rounding for enrollment (below 41)', () => { + testSkipRoundingForEnrollment('3.1') + }) + it(['>=41'], 'sets skip rounding for enrollment (41 and above)', () => { + testSkipRoundingForEnrollment('3.12') + }) + it(['>=41'], 'sets skip rounding for tracked entity (41 and above)', () => { goToStartPage() // set up table - selectEventWithProgramDimensions({ - ...E2E_PROGRAM, + selectTrackedEntityWithTypeAndProgramDimensions({ + typeName: 'Person', + programName: E2E_PROGRAM.programName, dimensions: [TEST_DIM_NUMBER], }) selectRelativePeriod({ - label: E2E_PROGRAM[DIMENSION_ID_EVENT_DATE], + label: E2E_PROGRAM[DIMENSION_ID_ENROLLMENT_DATE], period: TEST_REL_PE_THIS_YEAR, }) @@ -267,13 +342,13 @@ describe('skip rounding', () => { expectTableToBeUpdated() - getTableRows().eq(0).find('td').eq(1).should('contain', 3.7) + getTableRows().eq(0).find('td').eq(1).should('have.text', 3.12) openDataOptionsModal() cy.getBySel('skip-rounding').click() clickOptionsModalUpdateButton() - getTableRows().eq(0).find('td').eq(1).should('contain', 3.65) + getTableRows().eq(0).find('td').eq(1).should('have.text', 3.123456) }) }) diff --git a/cypress/integration/programDimensions.cy.js b/cypress/integration/programDimensions.cy.js index 11725a6b9..6f3e1dc69 100644 --- a/cypress/integration/programDimensions.cy.js +++ b/cypress/integration/programDimensions.cy.js @@ -352,7 +352,7 @@ I.e. Scheduled date works like this: openInputSidebar() - cy.getBySel('program-select').click() + cy.getBySel('program-dimensions-program-select').click() cy.contains(E2E_PROGRAM.programName).click() diff --git a/cypress/integration/programStatus.cy.js b/cypress/integration/programStatus.cy.js new file mode 100644 index 000000000..a5d92d026 --- /dev/null +++ b/cypress/integration/programStatus.cy.js @@ -0,0 +1,72 @@ +import { + clickAddRemoveProgramDimension, + openProgramDimensionsSidebar, + selectProgramForTE, + selectTrackedEntityWithType, +} from '../helpers/dimensions.js' +import { + assertChipContainsText, + assertTooltipContainsEntries, +} from '../helpers/layout.js' +import { clickMenubarUpdateButton } from '../helpers/menubar.js' +import { goToStartPage } from '../helpers/startScreen.js' +import { + getTableHeaderCells, + expectTableToBeVisible, + expectTableToMatchRows, + getTableRows, +} from '../helpers/table.js' + +describe(['>=41'], 'program status (TE)', () => { + const setUpTable = () => { + goToStartPage() + + // Select tracked entity type + selectTrackedEntityWithType('Person') + + // Select program status from child program + openProgramDimensionsSidebar() + selectProgramForTE('Child Programme') + clickAddRemoveProgramDimension('Program status') + clickMenubarUpdateButton() + + expectTableToBeVisible() + } + + it('can be filtered by status COMPLETED', () => { + setUpTable() + + clickMenubarUpdateButton() + + // expect a table with at least 10 rows a table header and correct layout chip + expectTableToBeVisible() + getTableRows().its('length').should('be.gte', 10) + getTableHeaderCells() + .contains('Program status, Child Programme') + .should('be.visible') + assertChipContainsText('Program status', 'all', 'Child Programme') + + // Add filter 'Completed' + + cy.getBySel('columns-axis').contains('Program status').click() + cy.getBySel('program-status-checkbox') + .contains('Completed') + .click() + .find('[type="checkbox"]') + .should('be.checked') + cy.getBySelLike('programStatus-modal-action-confirm') + .contains('Update') + .click() + + // expect a table with fewer rows and the layout chip to reflect the added filter + expectTableToBeVisible() + expectTableToMatchRows([ + 'Completed', + 'Completed', + 'Completed', + 'Completed', + ]) + assertChipContainsText('Program status', 1, 'Child Programme') + assertTooltipContainsEntries(['Completed']) + }) +}) diff --git a/cypress/integration/repeatedEvents.cy.js b/cypress/integration/repeatedEvents.cy.js index cda6082a5..02ba6cf46 100644 --- a/cypress/integration/repeatedEvents.cy.js +++ b/cypress/integration/repeatedEvents.cy.js @@ -5,6 +5,8 @@ import { import { E2E_PROGRAM, TEST_REL_PE_LAST_YEAR } from '../data/index.js' import { goToAO } from '../helpers/common.js' import { + clickAddRemoveProgramDataDimension, + clickAddRemoveProgramDimension, openDimension, openInputSidebar, openProgramDimensionsSidebar, @@ -12,9 +14,18 @@ import { selectEnrollmentWithProgramDimensions, selectEventWithProgram, selectEventWithProgramDimensions, + selectProgramForTE, + selectTrackedEntityWithType, } from '../helpers/dimensions.js' import { assertChipContainsText } from '../helpers/layout.js' import { clickMenubarUpdateButton } from '../helpers/menubar.js' +import { + clickOrgUnitDimensionModalUpdateButton, + expectOrgUnitDimensionModalToBeVisible, + expectOrgUnitDimensionToNotBeLoading, + openOrgUnitTreeItem, + selectOrgUnitTreeItem, +} from '../helpers/orgUnit.js' import { selectRelativePeriod } from '../helpers/period.js' import { goToStartPage } from '../helpers/startScreen.js' import { @@ -109,11 +120,11 @@ describe('repeated events', () => { }) expectHeaderToContainExact( 0, - 'E2E - Percentage - Stage 1 - Repeatable (most recent -1)' + 'E2E - Percentage, Stage 1 - Repeatable (most recent -1)' ) expectHeaderToContainExact( 1, - 'E2E - Percentage - Stage 1 - Repeatable (most recent)' + 'E2E - Percentage, Stage 1 - Repeatable (most recent)' ) // repetition 0/2 can be set successfully @@ -124,11 +135,11 @@ describe('repeated events', () => { }) expectHeaderToContainExact( 0, - 'E2E - Percentage - Stage 1 - Repeatable (oldest)' + 'E2E - Percentage, Stage 1 - Repeatable (oldest)' ) expectHeaderToContainExact( 1, - 'E2E - Percentage - Stage 1 - Repeatable (oldest +1)' + 'E2E - Percentage, Stage 1 - Repeatable (oldest +1)' ) // repetition 2/2 can be set successfully @@ -139,19 +150,19 @@ describe('repeated events', () => { }) expectHeaderToContainExact( 0, - 'E2E - Percentage - Stage 1 - Repeatable (oldest)' + 'E2E - Percentage, Stage 1 - Repeatable (oldest)' ) expectHeaderToContainExact( 1, - 'E2E - Percentage - Stage 1 - Repeatable (oldest +1)' + 'E2E - Percentage, Stage 1 - Repeatable (oldest +1)' ) expectHeaderToContainExact( 2, - 'E2E - Percentage - Stage 1 - Repeatable (most recent -1)' + 'E2E - Percentage, Stage 1 - Repeatable (most recent -1)' ) expectHeaderToContainExact( 3, - 'E2E - Percentage - Stage 1 - Repeatable (most recent)' + 'E2E - Percentage, Stage 1 - Repeatable (most recent)' ) // switch back to event, check that repetition is cleared @@ -173,6 +184,105 @@ describe('repeated events', () => { // no repetition in header expectHeaderToContainExact(0, dimensionName) }) + it(['>=41'], 'can use repetition for TE', () => { + // switch to Tracked entity and select a type + selectTrackedEntityWithType('Person') + + openProgramDimensionsSidebar() + + selectProgramForTE(E2E_PROGRAM.programName) + + const dimensionName = 'E2E - Percentage' + + clickAddRemoveProgramDataDimension(dimensionName) + + // remove reg ou + cy.getBySel('columns-axis') + .findBySel('dimension-menu-button-ou') + .click() + cy.contains('Remove').click() + + // add program ou + clickAddRemoveProgramDimension('Organisation unit') + + // move program ou to filter + cy.getBySel('columns-axis') + .findBySel('dimension-menu-button-J1QQtmzqhJz.ou') + .click() + cy.contains('Move to Filter').click() + + // filter program ou to Badjia + cy.getBySel('filters-axis').contains('Organisation unit').click() + expectOrgUnitDimensionModalToBeVisible() + expectOrgUnitDimensionToNotBeLoading() + openOrgUnitTreeItem('Bo') + openOrgUnitTreeItem('Badjia') + selectOrgUnitTreeItem('Njandama MCHP') + clickOrgUnitDimensionModalUpdateButton() + + expectTableToBeVisible() + + assertChipContainsText(dimensionName, 'all') + + // initially only has 1 column and 1 row + getTableHeaderCells().its('length').should('equal', 1) + getTableHeaderCells().eq(0).containsExact(dimensionName) + getTableDataCells().eq(0).invoke('text').should('eq', '46') + expectRepetitionToBe({ dimensionName, recent: 1, oldest: 0 }) + + // repetition 2/0 can be set successfully + setRepetition({ dimensionName, recent: 2, oldest: 0 }) + let result = ['45', '46'] + result.forEach((value, index) => { + getTableDataCells().eq(index).invoke('text').should('eq', value) + }) + expectHeaderToContainExact( + 0, + 'E2E - Percentage, Stage 1 - Repeatable (most recent -1)' + ) + expectHeaderToContainExact( + 1, + 'E2E - Percentage, Stage 1 - Repeatable (most recent)' + ) + + // repetition 0/2 can be set successfully + setRepetition({ dimensionName, recent: 0, oldest: 2 }) + result = ['45', '46'] + result.forEach((value, index) => { + getTableDataCells().eq(index).contains(value) + }) + expectHeaderToContainExact( + 0, + 'E2E - Percentage, Stage 1 - Repeatable (oldest)' + ) + expectHeaderToContainExact( + 1, + 'E2E - Percentage, Stage 1 - Repeatable (oldest +1)' + ) + + // repetition 2/2 can be set successfully + setRepetition({ dimensionName, recent: 2, oldest: 2 }) + result = ['45', '46', '45', '46'] + result.forEach((value, index) => { + getTableDataCells().eq(index).invoke('text').should('eq', value) + }) + expectHeaderToContainExact( + 0, + 'E2E - Percentage, Stage 1 - Repeatable (oldest)' + ) + expectHeaderToContainExact( + 1, + 'E2E - Percentage, Stage 1 - Repeatable (oldest +1)' + ) + expectHeaderToContainExact( + 2, + 'E2E - Percentage, Stage 1 - Repeatable (most recent -1)' + ) + expectHeaderToContainExact( + 3, + 'E2E - Percentage, Stage 1 - Repeatable (most recent)' + ) + }) it('repetition out of bounds returns as empty value', () => { const dimensionName = 'E2E - Percentage' setUpTable({ enrollment: E2E_PROGRAM, dimensionName }) @@ -185,15 +295,15 @@ describe('repeated events', () => { }) expectHeaderToContainExact( 0, - 'E2E - Percentage - Stage 1 - Repeatable (most recent -5)' + 'E2E - Percentage, Stage 1 - Repeatable (most recent -5)' ) expectHeaderToContainExact( 2, - 'E2E - Percentage - Stage 1 - Repeatable (most recent -3)' + 'E2E - Percentage, Stage 1 - Repeatable (most recent -3)' ) expectHeaderToContainExact( 5, - 'E2E - Percentage - Stage 1 - Repeatable (most recent)' + 'E2E - Percentage, Stage 1 - Repeatable (most recent)' ) }) it('repetition is disabled for non repetable stages', () => { diff --git a/cypress/integration/save.cy.js b/cypress/integration/save.cy.js index d60f3e57e..3a07970bf 100644 --- a/cypress/integration/save.cy.js +++ b/cypress/integration/save.cy.js @@ -1,9 +1,11 @@ +import { AXIS_ID_COLUMNS } from '@dhis2/analytics' import { DIMENSION_ID_ENROLLMENT_DATE, DIMENSION_ID_EVENT_DATE, } from '../../src/modules/dimensionConstants.js' import { E2E_PROGRAM, + TEST_DIM_NUMBER, TEST_FIX_PE_DEC_LAST_YEAR, TEST_REL_PE_LAST_YEAR, } from '../data/index.js' @@ -11,14 +13,17 @@ import { goToAO } from '../helpers/common.js' import { openProgramDimensionsSidebar, selectEventWithProgram, + selectTrackedEntityWithTypeAndProgramDimensions, } from '../helpers/dimensions.js' import { deleteVisualization, + openAOByName, renameVisualization, resaveVisualization, saveVisualization, saveVisualizationAs, } from '../helpers/fileMenu.js' +import { expectAxisToHaveDimension } from '../helpers/layout.js' import { clickMenubarUpdateButton, clickMenubarInterpretationsButton, @@ -112,8 +117,8 @@ describe('rename', () => { }) describe('save', () => { - it('new AO with name saves correctly', () => { - const AO_NAME = `TEST ${new Date().toLocaleString()}` + it('new AO with name saves correctly (event)', () => { + const AO_NAME = `TEST event ${new Date().toLocaleString()}` const UPDATED_AO_NAME = AO_NAME + ' 2' setupTable() @@ -122,6 +127,11 @@ describe('save', () => { expectAOTitleToContain(AO_NAME) expectTableToBeVisible() + // open AO by name + goToStartPage() + openAOByName(AO_NAME) + expectTableToBeVisible() + // save as with name change saveVisualizationAs(UPDATED_AO_NAME) expectAOTitleToContain(UPDATED_AO_NAME) @@ -134,7 +144,49 @@ describe('save', () => { deleteVisualization() }) + it(['>=41'], 'new AO with name saves correctly (TE)', () => { + const AO_NAME = `TEST TE ${new Date().toLocaleString()}` + const UPDATED_AO_NAME = AO_NAME + ' 2' + + // set up a simple TE line list + goToStartPage() + selectTrackedEntityWithTypeAndProgramDimensions({ + typeName: 'Person', + programName: event.programName, + dimensions: [TEST_DIM_NUMBER], + }) + clickMenubarUpdateButton() + expectTableToBeVisible() + + // save with a name + saveVisualization(AO_NAME) + expectAOTitleToContain(AO_NAME) + expectTableToBeVisible() + + // open AO by name + goToStartPage() + openAOByName(AO_NAME) + expectTableToBeVisible() + // expect axis to contain dimension with properly prefixed id + expectAxisToHaveDimension( + AXIS_ID_COLUMNS, + 'J1QQtmzqhJz.jfuXZB3A1ko.Vcu7eF3ndYW' + ) + + // save as with name change + saveVisualizationAs(UPDATED_AO_NAME) + expectAOTitleToContain(UPDATED_AO_NAME) + expectTableToBeVisible() + + // save as without name change + saveVisualizationAs() + expectAOTitleToContain(UPDATED_AO_NAME + ' (copy)') + expectTableToBeVisible() + + // delete AO to clean up + deleteVisualization() + }) it('new AO without name saves correctly', () => { cy.clock(cy.clock(Date.UTC(2022, 11, 29), ['Date'])) // month is 0-indexed, 11 = December const EXPECTED_AO_NAME_PART_1 = 'Untitled Line list visualization' diff --git a/cypress/integration/timeDimensions.cy.js b/cypress/integration/timeDimensions.cy.js index 55a0d225d..80de48644 100644 --- a/cypress/integration/timeDimensions.cy.js +++ b/cypress/integration/timeDimensions.cy.js @@ -33,8 +33,9 @@ const assertTimeDimension = (dimension) => { selectFixedPeriod({ label, period: { + type: 'Monthly', year: '2023', - name: '2023', + name: 'January 2023', }, }) } else { @@ -69,7 +70,7 @@ describe(['>37', '<39'], 'time dimensions', () => { { id: DIMENSION_ID_EVENT_DATE, rowsLength: 7 }, { id: DIMENSION_ID_ENROLLMENT_DATE, rowsLength: 12 }, { id: DIMENSION_ID_INCIDENT_DATE, rowsLength: 12 }, - { id: DIMENSION_ID_LAST_UPDATED, rowsLength: 11 }, + { id: DIMENSION_ID_LAST_UPDATED, rowsLength: 10 }, ] timeDimensions.forEach((dimension) => { @@ -87,7 +88,7 @@ describe(['>=39'], 'time dimensions', () => { { id: DIMENSION_ID_EVENT_DATE, rowsLength: 7 }, { id: DIMENSION_ID_ENROLLMENT_DATE, rowsLength: 13 }, { id: DIMENSION_ID_INCIDENT_DATE, rowsLength: 13 }, - { id: DIMENSION_ID_LAST_UPDATED, rowsLength: 12 }, + { id: DIMENSION_ID_LAST_UPDATED, rowsLength: 11 }, { id: DIMENSION_ID_SCHEDULED_DATE, rowsLength: 7 }, ] diff --git a/i18n/en.pot b/i18n/en.pot index ffe09471b..2c9821f0b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-01-12T10:52:11.438Z\n" -"PO-Revision-Date: 2024-01-12T10:52:11.438Z\n" +"POT-Creation-Date: 2024-02-13T14:56:50.029Z\n" +"PO-Revision-Date: 2024-02-13T14:56:50.030Z\n" msgid "Add to {{axisName}}" msgstr "Add to {{axisName}}" @@ -176,18 +176,24 @@ msgstr[1] "And {{count}} others..." msgid "None selected" msgstr "None selected" -msgid "Showing all values for this dimension" -msgstr "Showing all values for this dimension" - msgid "Program stage: {{- stageName}}" msgstr "Program stage: {{- stageName}}" +msgid "Program: {{- programName}}" +msgstr "Program: {{- programName}}" + +msgid "Showing all values for this dimension" +msgstr "Showing all values for this dimension" + msgid "No dimensions found for '{{- searchTerm}}'" msgstr "No dimensions found for '{{- searchTerm}}'" msgid "No dimensions found in the {{- programName}} program" msgstr "No dimensions found in the {{- programName}} program" +msgid " {{- trackedEntityType}} has no dimensions" +msgstr " {{- trackedEntityType}} has no dimensions" + msgid "No dimensions found" msgstr "No dimensions found" @@ -207,15 +213,8 @@ msgstr "Event" msgid "Enrollment" msgstr "Enrollment" -msgid "Could not load programs" -msgstr "Could not load programs" - -msgid "" -"The programs couldn't be retrieved. Try again or contact your system " -"administrator." -msgstr "" -"The programs couldn't be retrieved. Try again or contact your system " -"administrator." +msgid "Tracked entity" +msgstr "Tracked entity" msgid "See individual event data from a Tracker program stage or event program." msgstr "See individual event data from a Tracker program stage or event program." @@ -223,6 +222,9 @@ msgstr "See individual event data from a Tracker program stage or event program. msgid "See data from multiple program stages in a Tracker program." msgstr "See data from multiple program stages in a Tracker program." +msgid "See individual tracked entities from one or more Tracker programs." +msgstr "See individual tracked entities from one or more Tracker programs." + msgid "Global dimensions" msgstr "Global dimensions" @@ -265,6 +267,19 @@ msgstr "Category option group set" msgid "Choose an input to get started adding program dimensions." msgstr "Choose an input to get started adding program dimensions." +msgid "Program" +msgstr "Program" + +msgid "Could not load programs" +msgstr "Could not load programs" + +msgid "" +"The programs couldn't be retrieved. Try again or contact your system " +"administrator." +msgstr "" +"The programs couldn't be retrieved. Try again or contact your system " +"administrator." + msgid "Choose a program" msgstr "Choose a program" @@ -280,6 +295,34 @@ msgstr "All" msgid "No stages found" msgstr "No stages found" +msgid "Could not load types" +msgstr "Could not load types" + +msgid "" +"The types couldn't be retrieved. Try again or contact your system " +"administrator." +msgstr "" +"The types couldn't be retrieved. Try again or contact your system " +"administrator." + +msgid "Choose a type" +msgstr "Choose a type" + +msgid "No types found" +msgstr "No types found" + +msgid "{{- itemName}} dimensions" +msgstr "{{- itemName}} dimensions" + +msgid "Filter by program usage" +msgstr "Filter by program usage" + +msgid "Clear" +msgstr "Clear" + +msgid "Search dimensions" +msgstr "Search dimensions" + msgid "Your dimensions" msgstr "Your dimensions" @@ -588,6 +631,12 @@ msgstr "" "The visualization you are trying to view could not be found, the ID could " "be incorrect or it could have been deleted." +msgid "No tracked entity type selected" +msgstr "No tracked entity type selected" + +msgid "Choose a type from the Input sidebar." +msgstr "Choose a type from the Input sidebar." + msgid "No program selected" msgstr "No program selected" @@ -642,6 +691,9 @@ msgstr "" msgid "There's a syntax problem with the analytics request." msgstr "There's a syntax problem with the analytics request." +msgid "Registration date" +msgstr "Registration date" + msgid "Last updated on" msgstr "Last updated on" @@ -660,6 +712,12 @@ msgstr "User sub-units" msgid "User sub-x2-units" msgstr "User sub-x2-units" +msgid "Registration org. unit" +msgstr "Registration org. unit" + +msgid "Organisation unit" +msgstr "Organisation unit" + msgid "Table title" msgstr "Table title" @@ -687,9 +745,6 @@ msgstr "Limit minimum/maximum values" msgid "Style" msgstr "Style" -msgid "Organisation unit" -msgstr "Organisation unit" - msgid "Event status" msgstr "Event status" diff --git a/package.json b/package.json index 262f21ca8..547dbc50a 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,9 @@ "typescript": "^4.8.4" }, "dependencies": { - "@dhis2/analytics": "^26.3.0", + "@dhis2/analytics": "^26.6.0", "@dhis2/app-runtime": "^3.4.4", - "@dhis2/ui": "^8.14.5", + "@dhis2/ui": "^9.4.2", "@dnd-kit/core": "^5.0.3", "@dnd-kit/sortable": "^6.0.1", "@dnd-kit/utilities": "^3.2.0", @@ -61,7 +61,7 @@ "styled-jsx": "^4.0.1" }, "resolutions": { - "@dhis2/ui": "^8.3.1", + "@dhis2/ui": "^9.4.2", "i18next": "^20.5.0" } } diff --git a/src/actions/current.js b/src/actions/current.js index 743a5df47..cb0954793 100644 --- a/src/actions/current.js +++ b/src/actions/current.js @@ -1,6 +1,7 @@ import { genericClientError } from '../modules/error.js' import { layoutHasProgramId, + layoutHasTrackedEntityTypeId, validateLayout, } from '../modules/layoutValidation.js' import { @@ -51,7 +52,11 @@ export const tSetCurrentFromUi = } if (!validateOnly) { - if (layoutHasProgramId(currentFromUi)) { + // proceed if the layout either has a program id or a tracked entity type id + if ( + layoutHasProgramId(currentFromUi) || + layoutHasTrackedEntityTypeId(currentFromUi) + ) { dispatch(acSetCurrent(currentFromUi)) } else { dispatch(acClearCurrent()) diff --git a/src/actions/ui.js b/src/actions/ui.js index 26db5dc48..dc9024643 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -1,3 +1,4 @@ +import { DIMENSION_ID_ORGUNIT } from '@dhis2/analytics' import { DIMENSION_TYPES_PROGRAM, DIMENSION_IDS_TIME, @@ -5,14 +6,20 @@ import { DIMENSION_ID_PROGRAM_STATUS, DIMENSION_ID_SCHEDULED_DATE, DIMENSION_ID_LAST_UPDATED, + DIMENSION_ID_CREATED, } from '../modules/dimensionConstants.js' +import { extractDimensionIdParts } from '../modules/dimensionId.js' import { getDefaultTimeDimensionsMetadata, getDynamicTimeDimensionsMetadata, getProgramAsMetadata, + getDefaultOuMetadata, } from '../modules/metadata.js' import { PROGRAM_TYPE_WITH_REGISTRATION } from '../modules/programTypes.js' -import { OUTPUT_TYPE_EVENT } from '../modules/visualization.js' +import { + OUTPUT_TYPE_EVENT, + OUTPUT_TYPE_TRACKED_ENTITY, +} from '../modules/visualization.js' import { sGetMetadataById } from '../reducers/metadata.js' import { ADD_UI_LAYOUT_DIMENSIONS, @@ -44,6 +51,8 @@ import { TOGGLE_UI_SIDEBAR_HIDDEN, TOGGLE_UI_LAYOUT_PANEL_HIDDEN, SET_UI_ACCESSORY_PANEL_ACTIVE_TAB, + UPDATE_UI_ENTITY_TYPE_ID, + CLEAR_UI_ENTITY_TYPE, } from '../reducers/ui.js' export const acSetUiDraggingId = (value) => ({ @@ -67,6 +76,11 @@ export const acClearUiStageId = (metadata) => ({ metadata, }) +export const acClearUiEntityType = () => ({ + type: CLEAR_UI_ENTITY_TYPE, + metadata: getDefaultTimeDimensionsMetadata(), +}) + export const acUpdateUiProgramId = (value, metadata) => ({ type: UPDATE_UI_PROGRAM_ID, value, @@ -79,21 +93,31 @@ export const acUpdateUiProgramStageId = (value, metadata) => ({ metadata, }) +export const acUpdateUiEntityTypeId = (value, metadata) => ({ + type: UPDATE_UI_ENTITY_TYPE_ID, + value, + metadata, +}) + const tClearUiProgramRelatedDimensions = () => (dispatch, getState) => { const { ui, metadata } = getState() const idsToRemove = ui.layout.columns .concat(ui.layout.filters) - .filter((dimensionId) => { - const dimension = metadata[dimensionId] + .filter((id) => { + const dimension = metadata[id] + const { dimensionId } = extractDimensionIdParts(id) const isProgramDataDimension = DIMENSION_TYPES_PROGRAM.has( dimension.dimensionType ) const isProgramDimension = dimensionId === DIMENSION_ID_PROGRAM_STATUS || dimensionId === DIMENSION_ID_EVENT_STATUS || - (DIMENSION_IDS_TIME.has(dimension.id) && - dimensionId !== DIMENSION_ID_LAST_UPDATED) + (dimensionId === DIMENSION_ID_ORGUNIT && + id !== DIMENSION_ID_ORGUNIT) || + (DIMENSION_IDS_TIME.has(dimensionId) && + dimensionId !== DIMENSION_ID_LAST_UPDATED) || + dimensionId === DIMENSION_ID_CREATED return isProgramDataDimension || isProgramDimension }) @@ -123,27 +147,50 @@ export const tClearUiProgramStageDimensions = } export const tSetUiInput = (value) => (dispatch) => { + dispatch(acClearUiEntityType()) dispatch(acClearUiProgram()) dispatch(tClearUiProgramRelatedDimensions()) dispatch(acClearUiRepetition()) - dispatch(acSetUiInput(value, getDefaultTimeDimensionsMetadata())) + dispatch( + acSetUiInput(value, { + ...getDefaultTimeDimensionsMetadata(), + ...getDefaultOuMetadata(value.type), + }) + ) } export const tSetUiProgram = ({ program, stage }) => - (dispatch) => { + (dispatch, getState) => { + const state = getState() dispatch(acClearUiProgram()) - dispatch(tClearUiProgramRelatedDimensions()) + const inputType = sGetUiInputType(state) + if (inputType !== OUTPUT_TYPE_TRACKED_ENTITY) { + dispatch(tClearUiProgramRelatedDimensions()) + } program && dispatch( acUpdateUiProgramId(program.id, { ...getProgramAsMetadata(program), - ...getDynamicTimeDimensionsMetadata(program, stage), + ...getDynamicTimeDimensionsMetadata( + program, + stage, + inputType + ), }) ) stage && dispatch(acUpdateUiProgramStageId(stage.id)) } +export const tSetUiEntityType = + ({ type }) => + (dispatch) => { + dispatch(acClearUiProgram()) + dispatch(tClearUiProgramRelatedDimensions()) + dispatch(acClearUiEntityType()) + dispatch(acUpdateUiEntityTypeId(type.id, { [type.id]: type })) + } + export const tClearUiStage = () => (dispatch, getState) => { const state = getState() const program = sGetMetadataById(state, sGetUiProgramId(state)) diff --git a/src/actions/visualization.js b/src/actions/visualization.js index 84852af95..45130f693 100644 --- a/src/actions/visualization.js +++ b/src/actions/visualization.js @@ -1,9 +1,9 @@ import { getUiDimensionType } from '../modules/dimensionConstants.js' +import { formatDimensionId } from '../modules/dimensionId.js' import { getDynamicTimeDimensionsMetadata, getProgramAsMetadata, } from '../modules/metadata.js' -import { formatDimensionId } from '../modules/utils.js' import { getDimensionMetadataFromVisualization } from '../modules/visualization.js' import { SET_VISUALIZATION, @@ -11,7 +11,7 @@ import { } from '../reducers/visualization.js' export const acSetVisualization = (value) => { - const { program, programStage } = value + const { outputType, program, programStage } = value const timeDimensions = getDynamicTimeDimensionsMetadata( program, programStage @@ -28,10 +28,12 @@ export const acSetVisualization = (value) => { return md } - const prefixedId = formatDimensionId( - dimension.dimension, - dimension.programStage?.id - ) + const prefixedId = formatDimensionId({ + dimensionId: dimension.dimension, + programStageId: dimension.programStage?.id, + programId: dimension.program?.id, + outputType, + }) md.push({ [prefixedId]: { diff --git a/src/api/legendSets.js b/src/api/legendSets.js index 8e46fac5d..78dcf31cd 100644 --- a/src/api/legendSets.js +++ b/src/api/legendSets.js @@ -3,7 +3,7 @@ import { DIMENSION_TYPE_PROGRAM_ATTRIBUTE, DIMENSION_TYPE_PROGRAM_INDICATOR, } from '@dhis2/analytics' -import { extractDimensionIdParts } from '../modules/utils.js' +import { extractDimensionIdParts } from '../modules/dimensionId.js' const dataElementsQuery = { resource: 'dataElements', diff --git a/src/components/App.js b/src/components/App.js index ade64bd92..07e237d5c 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,4 +1,11 @@ -import { useCachedDataQuery, convertOuLevelsToUids } from '@dhis2/analytics' +import { + useCachedDataQuery, + convertOuLevelsToUids, + USER_ORG_UNIT, + USER_ORG_UNIT_CHILDREN, + USER_ORG_UNIT_GRANDCHILDREN, + DIMENSION_ID_ORGUNIT, +} from '@dhis2/analytics' import { useDataEngine, useDataMutation } from '@dhis2/app-runtime' import { CssVariables } from '@dhis2/ui' import cx from 'classnames' @@ -17,10 +24,16 @@ import { acSetUiOpenDimensionModal, acAddParentGraphMap, acSetShowExpandedLayoutPanel, + acSetUiAccessoryPanelActiveTab, } from '../actions/ui.js' import { acSetVisualization } from '../actions/visualization.js' import { parseCondition, OPERATOR_IN } from '../modules/conditions.js' import { EVENT_TYPE } from '../modules/dataStatistics.js' +import { + DIMENSION_ID_EVENT_STATUS, + DIMENSION_ID_PROGRAM_STATUS, +} from '../modules/dimensionConstants.js' +import { formatDimensionId } from '../modules/dimensionId.js' import { analyticsGenerationError, analyticsRequestError, @@ -33,13 +46,19 @@ import { visualizationNotFoundError, } from '../modules/error.js' import history from '../modules/history.js' +import { + getDefaultOuMetadata, + getDynamicTimeDimensionsMetadata, +} from '../modules/metadata.js' +import { getParentGraphMapFromVisualization } from '../modules/parentGraphMap.js' +import { getProgramDimensions } from '../modules/programDimensions.js' import { SYSTEM_SETTINGS_DIGIT_GROUP_SEPARATOR } from '../modules/systemSettings.js' -import { getParentGraphMapFromVisualization } from '../modules/ui.js' import { DERIVED_USER_SETTINGS_DISPLAY_NAME_PROPERTY, USER_SETTINGS_DISPLAY_PROPERTY, } from '../modules/userSettings.js' import { + OUTPUT_TYPE_TRACKED_ENTITY, getDimensionMetadataFields, transformVisualization, } from '../modules/visualization.js' @@ -63,6 +82,9 @@ import { Toolbar } from './Toolbar/Toolbar.js' import StartScreen from './Visualization/StartScreen.js' import { Visualization } from './Visualization/Visualization.js' +const dimensionFields = () => + 'dimension,dimensionType,filter,program[id],programStage[id],optionSet[id],valueType,legendSet[id],repetition,items[dimensionItem~rename(id)]' + const visualizationQuery = { eventVisualization: { resource: 'eventVisualizations', @@ -71,16 +93,18 @@ const visualizationQuery = { params: ({ nameProp }) => ({ fields: [ '*', - 'columns[dimension,dimensionType,filter,programStage[id],optionSet[id],valueType,legendSet[id],repetition,items[dimensionItem~rename(id)]]', - 'rows[dimension,dimensionType,filter,programStage[id],optionSet[id],valueType,legendSet[id],repetition,items[dimensionItem~rename(id)]]', - 'filters[dimension,dimensionType,filter,programStage[id],optionSet[id],valueType,legendSet[id],repetition,items[dimensionItem~rename(id)]]', + `columns[${dimensionFields}]`, + `rows[${dimensionFields}]`, + `filters[${dimensionFields}]`, `program[id,programType,${nameProp}~rename(name),displayEnrollmentDateLabel,displayIncidentDateLabel,displayIncidentDate,programStages[id,displayName~rename(name),repeatable]]`, 'programStage[id,displayName~rename(name),displayExecutionDateLabel,displayDueDateLabel,hideDueDate,repeatable]', + `programDimensions[id,${nameProp}~rename(name),enrollmentDateLabel,incidentDateLabel,programType,displayIncidentDate,displayEnrollmentDateLabel,displayIncidentDateLabel,programStages[id,${nameProp}~rename(name),repeatable,hideDueDate,displayExecutionDateLabel,displayDueDateLabel]]`, 'access', 'href', ...getDimensionMetadataFields(), 'dataElementDimensions[legendSet[id,name],dataElement[id,name]]', 'legend[set[id,displayName],strategy,style,showKey]', + 'trackedEntityType[id,displayName~rename(name)]', '!interpretations', '!userGroupAccesses', '!publicAccess', @@ -266,8 +290,17 @@ const App = () => { dispatch(acSetUiOpenDimensionModal(dimensionId)) const onResponsesReceived = (response) => { - const itemsMetadata = Object.entries(response.metaData.items).reduce( - (obj, [id, item]) => { + const itemsMetadata = Object.entries(response.metaData.items) + .filter( + ([item]) => + ![ + USER_ORG_UNIT, + USER_ORG_UNIT_CHILDREN, + USER_ORG_UNIT_GRANDCHILDREN, + DIMENSION_ID_ORGUNIT, + ].includes(item) + ) + .reduce((obj, [id, item]) => { obj[id] = { id, name: item.name || item.displayName, @@ -277,9 +310,7 @@ const App = () => { } return obj - }, - {} - ) + }, {}) dispatch(acAddMetadata(itemsMetadata)) dispatch(acSetVisualizationLoading(false)) @@ -347,8 +378,90 @@ const App = () => { } } } + if (Object.keys(optionSetsMetadata).length) { + dispatch(acAddMetadata(optionSetsMetadata)) + } + } - dispatch(acAddMetadata(optionSetsMetadata)) + const addTrackedEntityTypeMetadata = (visualization) => { + const { id, name } = visualization.trackedEntityType || {} + + if (id && name) { + dispatch(acAddMetadata({ [id]: { id, name } })) + } + } + const addFixedDimensionsMetadata = (visualization) => { + const fixedDimensionsMetadata = {} + + const dimensions = [ + ...(visualization.columns || []), + ...(visualization.rows || []), + ...(visualization.filters || []), + ] + + for (const dimension of dimensions.filter( + (d) => + [ + DIMENSION_ID_ORGUNIT, + DIMENSION_ID_EVENT_STATUS, + DIMENSION_ID_PROGRAM_STATUS, + ].includes(d.dimension) && d.program?.id + )) { + const dimensionId = formatDimensionId({ + dimensionId: dimension.dimension, + programId: dimension.program.id, + outputType: visualization.outputType, + }) + const metadata = getProgramDimensions(dimension.program.id)[ + dimensionId + ] + + if (metadata) { + fixedDimensionsMetadata[dimensionId] = metadata + } + } + if ( + visualization.outputType === OUTPUT_TYPE_TRACKED_ENTITY && + dimensions.some((d) => d.dimension === DIMENSION_ID_ORGUNIT) + ) { + fixedDimensionsMetadata[DIMENSION_ID_ORGUNIT] = + getDefaultOuMetadata(visualization.outputType)[ + DIMENSION_ID_ORGUNIT + ] + } + if (Object.keys(fixedDimensionsMetadata).length) { + dispatch(acAddMetadata(fixedDimensionsMetadata)) + } + } + + const addProgramDimensionsMetadata = (visualization) => { + const programDimensionsMetadata = {} + + visualization.programDimensions.forEach((program) => { + programDimensionsMetadata[program.id] = program + + const timeDimensions = getDynamicTimeDimensionsMetadata(program) + Object.keys(timeDimensions).forEach((timeDimensionId) => { + const formattedId = formatDimensionId({ + dimensionId: timeDimensionId, + programId: program.id, + outputType: visualization.outputType, + }) + programDimensionsMetadata[formattedId] = { + ...timeDimensions[timeDimensionId], + id: formattedId, + } + }) + + if (program.programStages) { + program.programStages.forEach((stage) => { + programDimensionsMetadata[stage.id] = stage + }) + } + }) + if (Object.keys(programDimensionsMetadata).length) { + dispatch(acAddMetadata(programDimensionsMetadata)) + } } useEffect(() => { @@ -361,6 +474,12 @@ const App = () => { ) addOptionSetsMetadata(visualization) + addTrackedEntityTypeMetadata(visualization) + addFixedDimensionsMetadata(visualization) + if (visualization.outputType === OUTPUT_TYPE_TRACKED_ENTITY) { + addProgramDimensionsMetadata(visualization) + dispatch(acSetUiAccessoryPanelActiveTab()) + } dispatch( acAddParentGraphMap( diff --git a/src/components/Dialogs/Conditions/AlphanumericCondition.js b/src/components/Dialogs/Conditions/AlphanumericCondition.js index bfd9f3b16..c1c6545c1 100644 --- a/src/components/Dialogs/Conditions/AlphanumericCondition.js +++ b/src/components/Dialogs/Conditions/AlphanumericCondition.js @@ -83,7 +83,7 @@ const BaseCondition = ({ key={key} value={key} label={value} - dataTest={'alphanumeric-condition-type'} + dataTest="alphanumeric-condition-type" /> ) )} @@ -105,7 +105,7 @@ const BaseCondition = ({ onChange={({ checked }) => toggleCaseSensitive(checked)} dense className={classes.caseSensitiveCheckbox} - dataTest={'condition-case-sensitive-checkbox'} + dataTest="condition-case-sensitive-checkbox" /> )}