From 1cec658bafbf8fcd00401bce1763b3889ea540ea Mon Sep 17 00:00:00 2001 From: LucaP Date: Wed, 2 Sep 2026 09:12:42 +0300 Subject: [PATCH] fix: disambiguate repeated-target event timing --- src/testing/performance-instrumentation.ts | 13 +++++- tests/performance/metrics.test.ts | 47 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/testing/performance-instrumentation.ts b/src/testing/performance-instrumentation.ts index 055fd37..13e0dab 100644 --- a/src/testing/performance-instrumentation.ts +++ b/src/testing/performance-instrumentation.ts @@ -30,6 +30,7 @@ export interface ScriptedActionSample { id: string; target: string; dispatchTime: number; + captureEndTime: number | null; acknowledgementCount: number; mutationTime: number | null; doubleRafTime: number | null; @@ -111,6 +112,7 @@ export const evaluateEventToNextPaint = ( entry.target === action.target && entry.startTime >= action.dispatchTime && entry.startTime <= action.dispatchTime + 500 + && (action.captureEndTime === null || entry.startTime < action.captureEndTime) )); if (matching.length === 0) return 16; const groups = new Map(); @@ -270,7 +272,9 @@ export const installAuthorizedPerformanceInstrumentation = async (): Promise= record.dispatchTime + && (record.captureEndTime === null || timed.startTime < record.captureEndTime)) { (record.eventEntries as EventTimingSample[]).push({ target: record.id, startTime: timed.startTime, @@ -360,10 +364,15 @@ export const installAuthorizedPerformanceInstrumentation = async (): Promise { id, target: `target-${id}`, dispatchTime: index * 1_000, + captureEndTime: null, acknowledgementCount: 1, mutationTime: index * 1_000 + 20, doubleRafTime: index * 1_000 + 40, @@ -74,6 +75,52 @@ describe('performance metric math', () => { expect(() => evaluateEventToNextPaint([{ ...actions[0]!, acknowledgementCount: 2 }, ...actions.slice(1)])).toThrow(/acknowledgement/u); }); + it('excludes a later compressed interaction when sequential actions share one DOM target', () => { + const ids = ['start', 'volume', 'mute', 'unmute', 'pause', 'resume', 'toolkit', 'contact'] as const; + const actions = ids.map((id, index): ScriptedActionSample => ({ + id, + target: id, + dispatchTime: index === 4 ? 400 : index === 5 ? 450 : index * 1_000, + captureEndTime: index === 4 ? 450 : null, + acknowledgementCount: 1, + mutationTime: (index === 4 ? 400 : index === 5 ? 450 : index * 1_000) + 5, + doubleRafTime: (index === 4 ? 400 : index === 5 ? 450 : index * 1_000) + 10, + eventTimingSupported: true, + eventEntries: id === 'pause' + ? [ + { target: 'pause', startTime: 410, duration: 30, interactionId: 41 }, + { target: 'pause', startTime: 460, duration: 40, interactionId: 42 }, + ] + : id === 'resume' + ? [{ target: 'resume', startTime: 460, duration: 40, interactionId: 42 }] + : [], + })); + + expect(evaluateEventToNextPaint(actions).values.slice(4, 6)).toEqual([30, 40]); + }); + + it('still rejects genuinely ambiguous interaction groups within one capture window', () => { + const ids = ['start', 'volume', 'mute', 'unmute', 'pause', 'resume', 'toolkit', 'contact'] as const; + const actions = ids.map((id, index): ScriptedActionSample => ({ + id, + target: id, + dispatchTime: index * 1_000, + captureEndTime: null, + acknowledgementCount: 1, + mutationTime: index * 1_000 + 5, + doubleRafTime: index * 1_000 + 10, + eventTimingSupported: true, + eventEntries: id === 'pause' + ? [ + { target: 'pause', startTime: 4_010, duration: 30, interactionId: 41 }, + { target: 'pause', startTime: 4_020, duration: 40, interactionId: 42 }, + ] + : [], + })); + + expect(() => evaluateEventToNextPaint(actions)).toThrow(/ambiguous/u); + }); + it('evaluates scene continuity at interval boundaries and callback coverage', () => { const result = evaluateSceneContinuity( [{ start: 0, end: 100 }],