import { test, expect, chromium } from '@playwright/test'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { PROBE_SCRIPT, startProbe } from '../lib/probe'; import { throttle } from '../lib/cdp'; import { append } from '../lib/results'; import { shouldTrace } from '../lib/trace'; const BUSY = 'file://' + path.resolve(__dirname, '../fixtures/busy.html'); const PERF_DIR = path.resolve(__dirname, '../..'); test('probe detects long task', async ({ page }) => { await page.addInitScript(PROBE_SCRIPT); await page.goto(BUSY); await page.evaluate(() => (window as any).__perf.start()); await page.click('#busy'); const m = await page.evaluate(() => (window as any).__perf.stop()); expect(m.long_task_n).toBeGreaterThanOrEqual(1); expect(m.long_task_ms).toBeGreaterThanOrEqual(100); expect(m.frame_n).toBeGreaterThan(0); expect(m.duration_ms).toBeGreaterThan(0); }); test('probe: main_thread_ms (CDP TaskDuration) и frame_excess_ms учитывают задачу 120 мс', async ({ page }) => { await page.addInitScript(PROBE_SCRIPT); await page.goto(BUSY); const p = await startProbe(page); await page.click('#busy'); const m = await p.stop(); expect(m.main_thread_ms).toBeGreaterThanOrEqual(100); expect(m.main_thread_ms).toBeLessThan(m.duration_ms); expect(m.frame_excess_ms).toBeGreaterThanOrEqual(80); // кадр ~120 мс сверх бюджета 16,7 мс expect(m.long_task_ms).toBeGreaterThanOrEqual(100); }); test('probe: main_thread_ms на простое - малый', async ({ page }) => { await page.addInitScript(PROBE_SCRIPT); await page.goto(BUSY); const p = await startProbe(page); await page.waitForTimeout(300); const m = await p.stop(); expect(m.main_thread_ms).toBeGreaterThanOrEqual(0); expect(m.main_thread_ms).toBeLessThan(50); }); test('throttle slows script', async ({ page }) => { await page.goto(BUSY); const loop = () => page.evaluate(() => { const t = performance.now(); let x = 0; for (let i = 0; i < 3e7; i++) x += i % 7; return performance.now() - t + x * 0; }); await throttle(page, 1); const base = await loop(); await throttle(page, 4); const slow = await loop(); expect(slow).toBeGreaterThanOrEqual(2 * base); }); test('fresh context isolation', async ({ browser }) => { const a = await browser.newContext(); const pa = await a.newPage(); await pa.goto(BUSY); await pa.evaluate(() => localStorage.setItem('k', 'v')); const b = await browser.newContext(); const pb = await b.newPage(); await pb.goto(BUSY); expect(await pb.evaluate(() => localStorage.getItem('k'))).toBeNull(); await a.close(); await b.close(); }); test('results format', async () => { const tmp = mkdtempSync(path.join(tmpdir(), 'perf-')); const runDir = path.join(tmp, `harness-${Date.now()}`); process.env.PERF_RESULTS_DIR = runDir; append({ scenario: 'harness', target: 'prod', role: 'admin', scale: 1, cpu_rate: 1, repeat: 0, metrics: { long_task_ms: 123.4 }, artifacts: [], }); const py = path.join(PERF_DIR, '.venv/bin/python'); const out = execFileSync(py, ['-m', 'perfkit.aggregate', runDir], { cwd: PERF_DIR }).toString().trim(); const summary = JSON.parse(execFileSync('cat', [path.join(out, 'summary.json')]).toString()); expect(summary.groups).toHaveLength(1); expect(summary.groups[0].n).toBe(1); rmSync(tmp, { recursive: true, force: true }); rmSync(out, { recursive: true, force: true }); }); test('globalTeardown: PERF_SQLSTATS_TAG -> dump --out /sql/', async () => { const { dumpArgs } = await import('../lib/global-teardown'); process.env.PERF_RESULTS_DIR = '/tmp/perf-rd'; try { delete process.env.PERF_SQLSTATS_TAG; expect(dumpArgs()).toEqual(['dump', '/tmp/perf-rd']); process.env.PERF_SQLSTATS_TAG = 'modals profiling'; expect(dumpArgs()).toEqual(['dump', '/tmp/perf-rd', '--out', '/tmp/perf-rd/sql/modals_profiling']); } finally { delete process.env.PERF_SQLSTATS_TAG; delete process.env.PERF_RESULTS_DIR; } }); test('shouldTrace: trace только на повторе 0 и только если повторов > 1 (иначе aggregate исключил бы единственную запись)', () => { expect(shouldTrace({ repeat: 0, repeats: 3 }, {})).toBe(true); expect(shouldTrace({ repeat: 1, repeats: 3 }, {})).toBe(false); expect(shouldTrace({ repeat: 0, repeats: 1 }, {})).toBe(false); // admin-контроль, control4x, PERF_S2_REPEATS=1 expect(shouldTrace({ repeat: 0, repeats: 2 }, {})).toBe(true); expect(shouldTrace({ repeat: 0, repeats: 3, filter: true }, {})).toBe(false); // смена фильтра не трассируется expect(shouldTrace({ repeat: 0, repeats: 3 }, { PERF_TRACE: '0' })).toBe(false); });