Перечень модалок (modals.json), measureOpen с main_thread_ms, профиль перерисовок React (reactProfile), сценарий modals.spec.ts, подкоманда stand.sh web-profiling (:8081), генератор modals-inventory.md. Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
4.1 KiB
TypeScript
77 lines
4.1 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
import path from 'node:path';
|
||
import { measureOpen } 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);
|
||
});
|