diff --git a/cypress/elements/chart.js b/cypress/elements/chart.js index a78f41ff32..e2f2bdb966 100644 --- a/cypress/elements/chart.js +++ b/cypress/elements/chart.js @@ -2,6 +2,7 @@ import { VIS_TYPE_COLUMN, VIS_TYPE_GAUGE, VIS_TYPE_PIE, + VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_PIVOT_TABLE, VIS_TYPE_SINGLE_VALUE, VIS_TYPE_YEAR_OVER_YEAR_COLUMN, @@ -23,7 +24,11 @@ const AOTitleDirtyEl = 'titlebar-dirty' const timeout = { timeout: 40000, } -const nonHighchartsTypes = [VIS_TYPE_PIVOT_TABLE, VIS_TYPE_SINGLE_VALUE] +const nonHighchartsTypes = [ + VIS_TYPE_OUTLIER_TABLE, + VIS_TYPE_PIVOT_TABLE, + VIS_TYPE_SINGLE_VALUE, +] export const expectVisualizationToBeVisible = (visType = VIS_TYPE_COLUMN) => nonHighchartsTypes.includes(visType) diff --git a/cypress/elements/dimensionModal/dataDimension.js b/cypress/elements/dimensionModal/dataDimension.js index 8a71c5cdc9..9a26f5fe44 100644 --- a/cypress/elements/dimensionModal/dataDimension.js +++ b/cypress/elements/dimensionModal/dataDimension.js @@ -11,6 +11,8 @@ const selectableItemsEl = 'data-dimension-transfer-sourceoptions' const selectedItemsEl = 'data-dimension-transfer-pickedoptions' const dataTypesSelectButtonEl = 'data-dimension-left-header-data-types-select-field-content' +const dataTypesSelectHelpEl = + 'data-dimension-left-header-data-types-select-field-help' const dataTypeSelectOptionEl = 'data-dimension-left-header-data-types-select-field-option' const groupSelectButtonEl = @@ -78,6 +80,9 @@ export const switchDataTab = (tabName) => { export const expectDataTypeToBe = (type) => cy.getBySel(dataTypesSelectButtonEl).should('contain', type) +export const expectDataTypeSelectHelpToContain = (text) => + cy.getBySel(dataTypesSelectHelpEl).should('have.text', text) + export const expectGroupSelectToNotBeVisible = () => cy.getBySel(groupSelectButtonEl).should('not.exist') @@ -111,8 +116,13 @@ export const switchSubGroupTo = (group) => { } export const switchDataTypeTo = (dataType) => { - cy.getBySel(dataTypesSelectButtonEl).click() - cy.getBySelLike(dataTypeSelectOptionEl).contains(dataType).click() + cy.getBySel(dataTypesSelectButtonEl).then(($typesSelect) => { + // account for disabled type selector with preselected item + if (!$typesSelect.text().includes(dataType)) { + cy.getBySel(dataTypesSelectButtonEl).click() + cy.getBySelLike(dataTypeSelectOptionEl).contains(dataType).click() + } + }) } export const switchDataTypeToAll = () => { diff --git a/cypress/elements/dimensionModal/index.js b/cypress/elements/dimensionModal/index.js index 9fbc71367f..ff96636212 100644 --- a/cypress/elements/dimensionModal/index.js +++ b/cypress/elements/dimensionModal/index.js @@ -113,6 +113,7 @@ export { expectDataItemToBeInactive, expectDataDimensionModalToBeVisible, expectDataTypeToBe, + expectDataTypeSelectHelpToContain, expectGroupSelectToNotBeVisible, expectNoDataItemsToBeSelected, inputSearchTerm, @@ -153,6 +154,8 @@ export { expectFixedPeriodTypeToBe, expectSelectablePeriodItemsAmountToBeLeast, expectSelectablePeriodItemsAmountToBe, + expectPeriodItemToBeInactive, + expectPeriodDimensionModalWarningToContain, } from './periodDimension.js' export { diff --git a/cypress/elements/dimensionModal/periodDimension.js b/cypress/elements/dimensionModal/periodDimension.js index 5de5e8020a..e5e4aa7782 100644 --- a/cypress/elements/dimensionModal/periodDimension.js +++ b/cypress/elements/dimensionModal/periodDimension.js @@ -9,13 +9,14 @@ const fixedPeriodsPeriodTypeButtonEl = 'period-dimension-fixed-period-filter-period-type-content' const periodTypeMenuEl = 'dhis2-uicore-select-menu-menuwrapper' //const fixedPeriodsYearEl = 'period-dimension-fixed-period-filter-year-content' +const optionContentEl = 'period-dimension-transfer-option-content' const selectableItemsEl = 'period-dimension-transfer-sourceoptions' const selectedItemsEl = 'period-dimension-transfer-pickedoptions' const relativePeriodTypeSelectOptionEl = 'period-dimension-relative-period-filter-option' - const fixedPeriodTypeSelectOptionEl = 'period-dimension-fixed-period-filter-period-type-option' +const rightHeaderEl = 'period-dimension-transfer-rightheader' export const expectPeriodDimensionModalToBeVisible = () => expectDimensionModalToBeVisible(DIMENSION_ID_PERIOD) @@ -96,6 +97,12 @@ export const expectFixedPeriodTypeSelectToNotContain = (periodType) => { ) } +export const expectPeriodItemToBeInactive = (id) => + cy + .get(`[data-value="${id}"]`) + .findBySel(optionContentEl) + .should('have.class', 'inactive') + export const openRelativePeriodsTypeSelect = () => cy.getBySel(relativePeriodsPeriodTypeButtonEl).click() @@ -117,3 +124,6 @@ export const expectSelectablePeriodItemsAmountToBeLeast = (amount) => $container.find('[data-test="period-dimension-transfer-option"]') ).to.have.length.of.at.least(amount) }) + +export const expectPeriodDimensionModalWarningToContain = (text) => + cy.getBySel(rightHeaderEl).should('contain', text) diff --git a/cypress/integration/visTypes/outlierTable.cy.js b/cypress/integration/visTypes/outlierTable.cy.js new file mode 100644 index 0000000000..751448ddb2 --- /dev/null +++ b/cypress/integration/visTypes/outlierTable.cy.js @@ -0,0 +1,334 @@ +import { + DIMENSION_ID_DATA, + DIMENSION_ID_ORGUNIT, + DIMENSION_ID_PERIOD, + visTypeDisplayNames, + VIS_TYPE_OUTLIER_TABLE, + AXIS_ID_COLUMNS, +} from '@dhis2/analytics' +import { expectVisualizationToBeVisible } from '../../elements/chart.js' +import { + clearInput, + typeInput, + expectAppToNotBeLoading, +} from '../../elements/common.js' +import { + selectDataElements, + clickDimensionModalHideButton, + clickDimensionModalUpdateButton, + expectDataTypeToBe, + expectDataTypeSelectHelpToContain, + expectPeriodItemToBeInactive, + expectPeriodDimensionModalWarningToContain, + selectRelativePeriods, + unselectAllItemsByButton, +} from '../../elements/dimensionModal/index.js' +import { openDimension } from '../../elements/dimensionsPanel.js' +import { deleteAO, saveNewAO } from '../../elements/fileMenu/index.js' +import { + expectDimensionOnAxisToHaveLockIcon, + expectDimensionOnAxisToHaveWarningIcon, + expectDimensionToHaveItemAmount, +} from '../../elements/layout.js' +import { + clickMenuBarUpdateButton, + openOptionsModal, +} from '../../elements/menuBar.js' +import { + clickOptionsTab, + clickOptionsModalHideButton, + clickOptionsModalUpdateButton, + OPTIONS_TAB_DATA, + OPTIONS_TAB_OUTLIERS, +} from '../../elements/optionsModal/index.js' +import { expectRouteToBeEmpty } from '../../elements/route.js' +import { + expectErrorToContainTitle, + expectStartScreenToBeVisible, + goToStartPage, +} from '../../elements/startScreen.js' +import { changeVisType } from '../../elements/visualizationTypeSelector.js' + +const TEST_DATA_ELEMENT_NAMES = ['ANC 1st visit', 'ANC 2nd visit'] +const TEST_VIS_NAME = `TEST OUTLIER TABLE ${new Date().toLocaleString()}` + +const maxResultsEl = 'option-outliers-max-result-input-content' +const NEW_MAX_RESULTS = 11 + +const detectionMethodEl = 'options-outliers-detection-method' +const NEW_DETECTION_METHOD = 'Z_SCORE' +const newDetectionMethodSelector = `input[value=STANDARD_${NEW_DETECTION_METHOD}]` +const thresholdEl = 'options-outliers-threshold-content' +const NEW_THRESHOLD = 2 + +describe(['>=41'], 'using an Outlier table visualization', () => { + it('creates, edits, saves it correctly', () => { + cy.log('navigates to a new Outlier table visualization') + goToStartPage() + changeVisType(visTypeDisplayNames[VIS_TYPE_OUTLIER_TABLE]) + + cy.log('Data is locked to Columns') + expectDimensionOnAxisToHaveLockIcon(DIMENSION_ID_DATA, AXIS_ID_COLUMNS) + + cy.log('Period is locked to Columns') + expectDimensionOnAxisToHaveLockIcon( + DIMENSION_ID_PERIOD, + AXIS_ID_COLUMNS + ) + + cy.log('Org unit is locked to Columns') + expectDimensionOnAxisToHaveLockIcon( + DIMENSION_ID_ORGUNIT, + AXIS_ID_COLUMNS + ) + + cy.log('Other dimensions section is disabled') + cy.getBySel('dimensions-panel-list-dynamic-dimensions') + .findBySel('dimensions-panel-list-dimension-item') + .should('not.have.css', 'cursor', 'pointer') + + cy.log('Your dimensions section is disabled') + cy.getBySel('dimensions-panel-list-non-predefined-dimensions') + .findBySel('dimensions-panel-list-dimension-item') + .should('not.have.css', 'cursor', 'pointer') + + cy.log( + 'shows a disabled data type selector with preselected Data elements' + ) + openDimension(DIMENSION_ID_DATA) + expectDataTypeToBe('Data elements') + expectDataTypeSelectHelpToContain( + 'Only Data elements can be used in Outlier table' + ) + clickDimensionModalHideButton() + + cy.log('shows error if no data element selected') + clickMenuBarUpdateButton() + expectErrorToContainTitle('No data selected') + + cy.log('selects 2 data elements') + openDimension(DIMENSION_ID_DATA) + selectDataElements(TEST_DATA_ELEMENT_NAMES) + clickDimensionModalHideButton() + expectDimensionToHaveItemAmount(DIMENSION_ID_DATA, 2) + + cy.log('shows error if no period is selected') + openDimension(DIMENSION_ID_PERIOD) + unselectAllItemsByButton() + clickDimensionModalUpdateButton() + expectErrorToContainTitle('No period selected') + + cy.log('adds 2 periods and displays a warning message') + openDimension(DIMENSION_ID_PERIOD) + selectRelativePeriods(['Last 12 months', 'This month'], 'Months') + expectPeriodItemToBeInactive('THIS_MONTH') + expectPeriodDimensionModalWarningToContain( + "'Outlier table' is intended to show a single item for this type of dimension. Only the first item will be used and saved." + ) + clickDimensionModalHideButton() + expectDimensionOnAxisToHaveWarningIcon( + DIMENSION_ID_PERIOD, + AXIS_ID_COLUMNS + ) + + cy.log('shows an outlier table') + clickMenuBarUpdateButton() + expectVisualizationToBeVisible(VIS_TYPE_OUTLIER_TABLE) + + cy.log('has the correct headers in the table') + const modZScoreHeaderLabels = [ + 'Data', + 'Category option combination', + 'Period', + 'Organisation unit', + 'Value', + 'Median', + 'Modified Z-score', + 'Median absolute deviation', + 'Min', + 'Max', + ] + + cy.getBySel('outlier-table') + .findBySel('table-header') + .each(($el, index) => { + expect($el).to.contain(modZScoreHeaderLabels[index]) + }) + + cy.log('has the correct default number of rows') + cy.getBySel('outlier-table') + .findBySel('table-row') + .should('have.length', 20) + + cy.log('has default sorting on Value descending') + cy.getBySel('outlier-table') + .findBySel('table-header') + .containsExact('Value') + .closest('[data-test="table-header"]') + .find('button') + .should('have.attr', 'title', 'Sort ascending by Value and update') + + cy.log('can be sorted on a different column') + cy.getBySel('outlier-table') + .findBySel('table-header') + .find('button[title="Sort descending by Min and update"]') + .click() + + expectVisualizationToBeVisible(VIS_TYPE_OUTLIER_TABLE) + + cy.getBySel('outlier-table') + .findBySel('table-header') + .containsExact('Value') + .closest('[data-test="table-header"]') + .find('button') + .should('have.attr', 'title', 'Sort descending by Value and update') + + cy.getBySel('outlier-table') + .findBySel('table-header') + .containsExact('Min') + .closest('[data-test="table-header"]') + .find('button') + .should('have.attr', 'title', 'Sort ascending by Min and update') + + cy.log('Options -> Data -> change max results') + openOptionsModal(OPTIONS_TAB_DATA) + + cy.intercept('GET', '**/analytics/outlierDetection?*').as( + 'analyticsRequest' + ) + + // NB. typing the 1st 2 digits of the number is a workaround since it's not possible to directly input the whole number without clearing first + // and when clearing the validation sets the value to 1 (the minimum allowed value) + + // min value is 1 + clearInput(maxResultsEl) + cy.getBySel(maxResultsEl).find('input').should('have.value', 1) + + // max value is 500 + typeInput('option-outliers-max-result-input-content', 50) + cy.getBySel(maxResultsEl).find('input').should('have.value', 500) + + clearInput(maxResultsEl) + typeInput('option-outliers-max-result-input-content', 1) + cy.getBySel(maxResultsEl) + .find('input') + .should('have.value', NEW_MAX_RESULTS) + + clickOptionsModalUpdateButton() + + cy.wait('@analyticsRequest').then(({ request, response }) => { + expect(request.url).to.contain(`maxResults=${NEW_MAX_RESULTS}`) + expect(response.body.rows.length).to.eq(NEW_MAX_RESULTS) + }) + + expectVisualizationToBeVisible(VIS_TYPE_OUTLIER_TABLE) + + cy.getBySel('outlier-table') + .findBySel('table-row') + .should('have.length', NEW_MAX_RESULTS) + + cy.log('Options -> Outliers -> change detection method') + openOptionsModal(OPTIONS_TAB_OUTLIERS) + + cy.getBySel(detectionMethodEl).find(newDetectionMethodSelector).click() + + cy.getBySel(detectionMethodEl) + .find(newDetectionMethodSelector) + .should('be.checked') + + clickOptionsModalUpdateButton() + + cy.wait('@analyticsRequest').then(({ request, response }) => { + expect(request.url).to.contain(`algorithm=${NEW_DETECTION_METHOD}`) + expect(response.body.metaData.algorithm).to.eq(NEW_DETECTION_METHOD) + }) + + expectVisualizationToBeVisible(VIS_TYPE_OUTLIER_TABLE) + + const headerLabelsZScore = [ + 'Data', + 'Category option combination', + 'Period', + 'Organisation unit', + 'Value', + 'Mean', + 'Z-score', + 'Standard deviation', + 'Min', + 'Max', + ] + + cy.getBySel('outlier-table') + .findBySel('table-header') + .each(($el, index) => { + expect($el).to.contain(headerLabelsZScore[index]) + }) + + cy.log('Options -> Outliers -> change threshold') + openOptionsModal(OPTIONS_TAB_OUTLIERS) + + clearInput(thresholdEl) + typeInput(thresholdEl, NEW_THRESHOLD) + + cy.getBySel(thresholdEl) + .find('input') + .should('have.value', NEW_THRESHOLD) + + clickOptionsModalUpdateButton() + + cy.wait('@analyticsRequest').then(({ request, response }) => { + expect(request.url).to.contain(`threshold=${NEW_THRESHOLD}`) + expect(response.body.metaData.threshold).to.eq(NEW_THRESHOLD) + }) + + expectVisualizationToBeVisible(VIS_TYPE_OUTLIER_TABLE) + + cy.log('shows a custom error screen if no data returned') + openOptionsModal(OPTIONS_TAB_OUTLIERS) + + clearInput(thresholdEl) + typeInput(thresholdEl, 10) + + clickOptionsModalUpdateButton() + + expectErrorToContainTitle('No outliers found') + + // reset before save + openOptionsModal(OPTIONS_TAB_OUTLIERS) + clearInput(thresholdEl) + typeInput(thresholdEl, NEW_THRESHOLD) + + clickOptionsModalUpdateButton() + + expectVisualizationToBeVisible(VIS_TYPE_OUTLIER_TABLE) + + cy.log('saves and all options are preserved') + saveNewAO(TEST_VIS_NAME) + + expectAppToNotBeLoading() + expectVisualizationToBeVisible(VIS_TYPE_OUTLIER_TABLE) + + openOptionsModal(OPTIONS_TAB_DATA) + + cy.getBySel(maxResultsEl) + .find('input') + .should('have.value', NEW_MAX_RESULTS) + + clickOptionsTab(OPTIONS_TAB_OUTLIERS) + + cy.getBySel(detectionMethodEl) + .find(newDetectionMethodSelector) + .should('be.checked') + + cy.getBySel(thresholdEl) + .find('input') + .should('have.value', NEW_THRESHOLD) + + clickOptionsModalHideButton() + + cy.log('deletes saved Outlier table AO') + deleteAO() + expectStartScreenToBeVisible() + expectRouteToBeEmpty() + }) +}) diff --git a/cypress/support/getExcludedTags.js b/cypress/support/getExcludedTags.js deleted file mode 100644 index b2fdb6a9f6..0000000000 --- a/cypress/support/getExcludedTags.js +++ /dev/null @@ -1,99 +0,0 @@ -const d2config = require('../../d2.config.js') -/* -The list of excluded tags returned by getExcludedTags are the tags that cypress -will ignore when running the test suite. So if a test is tagged with one of the -tags in the excluded list, then that test will not run. - -Using excluded tags (instead of included tags) allows for most of the tests to -remain untagged and be run against all supported versions of DHIS2. - -DHIS2 officially supports the latest 3 released versions of DHIS2. - -For example: 2.38, 2.39 and 2.40. Dev would then have version 2.41-SNAPSHOT. -Therefore, the getExcludedTags calculates the range of tags based on minimum -supported version + 3 (2.38, 2.39, 2.40, 2.41-SNAPSHOT) - -With the minimum supported version of 2.38, the tags will always -contain "38", "39", "40" and "41", but the comparison symbols will depend on -the current instance version. - -Allowed tag comparisons are ">", ">=", "<", "<=" -*/ - -const getString = (v) => (typeof v === 'number' ? v.toString() : v) - -const extractMinorVersion = (v) => - v.indexOf('2.') === 0 ? parseInt(v.slice(2, 4)) : parseInt(v.slice(0, 2)) - -const MIN_DHIS2_VERSION = extractMinorVersion( - getString(d2config.minDHIS2Version) -) - -const getInstanceMinorVersion = (dhis2InstanceVersion) => { - if (dhis2InstanceVersion.toLowerCase() === 'dev') { - return MIN_DHIS2_VERSION + 3 - } - - return extractMinorVersion(dhis2InstanceVersion) -} - -const getExcludedTags = (v) => { - const currentInstanceVersion = getInstanceMinorVersion(getString(v)) - - if (currentInstanceVersion < MIN_DHIS2_VERSION) { - throw new Error( - 'Instance version is lower than the minimum supported version' - ) - } - - let excludeTags = [] - if (currentInstanceVersion === MIN_DHIS2_VERSION) { - // For example instance = 2.38, MIN = 2.38 - excludeTags = [ - `<${currentInstanceVersion}`, - `>${currentInstanceVersion}`, - `>=${currentInstanceVersion + 1}`, - `>${currentInstanceVersion + 1}`, - `>=${currentInstanceVersion + 2}`, - `>${currentInstanceVersion + 2}`, - `>=${currentInstanceVersion + 3}`, - ] - } else if (currentInstanceVersion === MIN_DHIS2_VERSION + 1) { - // For example instance = 2.39, MIN = 2.38 - excludeTags = [ - `<=${currentInstanceVersion - 1}`, - `<${currentInstanceVersion - 1}`, - `<${currentInstanceVersion}`, - `>${currentInstanceVersion}`, - `>=${currentInstanceVersion + 1}`, - `>${currentInstanceVersion + 1}`, - `>=${currentInstanceVersion + 2}`, - ] - } else if (currentInstanceVersion === MIN_DHIS2_VERSION + 2) { - // For example instance = 2.40, MIN = 2.38 - excludeTags = [ - `<=${currentInstanceVersion - 2}`, - `<${currentInstanceVersion - 2}`, - `<=${currentInstanceVersion - 1}`, - `<${currentInstanceVersion - 1}`, - `<${currentInstanceVersion}`, - `>${currentInstanceVersion}`, - `>=${currentInstanceVersion + 1}`, - ] - } else { - // For example instance = 2.41, MIN = 2.38 - excludeTags = [ - `<=${currentInstanceVersion - 3}`, - `<${currentInstanceVersion - 3}`, - `<=${currentInstanceVersion - 2}`, - `<${currentInstanceVersion - 2}`, - `<${currentInstanceVersion - 1}`, - `<=${currentInstanceVersion - 1}`, - `<${currentInstanceVersion}`, - ] - } - - return excludeTags -} - -module.exports = { getExcludedTags } diff --git a/package.json b/package.json index 08e33654c1..0c8862f850 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "private": true, "scripts": { "deduplicate": "d2-app-scripts deduplicate", - "postinstall": "patch-package", "build": "d2-app-scripts build", "start": "d2-app-scripts start", "start:nobrowser": "BROWSER=none yarn start", @@ -35,10 +34,10 @@ "eslint-plugin-react-hooks": "^4.2.0", "jest-enzyme": "^7.1.2", "loglevel": "^1.8.1", - "patch-package": "^7.0.0", "postinstall-postinstall": "^2.1.0", "redux-mock-store": "^1.5.4", - "start-server-and-test": "^2.0.0" + "start-server-and-test": "^2.0.0", + "typescript": "^4.8.4" }, "dependencies": { "@dhis2/analytics": "^26.6.0", diff --git a/patches/react-scripts+5.0.1.patch b/patches/react-scripts+5.0.1.patch deleted file mode 100644 index e14ed4776a..0000000000 --- a/patches/react-scripts+5.0.1.patch +++ /dev/null @@ -1,25 +0,0 @@ -diff --git a/node_modules/react-scripts/config/webpack.config.js b/node_modules/react-scripts/config/webpack.config.js -index e465d8e..2bc428c 100644 ---- a/node_modules/react-scripts/config/webpack.config.js -+++ b/node_modules/react-scripts/config/webpack.config.js -@@ -239,19 +239,7 @@ module.exports = function (webpackEnv) { - : isEnvDevelopment && - (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')), - }, -- cache: { -- type: 'filesystem', -- version: createEnvironmentHash(env.raw), -- cacheDirectory: paths.appWebpackCache, -- store: 'pack', -- buildDependencies: { -- defaultWebpack: ['webpack/lib/'], -- config: [__filename], -- tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f => -- fs.existsSync(f) -- ), -- }, -- }, -+ cache: false, - infrastructureLogging: { - level: 'none', - }, diff --git a/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js b/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js index 4f7cb0f951..a3d0ef1215 100644 --- a/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js +++ b/src/components/VisualizationOptions/Options/OutlierDetectionMethod.js @@ -13,7 +13,7 @@ const OutlierDetectionMethod = ({ }) => ( <>