no_overflow вместо пустого complete, узкий catch по строкам, таймаут свода 2, подписка на мутации до ввода фильтра и проверка эффекта фильтра, traced=1. Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
3.2 KiB
TypeScript
70 lines
3.2 KiB
TypeScript
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 };
|
||
}
|