perf: enforce journey budgets and reference traces
This commit is contained in:
111
tests/performance/metrics.test.ts
Normal file
111
tests/performance/metrics.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
calculateCls,
|
||||
calculateLabTbt,
|
||||
calculateLostPresentationShare,
|
||||
evaluateEventToNextPaint,
|
||||
evaluateSceneContinuity,
|
||||
latestLcpBeforeCutoff,
|
||||
nearestRankP95,
|
||||
postRunningLongTasks,
|
||||
unexpectedLateShifts,
|
||||
type ScriptedActionSample,
|
||||
} from '@/testing/performance-instrumentation';
|
||||
|
||||
describe('performance metric math', () => {
|
||||
it('uses exact nearest-rank p95 without interpolation', () => {
|
||||
expect(nearestRankP95([100, 1, 3, 2, 4])).toBe(100);
|
||||
expect(nearestRankP95(Array.from({ length: 20 }, (_, index) => index + 1))).toBe(19);
|
||||
expect(() => nearestRankP95([])).toThrow(/sample/u);
|
||||
});
|
||||
|
||||
it('cuts LCP off at the first trusted input or hidden transition', () => {
|
||||
expect(latestLcpBeforeCutoff([100, 200, 450], 400)).toBe(200);
|
||||
expect(() => latestLcpBeforeCutoff([], 400)).toThrow(/LCP/u);
|
||||
});
|
||||
|
||||
it('calculates CLS with exact one-second and five-second session windows', () => {
|
||||
expect(calculateCls([
|
||||
{ startTime: 0, value: 0.01, hadRecentInput: false },
|
||||
{ startTime: 1_000, value: 0.01, hadRecentInput: false },
|
||||
{ startTime: 2_000, value: 0.01, hadRecentInput: false },
|
||||
{ startTime: 3_000, value: 0.01, hadRecentInput: false },
|
||||
{ startTime: 4_000, value: 0.01, hadRecentInput: false },
|
||||
{ startTime: 5_000, value: 0.01, hadRecentInput: false },
|
||||
{ startTime: 5_001, value: 0.2, hadRecentInput: false },
|
||||
{ startTime: 5_100, value: 1, hadRecentInput: true },
|
||||
])).toBeCloseTo(0.2);
|
||||
});
|
||||
|
||||
it('clips lab TBT to the post-50ms portion before running', () => {
|
||||
expect(calculateLabTbt([
|
||||
{ startTime: 0, duration: 100 },
|
||||
{ startTime: 180, duration: 100 },
|
||||
{ startTime: 250, duration: 100 },
|
||||
], 20, 300)).toBe(100);
|
||||
expect(() => calculateLabTbt([], null, 300)).toThrow(/FCP/u);
|
||||
});
|
||||
|
||||
it('detects every long task intersecting the post-running trace', () => {
|
||||
expect(postRunningLongTasks([
|
||||
{ startTime: 900, duration: 200 },
|
||||
{ startTime: 20_999, duration: 2 },
|
||||
{ startTime: 21_000, duration: 100 },
|
||||
], 1_000, 21_000)).toEqual([
|
||||
{ startTime: 900, duration: 200 },
|
||||
{ startTime: 20_999, duration: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires the exact eight action confirmations and applies the conservative 16ms bound', () => {
|
||||
const ids = ['start', 'volume', 'mute', 'unmute', 'pause', 'resume', 'toolkit', 'contact'] as const;
|
||||
const actions: ScriptedActionSample[] = ids.map((id, index) => ({
|
||||
id,
|
||||
target: `target-${id}`,
|
||||
dispatchTime: index * 1_000,
|
||||
acknowledgementCount: 1,
|
||||
mutationTime: index * 1_000 + 20,
|
||||
doubleRafTime: index * 1_000 + 40,
|
||||
eventTimingSupported: true,
|
||||
eventEntries: index === 7 ? [{ target: `target-${id}`, startTime: index * 1_000, duration: 80, interactionId: 7 }] : [],
|
||||
}));
|
||||
expect(evaluateEventToNextPaint(actions)).toEqual({ values: [16, 16, 16, 16, 16, 16, 16, 80], p95: 80 });
|
||||
expect(() => evaluateEventToNextPaint(actions.slice(0, 7))).toThrow(/eight/u);
|
||||
expect(() => evaluateEventToNextPaint([{ ...actions[0]!, acknowledgementCount: 2 }, ...actions.slice(1)])).toThrow(/acknowledgement/u);
|
||||
});
|
||||
|
||||
it('evaluates scene continuity at interval boundaries and callback coverage', () => {
|
||||
const result = evaluateSceneContinuity(
|
||||
[{ start: 0, end: 100 }],
|
||||
[
|
||||
{ start: 10, end: 12 },
|
||||
{ start: 28, end: 30 },
|
||||
{ start: 46, end: 48 },
|
||||
{ start: 64, end: 66 },
|
||||
{ start: 82, end: 84 },
|
||||
],
|
||||
16.667,
|
||||
);
|
||||
expect(result.passes).toBe(true);
|
||||
expect(result.expectedCallbacks).toBe(5);
|
||||
expect(result.actualCallbacks).toBe(5);
|
||||
expect(evaluateSceneContinuity([{ start: 0, end: 100 }], [{ start: 60, end: 61 }], 16.667).passes).toBe(false);
|
||||
});
|
||||
|
||||
it('counts lost presentation opportunities from page-level RAF intervals', () => {
|
||||
const result = calculateLostPresentationShare([0, 16.667, 50.001, 66.668], 16.667, 3);
|
||||
expect(result.opportunities).toBe(4);
|
||||
expect(result.lost).toBe(1);
|
||||
expect(result.share).toBeCloseTo(0.25);
|
||||
expect(result.hasSufficientCoverage).toBe(true);
|
||||
});
|
||||
|
||||
it('allows no unexpected late shifts', () => {
|
||||
expect(unexpectedLateShifts([
|
||||
{ startTime: 99, value: 1, hadRecentInput: false },
|
||||
{ startTime: 101, value: 1, hadRecentInput: true },
|
||||
{ startTime: 102, value: 0.1, hadRecentInput: false },
|
||||
{ startTime: 201, value: 0.1, hadRecentInput: false },
|
||||
], 100, 200)).toEqual([{ startTime: 102, value: 0.1, hadRecentInput: false }]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user