perf: enforce journey budgets and reference traces
This commit is contained in:
167
tests/performance/physical-drive.protocol.ts
Normal file
167
tests/performance/physical-drive.protocol.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
export type CycleDirection = 'forward' | 'reverse';
|
||||
|
||||
export interface SafeViewport { safeX: number; safeY: number; width: number; height: number }
|
||||
export interface ViewportGeometry { offsetLeft: number; offsetTop: number; width: number; height: number }
|
||||
export interface SafeInsets { left: number; right: number; top: number; bottom: number }
|
||||
export interface TargetBox { left: number; top: number; right: number; bottom: number }
|
||||
|
||||
type PointerAction =
|
||||
| { type: 'pointerMove'; duration: number; origin: 'viewport'; x: number; y: number }
|
||||
| { type: 'pointerDown'; button: 0 }
|
||||
| { type: 'pointerUp'; button: 0 }
|
||||
| { type: 'pause'; duration: number };
|
||||
|
||||
type KeyAction =
|
||||
| { type: 'keyDown'; value: string }
|
||||
| { type: 'keyUp'; value: string }
|
||||
| { type: 'pause'; duration: number };
|
||||
|
||||
export const roundHalfUp = (value: number): number => Math.floor(value + 0.5);
|
||||
|
||||
export const computeSafeViewport = (viewport: ViewportGeometry, insets: SafeInsets): SafeViewport => {
|
||||
const width = viewport.width - insets.left - insets.right;
|
||||
const height = viewport.height - insets.top - insets.bottom;
|
||||
if (!(width > 0) || !(height > 0)) throw new Error('Safe viewport dimensions must be positive.');
|
||||
return {
|
||||
safeX: viewport.offsetLeft + insets.left,
|
||||
safeY: viewport.offsetTop + insets.top,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildSwipeCycle = (direction: CycleDirection, safe: SafeViewport) => {
|
||||
const startFraction = direction === 'forward' ? 0.8 : 0.2;
|
||||
const delta = direction === 'forward' ? -0.7 : 0.7;
|
||||
const x = roundHalfUp(safe.safeX + 0.5 * safe.width);
|
||||
const actions: PointerAction[] = [
|
||||
{ type: 'pointerMove', duration: 0, origin: 'viewport', x, y: roundHalfUp(safe.safeY + safe.height * startFraction) },
|
||||
{ type: 'pointerDown', button: 0 },
|
||||
];
|
||||
for (let index = 1; index <= 15; index += 1) {
|
||||
actions.push({
|
||||
type: 'pointerMove',
|
||||
duration: 40,
|
||||
origin: 'viewport',
|
||||
x,
|
||||
y: roundHalfUp(safe.safeY + safe.height * (startFraction + delta * index / 15)),
|
||||
});
|
||||
}
|
||||
actions.push({ type: 'pointerUp', button: 0 }, { type: 'pause', duration: 400 });
|
||||
return {
|
||||
pointer: { type: 'pointer' as const, id: 'driveTouch' as const, parameters: { pointerType: 'touch' as const }, actions },
|
||||
keyboard: { type: 'key' as const, id: 'driveKeys' as const, actions: [{ type: 'pause' as const, duration: 1_000 }] },
|
||||
};
|
||||
};
|
||||
|
||||
const driveSources = (pointerActions: PointerAction[], keyboardActions: KeyAction[]) => ({
|
||||
pointer: {
|
||||
type: 'pointer' as const,
|
||||
id: 'driveTouch' as const,
|
||||
parameters: { pointerType: 'touch' as const },
|
||||
actions: pointerActions,
|
||||
},
|
||||
keyboard: { type: 'key' as const, id: 'driveKeys' as const, actions: keyboardActions },
|
||||
});
|
||||
|
||||
export const buildTargetActivation = (first: TargetBox, second: TargetBox, safe: SafeViewport) => {
|
||||
for (const edge of ['left', 'top', 'right', 'bottom'] as const) {
|
||||
if (Math.abs(first[edge] - second[edge]) >= 1) throw new Error('Activation target box must remain stable across both samples.');
|
||||
}
|
||||
const left = Math.max(safe.safeX, second.left);
|
||||
const top = Math.max(safe.safeY, second.top);
|
||||
const right = Math.min(safe.safeX + safe.width, second.right);
|
||||
const bottom = Math.min(safe.safeY + safe.height, second.bottom);
|
||||
if (!(right > left) || !(bottom > top)) throw new Error('Activation target must intersect the safe viewport.');
|
||||
return driveSources([
|
||||
{ type: 'pointerMove', duration: 0, origin: 'viewport', x: roundHalfUp((left + right) / 2), y: roundHalfUp((top + bottom) / 2) },
|
||||
{ type: 'pause', duration: 60 },
|
||||
{ type: 'pointerDown', button: 0 },
|
||||
{ type: 'pause', duration: 60 },
|
||||
{ type: 'pointerUp', button: 0 },
|
||||
], [{ type: 'pause', duration: 120 }]);
|
||||
};
|
||||
|
||||
export const buildVolumeKeySequence = () => driveSources(
|
||||
[{ type: 'pause', duration: 16_040 }],
|
||||
[
|
||||
{ type: 'pause', duration: 15_900 },
|
||||
{ type: 'keyDown', value: '\uE004' },
|
||||
{ type: 'pause', duration: 20 },
|
||||
{ type: 'keyUp', value: '\uE004' },
|
||||
{ type: 'pause', duration: 80 },
|
||||
{ type: 'keyDown', value: '\uE012' },
|
||||
{ type: 'pause', duration: 40 },
|
||||
{ type: 'keyUp', value: '\uE012' },
|
||||
],
|
||||
);
|
||||
|
||||
export const chooseCycleDirection = (
|
||||
direction: CycleDirection,
|
||||
rawScrollY: number,
|
||||
rawMaxScrollY: number,
|
||||
): CycleDirection => {
|
||||
const maxScrollY = Math.max(0, rawMaxScrollY);
|
||||
const scrollY = Math.min(maxScrollY, Math.max(0, rawScrollY));
|
||||
if (direction === 'forward' && scrollY >= maxScrollY - 2) return 'reverse';
|
||||
if (direction === 'reverse' && scrollY <= 2) return 'forward';
|
||||
return direction;
|
||||
};
|
||||
|
||||
const integerRange = (start: number, endExclusive: number): number[] => Array.from(
|
||||
{ length: endExclusive - start },
|
||||
(_, index) => start + index,
|
||||
);
|
||||
|
||||
export const physicalCycleOffsets = (): number[] => [
|
||||
...integerRange(30, 120),
|
||||
...integerRange(125, 180),
|
||||
...integerRange(275, 510).filter((offset) => offset !== 389 && offset !== 390),
|
||||
];
|
||||
|
||||
interface ProtocolEvent { offsetMs: number; action: string; assertions: string[] }
|
||||
interface ProtocolBranch { offsetMs: number; action: string; assertions: string[] }
|
||||
|
||||
export const buildPhysicalDriveProtocol = () => {
|
||||
const events: ProtocolEvent[] = [
|
||||
{ offsetMs: 0, action: 'begin-idle-portrait', assertions: ['one-canvas-loop', 'geometry'] },
|
||||
{ offsetMs: 15_000, action: 'activate-start-the-drive', assertions: ['state-by-15500'] },
|
||||
{ offsetMs: 15_900, action: 'volume-tab-keydown', assertions: [] },
|
||||
{ offsetMs: 15_920, action: 'volume-tab-keyup', assertions: ['slider-focused'] },
|
||||
{ offsetMs: 16_000, action: 'volume-arrow-left-keydown', assertions: [] },
|
||||
{ offsetMs: 16_040, action: 'volume-arrow-left-keyup', assertions: ['volume-35-by-16500'] },
|
||||
...physicalCycleOffsets().map((offset) => ({
|
||||
offsetMs: offset * 1_000,
|
||||
action: 'scroll-cycle',
|
||||
assertions: offset === 391
|
||||
? ['sample-direction-and-safe-viewport', 'cancel-exact-pending-navigation-epoch', 'release-barrier']
|
||||
: ['sample-direction-and-safe-viewport'],
|
||||
})),
|
||||
{ offsetMs: 120_000, action: 'rotate-landscape', assertions: ['geometry-by-125000', 'one-canvas-loop', 'tier-at-most-maximum'] },
|
||||
{ offsetMs: 180_000, action: 'background', assertions: ['no-page-input-for-30s'] },
|
||||
{ offsetMs: 210_000, action: 'foreground', assertions: ['lifecycle-counts-by-215000'] },
|
||||
{ offsetMs: 225_000, action: 'audio-interruption', assertions: ['suspended-and-resume-sound-by-227000'] },
|
||||
{ offsetMs: 230_000, action: 'activate-resume-sound', assertions: ['state-by-230500'] },
|
||||
{ offsetMs: 250_000, action: 'context-loss', assertions: [] },
|
||||
{ offsetMs: 252_000, action: 'context-restore', assertions: ['one-running-generation-or-static-by-257000'] },
|
||||
{ offsetMs: 270_000, action: 'rotate-portrait', assertions: ['geometry-by-275000'] },
|
||||
{ offsetMs: 390_000, action: 'arm-navigation-settlement-barrier', assertions: [] },
|
||||
{ offsetMs: 390_000, action: 'navigate-infrastructure', assertions: ['one-navigation-epoch'] },
|
||||
{ offsetMs: 510_000, action: 'background', assertions: ['no-page-input-for-30s'] },
|
||||
{ offsetMs: 540_000, action: 'foreground', assertions: ['lifecycle-counts-by-545000'] },
|
||||
{ offsetMs: 575_000, action: 'navigate-contact', assertions: ['state-by-575500'] },
|
||||
{ offsetMs: 580_000, action: 'settle-contact', assertions: ['contact-usable', 'final-geometry'] },
|
||||
{ offsetMs: 600_000, action: 'finish', assertions: ['no-crash', 'no-duplicates', 'monotonic-quality'] },
|
||||
].sort((left, right) => left.offsetMs - right.offsetMs);
|
||||
const branches: { live: ProtocolBranch[]; static: ProtocolBranch[] } = {
|
||||
live: [
|
||||
{ offsetMs: 550_000, action: 'pause-motion', assertions: ['zero-scene-raf', 'retained-frame', 'audio-independent'] },
|
||||
{ offsetMs: 565_000, action: 'resume-motion', assertions: ['one-canvas-loop-by-570000'] },
|
||||
],
|
||||
static: [
|
||||
{ offsetMs: 550_000, action: 'assert-static-without-input', assertions: ['poster', 'zero-raf', 'motion-unavailable', 'audio-independent'] },
|
||||
{ offsetMs: 565_000, action: 'assert-static-without-input', assertions: ['unchanged-poster', 'zero-raf', 'controls-usable'] },
|
||||
],
|
||||
};
|
||||
return { version: 'physical-drive.v1' as const, durationMs: 600_000, events, branches };
|
||||
};
|
||||
Reference in New Issue
Block a user