import test from "node:test"; import assert from "node:assert/strict"; import { JSDOM } from "jsdom"; import positionFloatingPanel from "../js/ui/utils/positionFloatingPanel.js"; import { createFloatingPanelStyles } from "../js/ui/utils/floatingPanelStyles.js"; test("flips to top when bottom placement collides with viewport", () => { const dom = new JSDOM( `
`, { pretendToBeVisual: true }, ); const { window } = dom; const { document } = window; window.innerWidth = 360; window.innerHeight = 320; const trigger = document.getElementById("trigger"); const panel = document.getElementById("panel"); trigger.getBoundingClientRect = () => ({ top: 260, bottom: 300, left: 100, right: 140, width: 40, height: 40, }); panel.getBoundingClientRect = () => ({ top: 0, bottom: 120, left: 0, right: 200, width: 200, height: 120, }); Object.defineProperties(panel, { offsetWidth: { value: 200 }, offsetHeight: { value: 120 }, }); const styles = createFloatingPanelStyles(panel); const positioner = positionFloatingPanel(trigger, panel, { offset: 8, viewportPadding: 12, styles, }); panel.hidden = false; panel.dataset.state = "open"; positioner.update(); assert.equal(panel.dataset.floatingPlacement, "top"); assert.equal(styles.getStrategy(), "fixed"); assert.deepEqual(styles.getFallbackPosition(), { top: 132, left: 100 }); assert.equal(panel.dataset.floatingMode, "fallback"); positioner.destroy(); assert.equal(panel.dataset.floatingPanel, undefined); assert.equal(trigger.dataset.floatingAnchor, undefined); }); test("respects RTL alignment and clamps within the viewport", () => { const dom = new JSDOM( `
`, { pretendToBeVisual: true, url: "https://example.com" }, ); const { window } = dom; const { document } = window; window.innerWidth = 320; window.innerHeight = 240; document.documentElement.dir = "rtl"; document.body.dir = "rtl"; const trigger = document.getElementById("trigger"); const panel = document.getElementById("panel"); trigger.getBoundingClientRect = () => ({ top: 32, bottom: 72, left: 220, right: 260, width: 40, height: 40, }); panel.getBoundingClientRect = () => ({ top: 0, bottom: 80, left: 0, right: 120, width: 120, height: 80, }); Object.defineProperties(panel, { offsetWidth: { value: 120 }, offsetHeight: { value: 80 }, }); const styles = createFloatingPanelStyles(panel); const positioner = positionFloatingPanel(trigger, panel, { alignment: "start", viewportPadding: 16, styles, }); panel.hidden = false; panel.dataset.state = "open"; positioner.update(); // In RTL mode, start alignment hugs the right edge of the trigger. assert.equal(panel.dataset.floatingPlacement, "bottom"); assert.equal(panel.dataset.floatingDir, "rtl"); assert.deepEqual(styles.getFallbackPosition(), { top: 72 + 8, left: 140 }); positioner.destroy(); }); test("updates when scroll containers move the trigger", async () => { const dom = new JSDOM( `
`, { pretendToBeVisual: true }, ); const { window } = dom; const { document } = window; window.innerWidth = 480; window.innerHeight = 360; const trigger = document.getElementById("trigger"); const panel = document.getElementById("panel"); let anchorTop = 100; trigger.getBoundingClientRect = () => ({ top: anchorTop, bottom: anchorTop + 40, left: 40, right: 80, width: 40, height: 40, }); panel.getBoundingClientRect = () => ({ top: 0, bottom: 60, left: 0, right: 180, width: 180, height: 60, }); Object.defineProperties(panel, { offsetWidth: { value: 180 }, offsetHeight: { value: 60 }, }); let scrollHandler = null; const originalAddEventListener = window.addEventListener; window.addEventListener = (type, listener, options) => { if (type === "scroll" && options === true) { scrollHandler = listener; } return originalAddEventListener.call(window, type, listener, options); }; const styles = createFloatingPanelStyles(panel); const positioner = positionFloatingPanel(trigger, panel, { offset: 12, styles, }); panel.hidden = false; panel.dataset.state = "open"; positioner.update(); assert.deepEqual(styles.getFallbackPosition(), { top: 152, left: 40 }); assert.ok(scrollHandler, "scroll listener should be registered"); anchorTop = 60; scrollHandler?.({ type: "scroll" }); assert.deepEqual(styles.getFallbackPosition(), { top: 112, left: 40 }); positioner.destroy(); window.addEventListener = originalAddEventListener; }); test("prefers anchor positioning when supported", () => { const dom = new JSDOM( `
`, { pretendToBeVisual: true }, ); const { window } = dom; const { document } = window; window.innerWidth = 640; window.innerHeight = 480; const trigger = document.getElementById("trigger"); const panel = document.getElementById("panel"); trigger.getBoundingClientRect = () => ({ top: 100, bottom: 140, left: 200, right: 240, width: 40, height: 40, }); panel.getBoundingClientRect = () => ({ top: 0, bottom: 90, left: 0, right: 200, width: 200, height: 90, }); Object.defineProperties(panel, { offsetWidth: { value: 200 }, offsetHeight: { value: 90 }, }); const originalCSS = window.CSS; window.CSS = { supports: (value) => value === "anchor-name: --floating-panel" || value === "position-anchor: --floating-panel", }; const positioner = positionFloatingPanel(trigger, panel, { offset: 10, }); panel.hidden = false; panel.dataset.state = "open"; positioner.update(); assert.equal(panel.dataset.floatingMode, "anchor"); assert.equal(positioner.styles.getMode(), "anchor"); assert.equal(panel.dataset.floatingPlacement, "bottom"); assert.deepEqual(positioner.styles.getFallbackPosition(), { top: 150, left: 200 }); positioner.destroy(); window.CSS = originalCSS; });