From 7d7a98f9a4133bc6c29fcb6351c1e2bfbc8458f3 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:49:25 +0400 Subject: [PATCH] =?UTF-8?q?fix(e2e):=20pinned-resize=20=D1=82=D1=8F=D0=BD?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BF=D0=B5=D1=80=D0=B2=D1=83=D1=8E=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=B4=D0=B2=D1=83=D1=85=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BB=D1=91=D0=BD=D0=BD=D1=8B=D1=85=20data-=D0=BA?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA,=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82=20=D1=8D=D1=84=D1=84=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=20=D0=B8=20=D1=87=D0=B8=D1=81=D0=BB=D0=BE=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D1=80=D0=B5=D0=BF=D0=BB=D1=91=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=8F=D1=87=D0=B5=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- perf/README.md | 2 +- perf/e2e/scenarios/pinned-resize.spec.ts | 59 +++++++++++++++++------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/perf/README.md b/perf/README.md index 102381a..7ddb9fa 100644 --- a/perf/README.md +++ b/perf/README.md @@ -72,7 +72,7 @@ cd .. && .venv/bin/python -m perfkit.aggregate results/s1 ### Перетаскивание ширины закреплённой колонки -`perf/e2e/scenarios/pinned-resize.spec.ts` (AURORA-1839): роль `rf` закрепляет первую колонку данных листа и тянет её границу 50 x (+4 px), затем 50 x (-4 px) с ожиданием кадра на шаге; после отпускания `lib/pinnedGeometry.ts` (`findPinnedGap`) проверяет примыкание `td[data-pinned]`. Запись: `scenario: pinned_resize`, `target: <лист>@pinned`, метрики `main_thread_ms_per_step` (шаг = движение мыши), `frame_p95_ms`, `heavy_frame_n`; при наложении колонок `incomplete` и `skip_reason: pinned_overlap` (`pinned_gap_index`). Для сборки базы без `data-col-resize-id` ресайзер ищется по `cursor: col-resize`. Команда: `PERF_TARGETS='FORM_2/AHR/*' PERF_TRACE=0 PERF_REPEATS=3 npx playwright test scenarios/pinned-resize.spec.ts`. Самопроверка геометрии: `node --test perf/e2e/tests/pinned-geometry.test.ts`. +`perf/e2e/scenarios/pinned-resize.spec.ts` (AURORA-1839): роль `rf` закрепляет две первые колонки данных листа и тянет границу первой из них 50 x (+4 px), затем 50 x (-4 px) с ожиданием кадра на шаге; после отпускания `lib/pinnedGeometry.ts` (`findPinnedGap`) проверяет примыкание `td[data-pinned]` (не менее 4 в строке, иначе `pinned_cells_missing`); если ширина тянущейся колонки на пике хода выросла меньше чем на 100 px - `resize_no_effect`; нет второй data-колонки - `second_column_not_found`. Запись: `scenario: pinned_resize`, `target: <лист>@pinned`, метрики `main_thread_ms_per_step` (шаг = движение мыши), `frame_p95_ms`, `heavy_frame_n`; при наложении колонок `incomplete` и `skip_reason: pinned_overlap` (`pinned_gap_index`). Для сборки базы без `data-col-resize-id` ресайзер ищется по `cursor: col-resize`. Команда: `PERF_TARGETS='FORM_2/AHR/*' PERF_REPEATS=3 npx playwright test scenarios/pinned-resize.spec.ts`. Самопроверка геометрии: `node --test perf/e2e/tests/pinned-geometry.test.ts`. ### Скролл при соавторе diff --git a/perf/e2e/scenarios/pinned-resize.spec.ts b/perf/e2e/scenarios/pinned-resize.spec.ts index 64a00cd..2abcd60 100644 --- a/perf/e2e/scenarios/pinned-resize.spec.ts +++ b/perf/e2e/scenarios/pinned-resize.spec.ts @@ -1,8 +1,8 @@ /** * Перетаскивание ширины закреплённой колонки (AURORA-1839, spec sheet-table). * - * Закрепляет первую колонку данных листа (роль rf), тянет её границу 50 x (+4 px) и 50 x (-4 px) с ожиданием кадра - * после каждого шага, после отпускания проверяет, что td[data-pinned="true"] одной строки примыкают друг к другу. + * Закрепляет две первые колонки данных листа (роль rf), тянет границу первой из них 50 x (+4 px) и 50 x (-4 px) с ожиданием кадра + * после каждого шага, после отпускания проверяет, что не менее 4 td[data-pinned="true"] одной строки примыкают друг к другу. * * Параметры (env): * PERF_TARGETS glob(ы) через запятую по имени листа `FORM/SHEET/direction` (по умолчанию 'FORM_2/AHR/*'); @@ -14,7 +14,8 @@ * PERF_RUN_ID, PERF_SCALE - как у scroll.spec.ts * * Метрика: main_thread_ms_per_step (шаг = движение мыши), frame_p95_ms, heavy_frame_n. - * skip_reason: resizer_not_found, pinned_overlap (+ pinned_gap_index), rows<20. + * skip_reason: column_not_found, second_column_not_found, resizer_not_found, resize_no_effect, pinned_cells_missing, + * pinned_overlap (+ pinned_gap_index), rows<20. */ import { test, type Page } from '@playwright/test'; import { loadManifest, scale } from '../lib/manifest'; @@ -30,6 +31,7 @@ const MIN_ROWS = 20; const LOAD_TIMEOUT = Number(process.env.PERF_LOAD_TIMEOUT_MS ?? 900_000); const STEP_PX = 4; const HALF_STEPS = 50; +const MIN_PINNED_CELLS = 4; const envList = (n: string, d: string[]) => (process.env[n] ? process.env[n]!.split(',').map((s) => s.trim()).filter(Boolean) : d); const CPU_RATES = envList('PERF_CPU_RATES', ['1']).map(Number); @@ -41,9 +43,10 @@ const wanted = (name: string) => TARGET_RES.some((r) => r.test(name) || r.test(` const sheetName = (s: { form_type: string; sheet: string; direction: string | null }) => `${s.form_type}/${s.sheet}/${s.direction ?? 'null'}`; const nextFrame = (page: Page) => page.evaluate(() => new Promise((r) => requestAnimationFrame(() => r()))); +type Sheet = ReturnType['sheets'][number]; const skipLog: string[] = []; -async function measure(browser: import('@playwright/test').Browser, s: ReturnType['sheets'][number], name: string, cpu: number, repeat: number) { +async function measure(browser: import('@playwright/test').Browser, s: Sheet, name: string, cpu: number, repeat: number) { const m = loadManifest(); const user = m.users['rf']; const target = `${name}@pinned`; @@ -84,16 +87,21 @@ async function measure(browser: import('@playwright/test').Browser, s: ReturnTyp await page.waitForTimeout(500); await throttle(page, cpu); - // колонка: первая data.* в DOM-порядке, иначе первая кроме служебных - const colId = await page.evaluate(() => { - const ids = Array.from(document.querySelectorAll('[data-col-select-id]')).map((e) => e.getAttribute('data-col-select-id') as string); - return ids.find((id) => id.startsWith('data.')) ?? ids.find((id) => id !== 'mrt-row-expand' && id !== 'sort_order') ?? null; - }); - if (!colId) { skip('column_not_found'); return; } - await page.locator(`[data-col-select-id="${colId}"]`).first().click(); - const pinBtn = page.locator('[data-pin-panel] button[aria-label="Закрепить область"]').first(); - await pinBtn.click(); - await page.waitForSelector('[data-pin-panel] button[aria-label="Закрепить область"][data-active="true"]'); + // колонки: две первые data.* в DOM-порядке; закрепляются обе, тянется первая (у неё есть закреплённый сосед справа, + // поэтому устаревший left у соседа делает проверку findPinnedGap способной упасть) + const dataIds = await page.evaluate(() => + Array.from(document.querySelectorAll('[data-col-select-id]')) + .map((e) => e.getAttribute('data-col-select-id') as string) + .filter((id) => id.startsWith('data.')), + ); + if (!dataIds.length) { skip('column_not_found'); return; } + if (dataIds.length < 2) { skip('second_column_not_found'); return; } + const colId = dataIds[0]; + for (const id of dataIds.slice(0, 2)) { + await page.locator(`[data-col-select-id="${id}"]`).first().click(); + await page.locator('[data-pin-panel] button[aria-label="Закрепить область"]').first().click(); + await page.waitForSelector('[data-pin-panel] button[aria-label="Закрепить область"][data-active="true"]'); + } // ресайзер: атрибут из ColumnResizer, иначе запасной поиск по cursor: col-resize (сборка базы AURORA-1837) let box: { x: number; y: number; width: number; height: number } | null = null; @@ -119,36 +127,53 @@ async function measure(browser: import('@playwright/test').Browser, s: ReturnTyp } if (!box) { skip('resizer_not_found'); return; } + const width0 = (await page.locator(`[data-col-select-id="${colId}"]`).first().boundingBox())?.width ?? 0; const x0 = box.x + box.width / 2; const y = box.y + box.height / 2; await page.mouse.move(x0, y); await page.mouse.down(); const pr = await startProbe(page); let x = x0; + let widthPeak = 0; const steps = HALF_STEPS * 2; for (let i = 0; i < steps; i++) { x += i < HALF_STEPS ? STEP_PX : -STEP_PX; await page.mouse.move(x, y); await nextFrame(page); + // один boundingBox на пике хода (шаг 50): его цена входит в пробу один раз из 100 шагов + if (i === HALF_STEPS - 1) widthPeak = (await page.locator(`[data-col-select-id="${colId}"]`).first().boundingBox())?.width ?? 0; } await page.mouse.up(); await nextFrame(page); await nextFrame(page); const probe = await pr.stop(); + const stepMetrics = { ...probe, ...perStep(probe, steps), steps }; + // ход 50 x 4 px = 200 px вправо; ширина должна вырасти хотя бы на половину + if (widthPeak - width0 < (STEP_PX * HALF_STEPS) / 2) { + skip('resize_no_effect', { ...stepMetrics, width_before: width0, width_peak: widthPeak }); + return; + } + + // строка с наибольшим числом закреплённых ячеек: mrt-row-expand, sort_order и две data-колонки const rects: PinnedCellRect[] = await page.evaluate(() => { + let best: { left: number; width: number }[] = []; for (const tr of Array.from(document.querySelectorAll('tbody tr'))) { const tds = Array.from(tr.querySelectorAll('td[data-pinned="true"]')); - if (tds.length >= 2) return tds.map((td) => { const r = td.getBoundingClientRect(); return { left: r.left, width: r.width }; }); + if (tds.length > best.length) best = tds.map((td) => { const r = td.getBoundingClientRect(); return { left: r.left, width: r.width }; }); } - return []; + return best; }); + if (rects.length < MIN_PINNED_CELLS) { + skip('pinned_cells_missing', { ...stepMetrics, pinned_cells_n: rects.length }); + return; + } const gap = findPinnedGap(rects); const overlap = gap >= 0; if (overlap) skipLog.push(`${target} rf cpu${cpu} #${repeat}: pinned_overlap at ${gap}`); append({ ...rec, - metrics: { ...probe, ...perStep(probe, steps), steps, ...(overlap ? { pinned_gap_index: gap } : {}) }, + metrics: { ...stepMetrics, pinned_cells_n: rects.length, ...(overlap ? { pinned_gap_index: gap } : {}) }, artifacts: [], incomplete: overlap, ...(overlap ? { skip_reason: 'pinned_overlap' } : {}),