import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { installMenuRepositionFix } from '../../resources/js/utils/menuRepositionFix'; // Ручной requestAnimationFrame: кадры прогоняем детерминированно. let rafQueue: FrameRequestCallback[] = []; function flushFrames(n: number): void { for (let i = 0; i < n; i++) { const batch = rafQueue; rafQueue = []; batch.forEach((cb) => cb(0)); } } // Дать MutationObserver (микротаска jsdom) сработать. const tick = (): Promise => new Promise((r) => setTimeout(r, 0)); function makeMenu(): HTMLElement { const menu = document.createElement('div'); menu.className = 'v-overlay v-menu'; const content = document.createElement('div'); content.className = 'v-overlay__content'; menu.appendChild(content); return menu; } let teardown: (() => void) | undefined; let rectWidth = 200; let rectLeft = 100; let origRect: typeof HTMLElement.prototype.getBoundingClientRect; let resizeSpy: ReturnType; beforeEach(() => { rafQueue = []; rectWidth = 200; rectLeft = 100; vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { rafQueue.push(cb); return rafQueue.length; }); origRect = HTMLElement.prototype.getBoundingClientRect; HTMLElement.prototype.getBoundingClientRect = function (): DOMRect { return { width: rectWidth, height: 10, left: rectLeft, top: 0, right: rectLeft + rectWidth, bottom: 10, x: rectLeft, y: 0, toJSON: () => ({}), } as DOMRect; }; resizeSpy = vi.fn(); window.addEventListener('resize', resizeSpy as unknown as EventListener); }); afterEach(() => { teardown?.(); teardown = undefined; HTMLElement.prototype.getBoundingClientRect = origRect; window.removeEventListener('resize', resizeSpy as unknown as EventListener); vi.unstubAllGlobals(); document.body.innerHTML = ''; }); describe('installMenuRepositionFix', () => { it('при появлении меню стабилизирует позицию и шлёт один resize', async () => { teardown = installMenuRepositionFix(); document.body.appendChild(makeMenu()); await tick(); flushFrames(6); expect(resizeSpy).toHaveBeenCalledTimes(1); }); it('на посторонний узел не реагирует', async () => { teardown = installMenuRepositionFix(); const other = document.createElement('div'); other.className = 'some-card'; document.body.appendChild(other); await tick(); flushFrames(6); expect(resizeSpy).not.toHaveBeenCalled(); }); it('идемпотентна: двойная установка не даёт двойной resize', async () => { teardown = installMenuRepositionFix(); installMenuRepositionFix(); // второй вызов — noop document.body.appendChild(makeMenu()); await tick(); flushFrames(6); expect(resizeSpy).toHaveBeenCalledTimes(1); }); it('предохранитель: если геометрия не устаканилась — не виснет и не шлёт resize', async () => { rectWidth = 0; // контент «нулевой» → условие стабилизации не выполняется teardown = installMenuRepositionFix(); document.body.appendChild(makeMenu()); await tick(); flushFrames(120); // больше предохранителя (90 кадров) expect(resizeSpy).not.toHaveBeenCalled(); expect(rafQueue.length).toBe(0); // цикл остановился, не зациклился }); });