From fe128adeef9353a05e2d25b31fb9eb9577cef090 Mon Sep 17 00:00:00 2001 From: Karoline Tufte Lien Date: Thu, 30 Jul 2026 16:59:25 +0200 Subject: [PATCH 1/5] fix: open Maps in a new tab instead of navigating away [DHIS2-17366] "Open as Map" replaced the current Data Visualizer tab via window.location.href, so any unsaved work in that tab was lost. Use window.open with target=_blank instead, matching how "Open in [App]" already behaves in Dashboard and Maps. --- .../VisualizationTypeSelector/VisualizationTypeSelector.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx index e0ec9f9dfd..93a059782e 100644 --- a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx +++ b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx @@ -66,7 +66,10 @@ const UnconnectedVisualizationTypeSelector = ({ set(currentAnalyticalObject) - window.location.href = `${baseUrl}/${MAPS_APP_URL}/#/${USER_DATASTORE_CURRENT_AO_KEY}` + window.open( + `${baseUrl}/${MAPS_APP_URL}/#/${USER_DATASTORE_CURRENT_AO_KEY}`, + '_blank' + ) } const VisTypesList = ( From 1da92420d70188f4fdbab149570be7a55f578a6c Mon Sep 17 00:00:00 2001 From: Karoline Tufte Lien Date: Thu, 30 Jul 2026 17:54:27 +0200 Subject: [PATCH 2/5] test: add e2e coverage for Open as Map opening in a new tab [DHIS2-17366] --- cypress/elements/visualizationTypeSelector.js | 5 +++ cypress/integration/openAsMap.cy.js | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 cypress/integration/openAsMap.cy.js diff --git a/cypress/elements/visualizationTypeSelector.js b/cypress/elements/visualizationTypeSelector.js index 6620e30e20..b97a32314a 100644 --- a/cypress/elements/visualizationTypeSelector.js +++ b/cypress/elements/visualizationTypeSelector.js @@ -10,6 +10,11 @@ export const changeVisType = (visTypeName) => { cy.getBySel(vstCardEl).contains(visTypeName).click() } +export const clickOpenAsMap = () => { + clickVisTypeSelector() + cy.getBySel(vstCardEl).contains('Open as Map').click() +} + export const expectVisTypeToBeValue = (value) => cy .getBySel(vstButtonTextEl) diff --git a/cypress/integration/openAsMap.cy.js b/cypress/integration/openAsMap.cy.js new file mode 100644 index 0000000000..2bf75428ce --- /dev/null +++ b/cypress/integration/openAsMap.cy.js @@ -0,0 +1,40 @@ +import { USER_DATASTORE_CURRENT_AO_KEY } from '../../src/modules/currentAnalyticalObject.js' +import { MAPS_APP_URL } from '../../src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx' +import { + expectAOTitleToBeValue, + expectVisualizationToBeVisible, +} from '../elements/chart.js' +import { openAOByName } from '../elements/fileMenu/open.js' +import { clickOpenAsMap } from '../elements/visualizationTypeSelector.js' +import { goToStartPage } from '../elements/startScreen.js' + +describe('open as map', () => { + it('opens Maps in a new tab instead of navigating away', () => { + const pivotTableName = 'ANC: ANC 1 Visits Cumulative Numbers' + + /* Stub window.open so Cypress does not actually navigate to the + * Maps app in a new tab */ + const windowOpenStub = cy.stub().as('open') + cy.on('window:before:load', (win) => { + cy.stub(win, 'open').callsFake(windowOpenStub) + }) + + goToStartPage() + openAOByName(pivotTableName) + expectAOTitleToBeValue(pivotTableName) + expectVisualizationToBeVisible('PIVOT_TABLE') + + clickOpenAsMap() + + cy.get('@open').should('have.been.calledOnce') + cy.get('@open').should((stub) => { + const url = stub.getCall(0).args[0] + const target = stub.getCall(0).args[1] + + expect(url).to.satisfy((url) => + url.endsWith(`/${MAPS_APP_URL}/#/${USER_DATASTORE_CURRENT_AO_KEY}`) + ) + expect(target).to.equal('_blank') + }) + }) +}) From 5f02f8bff12e099bca176f2b946493cb10decf61 Mon Sep 17 00:00:00 2001 From: Karoline Tufte Lien Date: Thu, 30 Jul 2026 18:12:34 +0200 Subject: [PATCH 3/5] fix: await datastore write before opening Maps and harden window.open [DHIS2-17366] Matches maps-app's own "Open in" pattern (await set() before window.open) to avoid a race where the new tab could load before the analytical object is persisted, and adds noopener to guard against reverse-tabnabbing. AI Assisted. --- cypress/integration/openAsMap.cy.js | 6 ++++-- .../VisualizationTypeSelector/VisualizationTypeSelector.jsx | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cypress/integration/openAsMap.cy.js b/cypress/integration/openAsMap.cy.js index 2bf75428ce..4afe892783 100644 --- a/cypress/integration/openAsMap.cy.js +++ b/cypress/integration/openAsMap.cy.js @@ -1,12 +1,12 @@ -import { USER_DATASTORE_CURRENT_AO_KEY } from '../../src/modules/currentAnalyticalObject.js' import { MAPS_APP_URL } from '../../src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx' +import { USER_DATASTORE_CURRENT_AO_KEY } from '../../src/modules/currentAnalyticalObject.js' import { expectAOTitleToBeValue, expectVisualizationToBeVisible, } from '../elements/chart.js' import { openAOByName } from '../elements/fileMenu/open.js' -import { clickOpenAsMap } from '../elements/visualizationTypeSelector.js' import { goToStartPage } from '../elements/startScreen.js' +import { clickOpenAsMap } from '../elements/visualizationTypeSelector.js' describe('open as map', () => { it('opens Maps in a new tab instead of navigating away', () => { @@ -30,11 +30,13 @@ describe('open as map', () => { cy.get('@open').should((stub) => { const url = stub.getCall(0).args[0] const target = stub.getCall(0).args[1] + const features = stub.getCall(0).args[2] expect(url).to.satisfy((url) => url.endsWith(`/${MAPS_APP_URL}/#/${USER_DATASTORE_CURRENT_AO_KEY}`) ) expect(target).to.equal('_blank') + expect(features).to.equal('noopener') }) }) }) diff --git a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx index 93a059782e..ffcfe27af5 100644 --- a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx +++ b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx @@ -64,11 +64,12 @@ const UnconnectedVisualizationTypeSelector = ({ ui ) - set(currentAnalyticalObject) + await set(currentAnalyticalObject) window.open( `${baseUrl}/${MAPS_APP_URL}/#/${USER_DATASTORE_CURRENT_AO_KEY}`, - '_blank' + '_blank', + 'noopener' ) } From ff9d8ca0bc2984b062e63e28d382386574ae8b6b Mon Sep 17 00:00:00 2001 From: Karoline Tufte Lien Date: Thu, 30 Jul 2026 18:30:08 +0200 Subject: [PATCH 4/5] style: wrap long line in openAsMap.cy.js to satisfy prettier Pre-existing 80-char overflow from the original PR, surfaced by CI lint. --- cypress/integration/openAsMap.cy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cypress/integration/openAsMap.cy.js b/cypress/integration/openAsMap.cy.js index 4afe892783..28134d5e53 100644 --- a/cypress/integration/openAsMap.cy.js +++ b/cypress/integration/openAsMap.cy.js @@ -33,7 +33,9 @@ describe('open as map', () => { const features = stub.getCall(0).args[2] expect(url).to.satisfy((url) => - url.endsWith(`/${MAPS_APP_URL}/#/${USER_DATASTORE_CURRENT_AO_KEY}`) + url.endsWith( + `/${MAPS_APP_URL}/#/${USER_DATASTORE_CURRENT_AO_KEY}` + ) ) expect(target).to.equal('_blank') expect(features).to.equal('noopener') From 8511efcbb4f007fadc0387f72c103808f9963e5d Mon Sep 17 00:00:00 2001 From: Karoline Tufte Lien Date: Thu, 30 Jul 2026 22:23:06 +0200 Subject: [PATCH 5/5] fix(e2e): inline constants in openAsMap spec so it bundles [DHIS2-17366] The spec imported MAPS_APP_URL from VisualizationTypeSelector.jsx, which pulled the React component and its CSS module into the Cypress browserify bundle. browserify cannot parse the CSS, so the spec failed to compile and reported "0 tests, 1 failing" on every DHIS2 instance version. No other spec imports from src/, so duplicate the two string constants instead. Co-Authored-By: Claude Opus 5 (1M context) --- cypress/integration/openAsMap.cy.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cypress/integration/openAsMap.cy.js b/cypress/integration/openAsMap.cy.js index 28134d5e53..a3173ad88f 100644 --- a/cypress/integration/openAsMap.cy.js +++ b/cypress/integration/openAsMap.cy.js @@ -1,5 +1,3 @@ -import { MAPS_APP_URL } from '../../src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx' -import { USER_DATASTORE_CURRENT_AO_KEY } from '../../src/modules/currentAnalyticalObject.js' import { expectAOTitleToBeValue, expectVisualizationToBeVisible, @@ -8,6 +6,12 @@ import { openAOByName } from '../elements/fileMenu/open.js' import { goToStartPage } from '../elements/startScreen.js' import { clickOpenAsMap } from '../elements/visualizationTypeSelector.js' +/* Duplicated from src instead of imported: importing app source pulls the + * React component and its CSS module into the Cypress browserify bundle, + * which cannot parse them */ +const MAPS_APP_URL = 'dhis-web-maps' +const USER_DATASTORE_CURRENT_AO_KEY = 'currentAnalyticalObject' + describe('open as map', () => { it('opens Maps in a new tab instead of navigating away', () => { const pivotTableName = 'ANC: ANC 1 Visits Cumulative Numbers'