From 31d0d48da857991a1151dfc6f32af0e480aa6e26 Mon Sep 17 00:00:00 2001 From: "Guppy L. Stott" Date: Wed, 17 Jun 2026 10:40:20 -0400 Subject: [PATCH] Fix global setting positioning bug and add cypress tests for behavior --- .../global-settings-dialog-positioning.cy.ts | 99 +++++++++++ .../globalSettingsDialogRequest.ts | 51 ++++++ .../microbe-trace-next-plugin.component.html | 3 + .../microbe-trace-next-plugin.component.ts | 166 +++++++++++++++++- .../BubbleComponent/bubble.component.html | 2 +- .../BubbleComponent/bubble.component.ts | 7 +- .../MapComponent/map-plugin.component.html | 4 +- .../MapComponent/map-plugin.component.ts | 7 +- .../phylogenetic-plugin.component.html | 2 +- .../phylogenetic-plugin.component.ts | 7 +- .../TwoDComponent/twoD-plugin.component.html | 6 +- .../TwoDComponent/twoD-plugin.component.ts | 7 +- 12 files changed, 340 insertions(+), 21 deletions(-) create mode 100644 cypress/e2e/journeys/flows/global-settings-dialog-positioning.cy.ts create mode 100644 src/app/helperClasses/globalSettingsDialogRequest.ts diff --git a/cypress/e2e/journeys/flows/global-settings-dialog-positioning.cy.ts b/cypress/e2e/journeys/flows/global-settings-dialog-positioning.cy.ts new file mode 100644 index 00000000..15601aa7 --- /dev/null +++ b/cypress/e2e/journeys/flows/global-settings-dialog-positioning.cy.ts @@ -0,0 +1,99 @@ +/// + +import { getProfile } from '../datasets/profile'; +import { + expandAccordionTabByHeader, + launchProfileToTwoD, + openTwoDSettingsDialog, +} from '../../../support/journey-helpers'; + +function getOverlapArea(first: DOMRect, second: DOMRect): number { + const overlapWidth = Math.max(0, Math.min(first.right, second.right) - Math.max(first.left, second.left)); + const overlapHeight = Math.max(0, Math.min(first.bottom, second.bottom) - Math.max(first.top, second.top)); + + return overlapWidth * overlapHeight; +} + +function getEffectiveZIndex(element: HTMLElement): number { + let current: HTMLElement | null = element; + + while (current) { + const zIndex = current.ownerDocument.defaultView?.getComputedStyle(current).zIndex; + const numericZIndex = zIndex && zIndex !== 'auto' ? Number(zIndex) : NaN; + + if (Number.isFinite(numericZIndex)) { + return numericZIndex; + } + + current = current.parentElement; + } + + return 0; +} + +function expectRectInsideViewport(rect: DOMRect, win: Window, label: string): void { + expect(rect.left, `${label} left`).to.be.at.least(0); + expect(rect.top, `${label} top`).to.be.at.least(0); + expect(rect.right, `${label} right`).to.be.at.most(win.innerWidth); + expect(rect.bottom, `${label} bottom`).to.be.at.most(win.innerHeight); +} + +describe('Journey Flow - Global Settings dialog positioning', () => { + const profile = getProfile('color-by-uploaded-categorical'); + + it('opens linked Global Settings above and beside the 2D Network Settings dialog', () => { + cy.viewport(1800, 900); + + launchProfileToTwoD(profile); + openTwoDSettingsDialog(); + + cy.get('@twoDSettings').contains('.nav-link', 'Nodes').click({ force: true }); + cy.get('@twoDSettings') + .find('.tab-pane:visible', { timeout: 15000 }) + .should('exist') + .as('nodesTab'); + + expandAccordionTabByHeader('@nodesTab', 'Colors'); + cy.get('@nodesTab').contains('button', 'Show Colors').scrollIntoView().click({ force: true }); + + cy.contains('.p-dialog-title', 'Global Settings', { timeout: 15000 }) + .should('be.visible') + .parents('.p-dialog') + .should('be.visible') + .as('globalSettings'); + + cy.get('@twoDSettings').should('be.visible'); + cy.get('@globalSettings').should('be.visible'); + + cy.get('@twoDSettings').then(($sourceDialog) => { + cy.get('@globalSettings').then(($globalSettingsDialog) => { + const sourceDialog = $sourceDialog[0] as HTMLElement; + const globalSettingsDialog = $globalSettingsDialog[0] as HTMLElement; + const win = globalSettingsDialog.ownerDocument.defaultView; + + expect(win, 'dialog window').to.exist; + + const sourceRect = sourceDialog.getBoundingClientRect(); + const globalRect = globalSettingsDialog.getBoundingClientRect(); + + expectRectInsideViewport(sourceRect, win!, '2D Network Settings'); + expectRectInsideViewport(globalRect, win!, 'Global Settings'); + + expect(getEffectiveZIndex(globalSettingsDialog), 'Global Settings z-index') + .to.be.greaterThan(getEffectiveZIndex(sourceDialog)); + + const centerX = Math.min(Math.max(globalRect.left + globalRect.width / 2, 1), win!.innerWidth - 1); + const centerY = Math.min(Math.max(globalRect.top + globalRect.height / 2, 1), win!.innerHeight - 1); + const topmostElement = globalSettingsDialog.ownerDocument.elementFromPoint(centerX, centerY); + + expect( + topmostElement === globalSettingsDialog || globalSettingsDialog.contains(topmostElement), + 'Global Settings is topmost at its center point', + ).to.equal(true); + + expect(getOverlapArea(sourceRect, globalRect), 'dialog overlap area on a wide viewport') + .to.be.lessThan(1); + }); + }); + }); +}); diff --git a/src/app/helperClasses/globalSettingsDialogRequest.ts b/src/app/helperClasses/globalSettingsDialogRequest.ts new file mode 100644 index 00000000..cbbc3e74 --- /dev/null +++ b/src/app/helperClasses/globalSettingsDialogRequest.ts @@ -0,0 +1,51 @@ +export type DialogRectSnapshot = { + top: number; + left: number; + right: number; + bottom: number; + width: number; + height: number; +}; + +export type GlobalSettingsDialogRequest = string | { + activeTab?: string; + sourceDialogRect?: DialogRectSnapshot; +}; + +export type NormalizedGlobalSettingsDialogRequest = { + activeTab: string; + sourceDialogRect?: DialogRectSnapshot; +}; + +export function createGlobalSettingsDialogRequest( + activeTab: string = 'Styling', + event?: MouseEvent +): Exclude { + return { + activeTab, + sourceDialogRect: getSourceDialogRect(event) + }; +} + +function getSourceDialogRect(event?: MouseEvent): DialogRectSnapshot | undefined { + const eventTarget = event?.currentTarget instanceof HTMLElement + ? event.currentTarget + : event?.target instanceof HTMLElement + ? event.target + : undefined; + + const sourceDialog = eventTarget?.closest('.p-dialog'); + if (!sourceDialog) { + return undefined; + } + + const rect = sourceDialog.getBoundingClientRect(); + return { + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + width: rect.width, + height: rect.height + }; +} diff --git a/src/app/microbe-trace-next-plugin.component.html b/src/app/microbe-trace-next-plugin.component.html index 7a0cec15..0f277ca5 100644 --- a/src/app/microbe-trace-next-plugin.component.html +++ b/src/app/microbe-trace-next-plugin.component.html @@ -314,6 +314,9 @@