/** * Menu position calculation tests */ import { describe, it, expect, beforeEach } from "vitest"; import { calculateMenuPosition, type MenuPosition } from "./menuPosition"; describe("menu position calculation", () => { let mockElement: HTMLElement; function createMockElement(rect: any = {}) { const element = document.createElement("button"); const defaultRect = { top: 100, bottom: 140, left: 50, right: 150, width: 100, height: 40, ...rect, }; element.getBoundingClientRect = () => defaultRect as any; return element; } beforeEach(() => { mockElement = createMockElement(); // Mock window dimensions Object.defineProperty(window, "innerHeight", { value: 800, writable: true, }); Object.defineProperty(window, "innerWidth", { value: 1024, writable: true, }); }); describe("basic positioning", () => { it("should return a MenuPosition object", () => { const position = calculateMenuPosition(mockElement); expect(position).toHaveProperty("x"); expect(position).toHaveProperty("y"); expect(position).toHaveProperty("placement"); expect(typeof position.x).toBe("number"); expect(typeof position.y).toBe("number"); expect(["bottom", "top"]).toContain(position.placement); }); it("should use default menu dimensions", () => { const position1 = calculateMenuPosition(mockElement); const position2 = calculateMenuPosition(mockElement, 160, 120); // With default dimensions (160x120), should place below expect(position1.placement).toBe("bottom"); }); it("should accept custom menu dimensions", () => { const position = calculateMenuPosition(mockElement, 200, 150); expect(typeof position.x).toBe("number"); expect(typeof position.y).toBe("number"); }); }); describe("vertical placement", () => { it("should place menu below trigger when there is space", () => { // Element at top 100, bottom 140, with 800px viewport // Space below = 800 - 140 = 660px (plenty for 120px menu) const position = calculateMenuPosition(mockElement, 160, 120); expect(position.placement).toBe("bottom"); expect(position.y).toBe(144); // 140 (bottom) + 4 (gap) }); it("should place menu above trigger when no space below", () => { // Element at bottom 750, only 50px below (not enough for 120px menu) mockElement = createMockElement({ top: 700, bottom: 750, left: 50, right: 150, width: 100, height: 50, }); const position = calculateMenuPosition(mockElement, 160, 120); expect(position.placement).toBe("top"); expect(position.y).toBe(576); // 700 - 120 - 4 }); it("should fallback to below when space is insufficient both ways", () => { // Element in middle with very small viewport Object.defineProperty(window, "innerHeight", { value: 180, writable: true, }); mockElement = createMockElement({ top: 80, bottom: 100, left: 50, right: 150, width: 100, height: 20, }); const position = calculateMenuPosition(mockElement, 160, 120); // Should prefer bottom even if doesn't fit expect(position.placement).toBe("bottom"); }); }); describe("horizontal positioning", () => { it("should align menu right edge with button right edge", () => { const position = calculateMenuPosition(mockElement, 160, 120); // Button right = 150, menu width = 160 // x should be 150 - 160 = -10, but clamped to 8 expect(position.x).toBe(8); }); it("should prevent overflow on right edge", () => { // Element far to the right mockElement = createMockElement({ top: 100, bottom: 140, left: 950, right: 1050, // Beyond 1024px viewport width: 100, height: 40, }); const position = calculateMenuPosition(mockElement, 160, 120); // Should clamp to viewport width - menu width - margin // 1024 - 160 - 8 = 856 expect(position.x).toBe(856); }); it("should prevent overflow on left edge", () => { // Element far to the left mockElement = createMockElement({ top: 100, bottom: 140, left: 10, right: 60, width: 50, height: 40, }); const position = calculateMenuPosition(mockElement, 160, 120); // Should respect left margin of 8px expect(position.x).toBeGreaterThanOrEqual(8); }); it("should center menu horizontally when possible", () => { // Element in middle of viewport mockElement = createMockElement({ top: 100, bottom: 140, left: 432, right: 592, // Centered at 512px width: 160, height: 40, }); const position = calculateMenuPosition(mockElement, 160, 120); // Menu aligns right with button right (592), so x = 592 - 160 = 432 expect(position.x).toBe(432); }); }); describe("edge cases", () => { it("should handle very small viewport", () => { Object.defineProperty(window, "innerHeight", { value: 100, writable: true, }); Object.defineProperty(window, "innerWidth", { value: 150, writable: true, }); const position = calculateMenuPosition(mockElement, 160, 120); // With a very small viewport, the position calculation may return negative x // This is acceptable as the menu can overflow the viewport in edge cases expect(typeof position.x).toBe("number"); expect(typeof position.y).toBe("number"); expect(["bottom", "top"]).toContain(position.placement); }); it("should handle element at viewport edges", () => { // Element at top-left corner mockElement = createMockElement({ top: 0, bottom: 40, left: 0, right: 40, width: 40, height: 40, }); const position = calculateMenuPosition(mockElement, 160, 120); // Should position below expect(position.placement).toBe("bottom"); // X should be clamped to left margin expect(position.x).toBe(8); }); it("should handle large menu dimensions", () => { const position = calculateMenuPosition(mockElement, 500, 400); expect(typeof position.x).toBe("number"); expect(typeof position.y).toBe("number"); expect(position.x).toBeGreaterThanOrEqual(0); }); }); describe("consistency", () => { it("should return consistent results for same input", () => { const position1 = calculateMenuPosition(mockElement, 160, 120); const position2 = calculateMenuPosition(mockElement, 160, 120); expect(position1.x).toBe(position2.x); expect(position1.y).toBe(position2.y); expect(position1.placement).toBe(position2.placement); }); it("should respect gaps and margins", () => { const position = calculateMenuPosition(mockElement, 160, 120); // If placed below, y should be at least bottom + 4 if (position.placement === "bottom") { expect(position.y).toBeGreaterThanOrEqual(140 + 4); } // X should always respect 8px margins expect(position.x).toBeGreaterThanOrEqual(8); }); }); });