diff --git a/cypress/e2e/servers_compare.cy.ts b/cypress/e2e/servers_compare.cy.ts index 91c8f1da..9e66139a 100644 --- a/cypress/e2e/servers_compare.cy.ts +++ b/cypress/e2e/servers_compare.cy.ts @@ -2,13 +2,30 @@ import { E2EEvent } from "../support/generics"; const BENCHMARKS_COUNT_WITHOUT_STRESSNG = 9; const BENCHMARKS_COUNT = 11; +const COMPARE_PRICE_URL = + "/compare?instances=W3siZGlzcGxheV9uYW1lIjoidDJhLXN0YW5kYXJkLTEiLCJ2ZW5kb3IiOiJnY3AiLCJzZXJ2ZXIiOiJ0MmEtc3RhbmRhcmQtMSIsInpvbmVzUmVnaW9ucyI6W119LHsiZGlzcGxheV9uYW1lIjoiYzdnLm1lZGl1bSIsInZlbmRvciI6ImF3cyIsInNlcnZlciI6ImM3Zy5tZWRpdW0iLCJ6b25lc1JlZ2lvbnMiOltdfV0%3D"; +const COMPARE_36_VCPU_URL = + "/compare?instances=W3sidmVuZG9yIjoiYXdzIiwic2VydmVyIjoiYzVuLjl4bGFyZ2UifSx7InZlbmRvciI6ImF3cyIsInNlcnZlciI6ImQyLjh4bGFyZ2UifV0%3D"; + +function showCompareTooltip() { + cy.get('#main-table tr.rows-to-hide-for-test lucide-icon[name="info"]') + .first() + .then(($icon) => { + cy.window().then((win) => { + $icon[0].dispatchEvent( + new win.MouseEvent("mouseenter", { + bubbles: false, + cancelable: true, + view: win, + }), + ); + }); + }); +} describe("Server Compare", () => { it("Server with price 1 vCPU", () => { - E2EEvent.visitURL( - "/compare?instances=W3siZGlzcGxheV9uYW1lIjoidDJhLXN0YW5kYXJkLTEiLCJ2ZW5kb3IiOiJnY3AiLCJzZXJ2ZXIiOiJ0MmEtc3RhbmRhcmQtMSIsInpvbmVzUmVnaW9ucyI6W119LHsiZGlzcGxheV9uYW1lIjoiYzdnLm1lZGl1bSIsInZlbmRvciI6ImF3cyIsInNlcnZlciI6ImM3Zy5tZWRpdW0iLCJ6b25lc1JlZ2lvbnMiOltdfV0%3D", - 4000, - ); + E2EEvent.visitURL(COMPARE_PRICE_URL, 4000); E2EEvent.checkBreadcrumbs(); @@ -24,10 +41,7 @@ describe("Server Compare", () => { }); it("Server with price 36 vCPU", () => { - E2EEvent.visitURL( - "/compare?instances=W3sidmVuZG9yIjoiYXdzIiwic2VydmVyIjoiYzVuLjl4bGFyZ2UifSx7InZlbmRvciI6ImF3cyIsInNlcnZlciI6ImQyLjh4bGFyZ2UifV0%3D", - 4000, - ); + E2EEvent.visitURL(COMPARE_36_VCPU_URL, 4000); E2EEvent.checkBreadcrumbs(); @@ -41,4 +55,40 @@ describe("Server Compare", () => { BENCHMARKS_COUNT, ); }); + + it("dismisses the compare tooltip when the table scrolls on smaller screens", () => { + cy.viewport(800, 900); + E2EEvent.visitURL(COMPARE_PRICE_URL, 4000); + + showCompareTooltip(); + + cy.get("#tooltipcompareDefault") + .should("have.css", "display", "block") + .and("have.css", "opacity", "1") + .and("contain.text", "Performance benchmark score"); + + cy.get("#table_holder").then(($tableHolder) => { + $tableHolder[0].dispatchEvent(new Event("scroll")); + }); + + cy.get("#tooltipcompareDefault") + .should("have.css", "display", "none") + .and("have.css", "opacity", "0"); + }); + + it("dismisses the compare tooltip when the page scrolls", () => { + E2EEvent.visitURL(COMPARE_PRICE_URL, 4000); + + showCompareTooltip(); + + cy.get("#tooltipcompareDefault") + .should("have.css", "display", "block") + .and("have.css", "opacity", "1"); + + cy.scrollTo(0, 500); + + cy.get("#tooltipcompareDefault") + .should("have.css", "display", "none") + .and("have.css", "opacity", "0"); + }); }); diff --git a/src/app/services/ui-tooltip.service.spec.ts b/src/app/services/ui-tooltip.service.spec.ts index e0a07c82..ba3ad975 100644 --- a/src/app/services/ui-tooltip.service.spec.ts +++ b/src/app/services/ui-tooltip.service.spec.ts @@ -4,6 +4,17 @@ import { UiTooltipService } from "./ui-tooltip.service"; describe("UiTooltipService", () => { let service: UiTooltipService; + function createTooltipTarget(): HTMLButtonElement { + const target = document.createElement("button"); + spyOn(target, "getBoundingClientRect").and.returnValue({ + left: 80, + right: 100, + top: 50, + bottom: 70, + } as DOMRect); + return target; + } + beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(UiTooltipService); @@ -18,13 +29,7 @@ describe("UiTooltipService", () => { it("shows a tooltip using viewport-aware placement", () => { const tooltip = document.createElement("div"); - const target = document.createElement("button"); - spyOn(target, "getBoundingClientRect").and.returnValue({ - left: 80, - right: 100, - top: 50, - bottom: 70, - } as DOMRect); + const target = createTooltipTarget(); service.show(tooltip, { currentTarget: target, @@ -56,14 +61,7 @@ describe("UiTooltipService", () => { it("does not cancel a pending frame when hiding a different tooltip", () => { const activeTooltip = document.createElement("div"); const otherTooltip = document.createElement("div"); - const target = document.createElement("button"); - - spyOn(target, "getBoundingClientRect").and.returnValue({ - left: 80, - right: 100, - top: 50, - bottom: 70, - } as DOMRect); + const target = createTooltipTarget(); (window.requestAnimationFrame as jasmine.Spy).and.returnValue(7); (window.cancelAnimationFrame as jasmine.Spy).calls.reset(); @@ -77,4 +75,84 @@ describe("UiTooltipService", () => { expect(window.cancelAnimationFrame).not.toHaveBeenCalled(); }); + + it("hides the active tooltip when the window scrolls", () => { + const tooltip = document.createElement("div"); + const target = createTooltipTarget(); + + service.show(tooltip, { + currentTarget: target, + target, + } as unknown as MouseEvent); + + window.dispatchEvent(new Event("scroll")); + + expect(tooltip.style.display).toBe("none"); + expect(tooltip.style.opacity).toBe("0"); + }); + + it("hides the active tooltip when a scrollable ancestor scrolls", () => { + const tooltip = document.createElement("div"); + const scrollContainer = document.createElement("div"); + const target = createTooltipTarget(); + + scrollContainer.appendChild(target); + document.body.appendChild(scrollContainer); + + service.show(tooltip, { + currentTarget: target, + target, + } as unknown as MouseEvent); + + scrollContainer.dispatchEvent(new Event("scroll")); + + expect(tooltip.style.display).toBe("none"); + + scrollContainer.remove(); + }); + + it("removes the scroll listener when the tooltip is hidden", () => { + const tooltip = document.createElement("div"); + const target = createTooltipTarget(); + + service.show(tooltip, { + currentTarget: target, + target, + } as unknown as MouseEvent); + + service.hide(tooltip); + tooltip.style.display = "block"; + tooltip.style.opacity = "1"; + + window.dispatchEvent(new Event("scroll")); + + expect(tooltip.style.display).toBe("block"); + expect(tooltip.style.opacity).toBe("1"); + }); + + it("cleans up scroll dismissal after replacing and hiding the active tooltip", () => { + const firstTooltip = document.createElement("div"); + const secondTooltip = document.createElement("div"); + const firstTarget = createTooltipTarget(); + const secondTarget = createTooltipTarget(); + + service.show(firstTooltip, { + currentTarget: firstTarget, + target: firstTarget, + } as unknown as MouseEvent); + + service.show(secondTooltip, { + currentTarget: secondTarget, + target: secondTarget, + } as unknown as MouseEvent); + + service.hide(secondTooltip); + secondTooltip.style.display = "block"; + secondTooltip.style.opacity = "1"; + + window.dispatchEvent(new Event("scroll")); + + expect(secondTooltip.style.display).toBe("block"); + expect(secondTooltip.style.opacity).toBe("1"); + }); }); diff --git a/src/app/services/ui-tooltip.service.ts b/src/app/services/ui-tooltip.service.ts index f481e4d6..e4c0ea3f 100644 --- a/src/app/services/ui-tooltip.service.ts +++ b/src/app/services/ui-tooltip.service.ts @@ -11,6 +11,9 @@ export type TooltipPlacement = { export class UiTooltipService { private readonly viewportPadding = 16; private readonly tooltipOffset = 5; + private readonly dismissListenerOptions: AddEventListenerOptions = { + capture: true, + }; private readonly defaultPlacement: TooltipPlacement = { left: "anchor-right", top: "anchor-below", @@ -18,6 +21,13 @@ export class UiTooltipService { private activeTooltipElement?: HTMLElement; private activeAnchorElement?: Element; private animationFrameId?: number; + private readonly hideActiveTooltip = () => { + if (!this.activeTooltipElement) { + return; + } + + this.hide(this.activeTooltipElement); + }; show( tooltipElement: HTMLElement, @@ -30,14 +40,12 @@ export class UiTooltipService { } this.cancelPendingFrame(); + this.unregisterDismissListeners(); this.activeTooltipElement = tooltipElement; this.activeAnchorElement = anchorElement; + this.registerDismissListeners(); - tooltipElement.style.display = "block"; - tooltipElement.style.visibility = "hidden"; - tooltipElement.style.opacity = "0"; - tooltipElement.style.left = "0px"; - tooltipElement.style.top = "0px"; + this.prepareTooltipForPositioning(tooltipElement); this.animationFrameId = window.requestAnimationFrame(() => { this.animationFrameId = undefined; @@ -80,15 +88,12 @@ export class UiTooltipService { if (this.activeTooltipElement === tooltipElement) { this.cancelPendingFrame(); + this.unregisterDismissListeners(); this.activeTooltipElement = undefined; this.activeAnchorElement = undefined; } - tooltipElement.style.display = "none"; - tooltipElement.style.visibility = "hidden"; - tooltipElement.style.opacity = "0"; - tooltipElement.style.left = "0px"; - tooltipElement.style.top = "0px"; + this.resetTooltipStyles(tooltipElement); } private positionTooltip( @@ -192,4 +197,46 @@ export class UiTooltipService { this.animationFrameId = undefined; } } + + private prepareTooltipForPositioning(tooltipElement: HTMLElement): void { + tooltipElement.style.display = "block"; + tooltipElement.style.visibility = "hidden"; + tooltipElement.style.opacity = "0"; + tooltipElement.style.left = "0px"; + tooltipElement.style.top = "0px"; + } + + private resetTooltipStyles(tooltipElement: HTMLElement): void { + tooltipElement.style.display = "none"; + tooltipElement.style.visibility = "hidden"; + tooltipElement.style.opacity = "0"; + tooltipElement.style.left = "0px"; + tooltipElement.style.top = "0px"; + } + + private registerDismissListeners(): void { + document.addEventListener( + "scroll", + this.hideActiveTooltip, + this.dismissListenerOptions, + ); + window.addEventListener( + "scroll", + this.hideActiveTooltip, + this.dismissListenerOptions, + ); + } + + private unregisterDismissListeners(): void { + document.removeEventListener( + "scroll", + this.hideActiveTooltip, + this.dismissListenerOptions, + ); + window.removeEventListener( + "scroll", + this.hideActiveTooltip, + this.dismissListenerOptions, + ); + } }