Files
Autonomous-Bug-Explorer/tests/plugins/performanceCollector.test.ts

186 lines
6.7 KiB
TypeScript

/**
* 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);
});
});