Files
Portfolio/tests/performance/physical-drive.protocol.ts

168 lines
8.1 KiB
TypeScript

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