/** * Замер открытия модалки (Task 7). * measureOpen(page, opener, {id, appear?, timeoutMs?}) -> {click_to_visible_ms, long_task_n, long_task_ms, main_thread_ms} * * - начало: performance.now() в обработчике pointerdown (capture, на window); * - появление: MutationObserver (+ проверка на каждом кадре) ловит видимый элемент по `appear` * (по умолчанию `[role=dialog]`; у common/Modal и emotion-модалок приложения role нет, тогда css + text); * - конец: кадр после кадра с появившейся модалкой (двойной rAF = «модалка отрисована»); * - long tasks: PerformanceObserver 'longtask', засчитываются задачи, начавшиеся между pointerdown и концом; * - main_thread_ms: прирост CDP Performance.TaskDuration от клика до конца окна (все задачи main thread: * скрипт + стили + layout + paint-подготовка). * Нет модалки за timeoutMs (5 с) - ошибка с id модалки. */ import type { Page } from '@playwright/test'; import { mainThreadMs } from './cdp'; export interface Appear { css?: string; text?: string } export interface OpenOpts { id: string; appear?: Appear; timeoutMs?: number } export interface OpenMetrics { click_to_visible_ms: number; long_task_n: number; long_task_ms: number; main_thread_ms: number; } export const OPEN_TIMEOUT_MS = 5000; export async function measureOpen(page: Page, opener: () => Promise, opts: OpenOpts): Promise { const timeout = opts.timeoutMs ?? OPEN_TIMEOUT_MS; const appear: Required = { css: opts.appear?.css ?? '[role=dialog]', text: opts.appear?.text ?? '' }; await page.evaluate( ({ css, text, tmo }) => { const w = window as any; const visible = () => { for (const el of Array.from(document.querySelectorAll(css)) as HTMLElement[]) { if (text && !(el.textContent ?? '').includes(text)) continue; const r = el.getBoundingClientRect(); const st = getComputedStyle(el); if (r.width > 0 && r.height > 0 && st.display !== 'none' && st.visibility !== 'hidden') return true; } return false; }; const st: any = { t0: -1, tEnd: -1, tasks: [] as Array<{ start: number; dur: number }>, timedOut: false }; const onDown = () => { if (st.t0 < 0) st.t0 = performance.now(); }; window.addEventListener('pointerdown', onDown, true); let po: PerformanceObserver | null = null; try { po = new PerformanceObserver((l) => { for (const e of l.getEntries()) st.tasks.push({ start: e.startTime, dur: e.duration }); }); po.observe({ type: 'longtask', buffered: false }); } catch { /* longtask недоступен */ } let resolveShown: () => void = () => {}; st.shown = new Promise((r) => { resolveShown = r; }); // кадр с модалкой отрисован (tEnd зафиксирован) st.done = new Promise((resolve) => { // + 100 мс на доставку longtask let finished = false; const cleanup = () => { finished = true; mo.disconnect(); window.removeEventListener('pointerdown', onDown, true); }; const end = () => { // кадр с модалкой уже построен; следующий rAF - после его отрисовки requestAnimationFrame(() => requestAnimationFrame(() => { st.tEnd = performance.now(); resolveShown(); cleanup(); setTimeout(() => { po?.disconnect(); resolve(); }, 100); // longtask доставляется асинхронно })); }; const check = () => { if (!finished && st.t0 >= 0 && visible()) { finished = true; mo.disconnect(); end(); } }; const mo = new MutationObserver(check); mo.observe(document.documentElement, { childList: true, subtree: true, attributes: true }); // страховка: модалка могла стать видимой без мутации (transition/стили) const poll = () => { if (finished) return; check(); if (!finished && !st.timedOut) requestAnimationFrame(poll); }; requestAnimationFrame(poll); setTimeout(() => { if (!finished) { st.timedOut = true; cleanup(); po?.disconnect(); resolveShown(); resolve(); } }, tmo); }); w.__modalOpen = st; }, { ...appear, tmo: timeout }, ); const cpu0 = await mainThreadMs(page); await opener(); // main thread меряем сразу по отрисовке модалки (без 100 мс ожидания longtask и без последующих задач страницы) await page.evaluate(() => (window as any).__modalOpen.shown); const cpu1 = await mainThreadMs(page); const res = await page.evaluate(async () => { const st = (window as any).__modalOpen; await st.done; return { t0: st.t0, tEnd: st.tEnd, timedOut: st.timedOut, tasks: st.tasks as Array<{ start: number; dur: number }> }; }); if (res.timedOut || res.tEnd < 0) { throw new Error(`modal '${opts.id}' did not appear within ${timeout} ms (selector ${appear.css}${appear.text ? ` + text '${appear.text}'` : ''}, pointerdown ${res.t0 >= 0 ? 'seen' : 'not seen'})`); } const inWin = res.tasks.filter((t) => t.start >= res.t0 - 1 && t.start <= res.tEnd); return { click_to_visible_ms: res.tEnd - res.t0, long_task_n: inWin.length, long_task_ms: inWin.reduce((a, t) => a + t.dur, 0), main_thread_ms: cpu1 - cpu0, }; }