import { test, expect } from '@playwright/test'; import path from 'node:path'; import { measureOpen, tasksInWindow } from '../lib/modal'; import { REACT_HOOK_SCRIPT, profileStart, profileStop } from '../lib/reactProfile'; const fx = (n: string) => 'file://' + path.resolve(__dirname, '../fixtures', n); test('measureOpen: role=dialog через 150 мс - click_to_visible_ms в [150, 400], main_thread_ms > 0 и <= click_to_visible_ms', async ({ page }) => { await page.goto(fx('modal.html')); const r = await measureOpen(page, () => page.click('#open'), { id: 'fixture_open' }); expect(r.click_to_visible_ms).toBeGreaterThanOrEqual(150); expect(r.click_to_visible_ms).toBeLessThanOrEqual(400); expect(r.main_thread_ms).toBeGreaterThan(0); expect(r.main_thread_ms).toBeLessThanOrEqual(r.click_to_visible_ms); expect(r.long_task_n).toBe(0); }); test('measureOpen: синхронная работа 90 мс даёт long task и main_thread_ms >= 90', async ({ page }) => { await page.goto(fx('modal.html')); const r = await measureOpen(page, () => page.click('#open-busy'), { id: 'fixture_busy' }); expect(r.long_task_n).toBeGreaterThanOrEqual(1); expect(r.long_task_ms).toBeGreaterThanOrEqual(80); expect(r.main_thread_ms).toBeGreaterThanOrEqual(85); expect(r.main_thread_ms).toBeLessThanOrEqual(r.click_to_visible_ms); }); test('measureOpen: произвольный признак (css + text) вместо role=dialog', async ({ page }) => { await page.goto(fx('modal.html')); const r = await measureOpen(page, () => page.click('#open-plain'), { id: 'fixture_plain', appear: { css: '.plain-modal h2', text: 'Заголовок' } }); expect(r.click_to_visible_ms).toBeGreaterThanOrEqual(150); // неверный текст - не считается появлением await page.reload(); await expect( measureOpen(page, () => page.click('#open-plain'), { id: 'fixture_wrong_text', appear: { css: '.plain-modal h2', text: 'Нет такого' }, timeoutMs: 1500 }), ).rejects.toThrow(/fixture_wrong_text/); }); test('measureOpen: кнопка без модалки падает по таймауту 5 с, id модалки в сообщении', async ({ page }) => { await page.goto(fx('modal.html')); const t0 = Date.now(); await expect(measureOpen(page, () => page.click('#nothing'), { id: 'fixture_none' })).rejects.toThrow(/fixture_none/); const dt = Date.now() - t0; expect(dt).toBeGreaterThanOrEqual(4800); expect(dt).toBeLessThan(9000); }); test('reactProfile: хук получает коммиты, топ-20 по displayName с renders и self_ms', async ({ page }) => { await page.addInitScript(REACT_HOOK_SCRIPT); await page.goto(fx('modal.html')); await profileStart(page); // синтетический коммит: App(10 мс, self 4) -> [Row(3), Row(3)], без alternate => новые fiber'ы await page.evaluate(() => { const mk = (name: string, actual: number, children: any[] = [], flags = 1) => { const f: any = { tag: 0, type: Object.assign(function () {}, { displayName: name }), flags, actualDuration: actual, child: null, sibling: null, alternate: null }; children.forEach((c, i) => { c.return = f; if (i === 0) f.child = c; else children[i - 1].sibling = c; }); return f; }; const root: any = { current: { tag: 3, flags: 0, actualDuration: 0, alternate: null, child: null, sibling: null } }; root.current.child = mk('App', 10, [mk('Row', 3), mk('Row', 3)]); root.current.child.return = root.current; const hook = (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; const id = hook.inject({ version: 'test' }); hook.onCommitFiberRoot(id, root, 0, false); hook.onCommitFiberRoot(id, root, 0, false); }); const p = await profileStop(page); expect(p.commits).toBe(2); const row = p.top.find((x) => x.component === 'Row')!; const app = p.top.find((x) => x.component === 'App')!; expect(row.renders).toBe(4); expect(row.self_ms).toBeCloseTo(12, 5); expect(app.renders).toBe(2); expect(app.self_ms).toBeCloseTo(8, 5); expect(p.top[0].component).toBe('Row'); // сортировка по self_ms expect(p.top.length).toBeLessThanOrEqual(20); }); test('tasksInWindow: задача, начавшаяся до pointerdown и пересекающая окно, засчитывается (задача клика)', () => { const t0 = 100, tEnd = 300; const tasks = [ { start: 40, dur: 50 }, // закончилась до клика - нет { start: 95, dur: 120 }, // задача клика: началась раньше t0 (обработчик pointerdown внутри неё) - да { start: 150, dur: 60 }, // внутри окна - да { start: 299, dur: 80 }, // началась до конца окна - да { start: 310, dur: 60 }, // после окна - нет ]; expect(tasksInWindow(tasks, t0, tEnd).map((t) => t.start)).toEqual([95, 150, 299]); });