Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions cypress/e2e/servers_compare.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();

Expand All @@ -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");
});
});
108 changes: 93 additions & 15 deletions src/app/services/ui-tooltip.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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");
});
});
67 changes: 57 additions & 10 deletions src/app/services/ui-tooltip.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@ 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",
};
private activeTooltipElement?: HTMLElement;
private activeAnchorElement?: Element;
private animationFrameId?: number;
private readonly hideActiveTooltip = () => {
if (!this.activeTooltipElement) {
return;
}

this.hide(this.activeTooltipElement);
};

show(
tooltipElement: HTMLElement,
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
);
}
}