Andrey Epifancev a1251436c3 perf: исправления сценариев скролла (ревью, раунд 1)
no_overflow вместо пустого complete, узкий catch по строкам, таймаут свода 2,
подписка на мутации до ввода фильтра и проверка эффекта фильтра, traced=1.

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:03:28 +04:00

70 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Page } from '@playwright/test';
export type Axis = 'y' | 'x';
const raf = () => new Promise<void>((r) => requestAnimationFrame(() => r()));
async function pos(page: Page, sel: string, axis: Axis) {
return page.evaluate(
([s, a]) => {
const el = document.querySelector(s as string) as HTMLElement | null;
if (!el) return null;
return a === 'y'
? { p: el.scrollTop, size: el.clientHeight, total: el.scrollHeight }
: { p: el.scrollLeft, size: el.clientWidth, total: el.scrollWidth };
},
[sel, axis] as const,
);
}
/**
* Прокрутка колесом мыши шагами stepPx: вниз (вправо) до конца и обратно.
* После каждого шага ждём requestAnimationFrame.
* complete: в конце прямого прохода scrollTop + clientHeight >= scrollHeight - 2
* (для x - scrollLeft/clientWidth/scrollWidth). maxSteps ограничивает прямой проход
* (при его исчерпании обратный проход не выполняется, позиция сбрасывается без замера).
* Если по оси нет переполнения (scrollHeight <= clientHeight), возвращается complete=false, steps=0, reason='no_overflow'.
* steps - число шагов колеса (вниз + обратно).
*/
export async function scrollThrough(
page: Page, containerSelector: string, axis: Axis, stepPx = 200, maxSteps = 5000,
): Promise<{ complete: boolean; steps: number; reason?: 'no_overflow' }> {
const box = await page.locator(containerSelector).first().boundingBox();
if (!box) throw new Error(`scroll container not found: ${containerSelector}`);
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
const wheel = (d: number) => (axis === 'y' ? page.mouse.wheel(0, d) : page.mouse.wheel(d, 0));
const tick = () => page.evaluate(raf);
const first = await pos(page, containerSelector, axis);
// нет переполнения по оси - скроллить нечего, замер пустой: не засчитываем как complete
if (first && first.total <= first.size) return { complete: false, steps: 0, reason: 'no_overflow' };
let steps = 0;
let complete = false;
while (steps < maxSteps) {
const s = await pos(page, containerSelector, axis);
if (!s) break;
if (s.p + s.size >= s.total - 2) { complete = true; break; }
await wheel(stepPx); await tick(); steps++;
}
// финальная проверка (последний шаг мог дойти до конца)
if (!complete) {
const s = await pos(page, containerSelector, axis);
complete = !!s && s.p + s.size >= s.total - 2;
}
if (!complete) {
await page.evaluate(([s, a]) => {
const el = document.querySelector(s as string) as HTMLElement;
if (a === 'y') el.scrollTop = 0; else el.scrollLeft = 0;
}, [containerSelector, axis] as const);
return { complete, steps };
}
// обратно
while (steps < 2 * maxSteps) {
const s = await pos(page, containerSelector, axis);
if (!s || s.p <= 2) break;
await wheel(-stepPx); await tick(); steps++;
}
return { complete, steps };
}