fix(e2e): pinned-resize тянет первую из двух закреплённых data-колонок, проверяет эффект и число закреплённых ячеек
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e0e460b6c2
commit
7d7a98f9a4
@ -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`.
|
||||||
|
|
||||||
### Скролл при соавторе
|
### Скролл при соавторе
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Перетаскивание ширины закреплённой колонки (AURORA-1839, spec sheet-table).
|
* Перетаскивание ширины закреплённой колонки (AURORA-1839, spec sheet-table).
|
||||||
*
|
*
|
||||||
* Закрепляет первую колонку данных листа (роль rf), тянет её границу 50 x (+4 px) и 50 x (-4 px) с ожиданием кадра
|
* Закрепляет две первые колонки данных листа (роль rf), тянет границу первой из них 50 x (+4 px) и 50 x (-4 px) с ожиданием кадра
|
||||||
* после каждого шага, после отпускания проверяет, что td[data-pinned="true"] одной строки примыкают друг к другу.
|
* после каждого шага, после отпускания проверяет, что не менее 4 td[data-pinned="true"] одной строки примыкают друг к другу.
|
||||||
*
|
*
|
||||||
* Параметры (env):
|
* Параметры (env):
|
||||||
* PERF_TARGETS glob(ы) через запятую по имени листа `FORM/SHEET/direction` (по умолчанию 'FORM_2/AHR/*');
|
* PERF_TARGETS glob(ы) через запятую по имени листа `FORM/SHEET/direction` (по умолчанию 'FORM_2/AHR/*');
|
||||||
@ -14,7 +14,8 @@
|
|||||||
* PERF_RUN_ID, PERF_SCALE - как у scroll.spec.ts
|
* PERF_RUN_ID, PERF_SCALE - как у scroll.spec.ts
|
||||||
*
|
*
|
||||||
* Метрика: main_thread_ms_per_step (шаг = движение мыши), frame_p95_ms, heavy_frame_n.
|
* Метрика: 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 { test, type Page } from '@playwright/test';
|
||||||
import { loadManifest, scale } from '../lib/manifest';
|
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 LOAD_TIMEOUT = Number(process.env.PERF_LOAD_TIMEOUT_MS ?? 900_000);
|
||||||
const STEP_PX = 4;
|
const STEP_PX = 4;
|
||||||
const HALF_STEPS = 50;
|
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 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);
|
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 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<void>((r) => requestAnimationFrame(() => r())));
|
const nextFrame = (page: Page) => page.evaluate(() => new Promise<void>((r) => requestAnimationFrame(() => r())));
|
||||||
|
|
||||||
|
type Sheet = ReturnType<typeof loadManifest>['sheets'][number];
|
||||||
const skipLog: string[] = [];
|
const skipLog: string[] = [];
|
||||||
|
|
||||||
async function measure(browser: import('@playwright/test').Browser, s: ReturnType<typeof loadManifest>['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 m = loadManifest();
|
||||||
const user = m.users['rf'];
|
const user = m.users['rf'];
|
||||||
const target = `${name}@pinned`;
|
const target = `${name}@pinned`;
|
||||||
@ -84,16 +87,21 @@ async function measure(browser: import('@playwright/test').Browser, s: ReturnTyp
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await throttle(page, cpu);
|
await throttle(page, cpu);
|
||||||
|
|
||||||
// колонка: первая data.* в DOM-порядке, иначе первая кроме служебных
|
// колонки: две первые data.* в DOM-порядке; закрепляются обе, тянется первая (у неё есть закреплённый сосед справа,
|
||||||
const colId = await page.evaluate(() => {
|
// поэтому устаревший left у соседа делает проверку findPinnedGap способной упасть)
|
||||||
const ids = Array.from(document.querySelectorAll('[data-col-select-id]')).map((e) => e.getAttribute('data-col-select-id') as string);
|
const dataIds = await page.evaluate(() =>
|
||||||
return ids.find((id) => id.startsWith('data.')) ?? ids.find((id) => id !== 'mrt-row-expand' && id !== 'sort_order') ?? null;
|
Array.from(document.querySelectorAll('[data-col-select-id]'))
|
||||||
});
|
.map((e) => e.getAttribute('data-col-select-id') as string)
|
||||||
if (!colId) { skip('column_not_found'); return; }
|
.filter((id) => id.startsWith('data.')),
|
||||||
await page.locator(`[data-col-select-id="${colId}"]`).first().click();
|
);
|
||||||
const pinBtn = page.locator('[data-pin-panel] button[aria-label="Закрепить область"]').first();
|
if (!dataIds.length) { skip('column_not_found'); return; }
|
||||||
await pinBtn.click();
|
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"]');
|
await page.waitForSelector('[data-pin-panel] button[aria-label="Закрепить область"][data-active="true"]');
|
||||||
|
}
|
||||||
|
|
||||||
// ресайзер: атрибут из ColumnResizer, иначе запасной поиск по cursor: col-resize (сборка базы AURORA-1837)
|
// ресайзер: атрибут из ColumnResizer, иначе запасной поиск по cursor: col-resize (сборка базы AURORA-1837)
|
||||||
let box: { x: number; y: number; width: number; height: number } | null = null;
|
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; }
|
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 x0 = box.x + box.width / 2;
|
||||||
const y = box.y + box.height / 2;
|
const y = box.y + box.height / 2;
|
||||||
await page.mouse.move(x0, y);
|
await page.mouse.move(x0, y);
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
const pr = await startProbe(page);
|
const pr = await startProbe(page);
|
||||||
let x = x0;
|
let x = x0;
|
||||||
|
let widthPeak = 0;
|
||||||
const steps = HALF_STEPS * 2;
|
const steps = HALF_STEPS * 2;
|
||||||
for (let i = 0; i < steps; i++) {
|
for (let i = 0; i < steps; i++) {
|
||||||
x += i < HALF_STEPS ? STEP_PX : -STEP_PX;
|
x += i < HALF_STEPS ? STEP_PX : -STEP_PX;
|
||||||
await page.mouse.move(x, y);
|
await page.mouse.move(x, y);
|
||||||
await nextFrame(page);
|
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 page.mouse.up();
|
||||||
await nextFrame(page);
|
await nextFrame(page);
|
||||||
await nextFrame(page);
|
await nextFrame(page);
|
||||||
const probe = await pr.stop();
|
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(() => {
|
const rects: PinnedCellRect[] = await page.evaluate(() => {
|
||||||
|
let best: { left: number; width: number }[] = [];
|
||||||
for (const tr of Array.from(document.querySelectorAll('tbody tr'))) {
|
for (const tr of Array.from(document.querySelectorAll('tbody tr'))) {
|
||||||
const tds = Array.from(tr.querySelectorAll('td[data-pinned="true"]'));
|
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 gap = findPinnedGap(rects);
|
||||||
const overlap = gap >= 0;
|
const overlap = gap >= 0;
|
||||||
if (overlap) skipLog.push(`${target} rf cpu${cpu} #${repeat}: pinned_overlap at ${gap}`);
|
if (overlap) skipLog.push(`${target} rf cpu${cpu} #${repeat}: pinned_overlap at ${gap}`);
|
||||||
append({
|
append({
|
||||||
...rec,
|
...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: [],
|
artifacts: [],
|
||||||
incomplete: overlap,
|
incomplete: overlap,
|
||||||
...(overlap ? { skip_reason: 'pinned_overlap' } : {}),
|
...(overlap ? { skip_reason: 'pinned_overlap' } : {}),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user