c78b69fcaf
Также: привести resizeSpy в тесте к EventListener (тип-чистота vue-tsc). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
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<void> => 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<typeof vi.fn>;
|
|
|
|
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); // цикл остановился, не зациклился
|
|
});
|
|
});
|