186 lines
6.7 KiB
TypeScript
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);
|
|
});
|
|
});
|