Files
Portfolio/tests/performance/metrics.test.ts

112 lines
4.5 KiB
TypeScript

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 }]);
});
});