112 lines
4.5 KiB
TypeScript
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 }]);
|
|
});
|
|
});
|