/** * Tests for PerformanceCollector with mocked page.evaluate. */ import { PerformanceCollector, DEFAULT_PERF_CONFIG } from '../../src/plugins/collectors/PerformanceCollector'; import type { IAction } from '../../src/core/interfaces'; // ─── Helpers ────────────────────────────────────────────────────────────────── function makeAction(): IAction { return { id: 'a1', type: 'click', timestamp: Date.now(), seed: 42, stateId: 'state1' }; } function makePage(timing = { ttfb: 100, domContentLoaded: 500, loadComplete: 1000 }, vitals = { lcp: null as number | null, cls: null as number | null, inp: null as number | null }) { return { url: () => 'http://localhost:3000/page', evaluate: jest.fn() .mockResolvedValueOnce(timing) .mockResolvedValueOnce(vitals), }; } // ─── Tests ──────────────────────────────────────────────────────────────────── describe('PerformanceCollector — disabled', () => { it('returns zeroed metrics and no anomalies when disabled', async () => { const collector = new PerformanceCollector({ enabled: false }); const page = makePage(); const { metrics, anomalies } = await collector.collect(page as never, 'state1', 'sess1', []); expect(anomalies).toHaveLength(0); expect(metrics.ttfb).toBe(0); expect(metrics.lcp).toBeNull(); expect(page.evaluate).not.toHaveBeenCalled(); }); }); describe('PerformanceCollector — enabled', () => { it('captures timing metrics correctly', async () => { const collector = new PerformanceCollector({ enabled: true }); const page = makePage( { ttfb: 200, domContentLoaded: 800, loadComplete: 1500 }, { lcp: null, cls: null, inp: null } ); const { metrics } = await collector.collect(page as never, 'state1', 'sess1', [makeAction()]); expect(metrics.ttfb).toBe(200); expect(metrics.domContentLoaded).toBe(800); expect(metrics.loadComplete).toBe(1500); expect(metrics.sessionId).toBe('sess1'); expect(metrics.stateId).toBe('state1'); expect(metrics.url).toBe('http://localhost:3000/page'); }); it('captures Core Web Vitals when available', async () => { const collector = new PerformanceCollector({ enabled: true }); const page = makePage( { ttfb: 100, domContentLoaded: 400, loadComplete: 900 }, { lcp: 1800, cls: 0.05, inp: 120 } ); const { metrics } = await collector.collect(page as never, 'state1', 'sess1', []); expect(metrics.lcp).toBe(1800); expect(metrics.cls).toBe(0.05); expect(metrics.inp).toBe(120); }); it('returns no anomalies when all metrics are within thresholds', async () => { const collector = new PerformanceCollector({ enabled: true, lcpThresholdMs: 4000, clsThreshold: 0.25, inpThresholdMs: 500, ttfbThresholdMs: 1800, }); const page = makePage( { ttfb: 200, domContentLoaded: 500, loadComplete: 1000 }, { lcp: 1500, cls: 0.05, inp: 100 } ); const { anomalies } = await collector.collect(page as never, 'state1', 'sess1', []); expect(anomalies).toHaveLength(0); }); it('detects LCP violation above threshold', async () => { const collector = new PerformanceCollector({ enabled: true, lcpThresholdMs: 2500, clsThreshold: 0.25, inpThresholdMs: 500, ttfbThresholdMs: 1800, }); const page = makePage( { ttfb: 200, domContentLoaded: 600, loadComplete: 1200 }, { lcp: 5000, cls: 0.01, inp: 50 } ); const { anomalies } = await collector.collect(page as never, 'state1', 'sess1', [makeAction()]); expect(anomalies).toHaveLength(1); expect(anomalies[0]!.type).toBe('performance_degradation'); expect(anomalies[0]!.severity).toBe('high'); expect(anomalies[0]!.description).toContain('LCP'); expect(anomalies[0]!.evidence.rawErrors).toEqual( expect.arrayContaining([expect.stringContaining('LCP')]) ); }); it('detects TTFB violation above threshold', async () => { const collector = new PerformanceCollector({ enabled: true, lcpThresholdMs: 4000, clsThreshold: 0.25, inpThresholdMs: 500, ttfbThresholdMs: 800, }); const page = makePage( { ttfb: 2000, domContentLoaded: 2500, loadComplete: 4000 }, { lcp: null, cls: null, inp: null } ); const { anomalies } = await collector.collect(page as never, 'state1', 'sess1', []); expect(anomalies).toHaveLength(1); expect(anomalies[0]!.type).toBe('performance_degradation'); expect(anomalies[0]!.evidence.rawErrors).toEqual( expect.arrayContaining([expect.stringContaining('TTFB')]) ); }); it('detects CLS violation', async () => { const collector = new PerformanceCollector({ enabled: true, lcpThresholdMs: 4000, clsThreshold: 0.1, inpThresholdMs: 500, ttfbThresholdMs: 1800, }); const page = makePage( { ttfb: 200, domContentLoaded: 500, loadComplete: 1000 }, { lcp: null, cls: 0.35, inp: null } ); const { anomalies } = await collector.collect(page as never, 'state1', 'sess1', []); expect(anomalies).toHaveLength(1); const rawErrors = anomalies[0]!.evidence.rawErrors ?? []; expect(rawErrors.some((e) => e.includes('CLS'))).toBe(true); }); it('accumulates metrics in getMetrics()', async () => { const collector = new PerformanceCollector({ enabled: true }); const page = makePage( { ttfb: 100, domContentLoaded: 400, loadComplete: 800 }, { lcp: null, cls: null, inp: null } ); await collector.collect(page as never, 'state1', 'sess1', []); const page2 = makePage( { ttfb: 150, domContentLoaded: 450, loadComplete: 900 }, { lcp: 2000, cls: null, inp: null } ); await collector.collect(page2 as never, 'state2', 'sess1', []); expect(collector.getMetrics()).toHaveLength(2); }); it('handles page.evaluate failure gracefully', async () => { const collector = new PerformanceCollector({ enabled: true }); const page = { url: () => 'http://localhost', evaluate: jest.fn().mockRejectedValue(new Error('page disconnected')), }; const { metrics, anomalies } = await collector.collect(page as never, 's1', 'sess1', []); expect(metrics.ttfb).toBe(0); expect(anomalies).toHaveLength(0); }); it('uses default thresholds matching DEFAULT_PERF_CONFIG', () => { expect(DEFAULT_PERF_CONFIG.lcpThresholdMs).toBe(4000); expect(DEFAULT_PERF_CONFIG.clsThreshold).toBe(0.25); expect(DEFAULT_PERF_CONFIG.inpThresholdMs).toBe(500); expect(DEFAULT_PERF_CONFIG.ttfbThresholdMs).toBe(1800); }); });