- run-all.sh: копия run-all.status.json в report-data с относительными путями; пример control4x с отдельным тегом и предупреждением (I-3, M-8); очищены три закоммиченные копии статуса - lib/modal.ts: long tasks по пересечению окна (tasksInWindow), тест (M-7) - scale_compare.py: строки листов на момент скролла (до grow), пояснения (M-6) - hotspots_summary.py: оговорка о разбавлении «на шаг» (M-5) - netlog_sizes.py + report-data/20261026-s1/analysis/netlog-sizes.md (M-3) Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
4.9 KiB
TypeScript
89 lines
4.9 KiB
TypeScript
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]);
|
||
});
|