@treelocator/runtime 0.4.7 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,10 +47,15 @@ export interface DejitterConfig {
47
47
  outlier: {
48
48
  ratioThreshold: number;
49
49
  };
50
+ lag: {
51
+ minDelay: number;
52
+ highDelay: number;
53
+ medDelay: number;
54
+ };
50
55
  };
51
56
  }
52
57
  export interface DejitterFinding {
53
- type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier';
58
+ type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier' | 'lag';
54
59
  severity: 'high' | 'medium' | 'low' | 'info';
55
60
  elem: string;
56
61
  elemLabel: {
@@ -85,6 +90,7 @@ export interface DejitterAPI {
85
90
  getRaw(): {
86
91
  rawFrames: any[];
87
92
  mutations: any[];
93
+ interactions: any[];
88
94
  };
89
95
  toJSON(): string;
90
96
  }
@@ -48,6 +48,11 @@ export function createDejitterRecorder() {
48
48
  },
49
49
  outlier: {
50
50
  ratioThreshold: 3
51
+ },
52
+ lag: {
53
+ minDelay: 50,
54
+ highDelay: 200,
55
+ medDelay: 100
51
56
  }
52
57
  }
53
58
  };
@@ -59,12 +64,14 @@ export function createDejitterRecorder() {
59
64
  let rafId = null;
60
65
  let stopTimer = null;
61
66
  let mutationObserver = null;
67
+ let interactionAbort = null;
62
68
  let onStopCallbacks = [];
63
69
  let lastSeen = new Map();
64
70
  let rawFrames = [];
65
71
  let lastChangeTime = 0;
66
72
  let hasSeenChange = false;
67
73
  let mutations = [];
74
+ let interactions = [];
68
75
  let nextElemId = 0;
69
76
  function elemId(el) {
70
77
  if (!el.__dj_id) {
@@ -210,6 +217,24 @@ export function createDejitterRecorder() {
210
217
  characterData: true
211
218
  });
212
219
  }
220
+ function startInteractionListeners() {
221
+ interactionAbort = new AbortController();
222
+ const opts = {
223
+ capture: true,
224
+ signal: interactionAbort.signal
225
+ };
226
+ const handler = e => {
227
+ if (!recording) return;
228
+ const t = Math.round(performance.now() - startTime);
229
+ interactions.push({
230
+ t,
231
+ type: e.type
232
+ });
233
+ };
234
+ for (const evt of ['click', 'pointerdown', 'keydown']) {
235
+ document.addEventListener(evt, handler, opts);
236
+ }
237
+ }
213
238
 
214
239
  // --- Downsampling ---
215
240
 
@@ -736,6 +761,39 @@ export function createDejitterRecorder() {
736
761
  }
737
762
  return findings;
738
763
  }
764
+ function detectLagFindings(elements) {
765
+ const findings = [];
766
+ if (interactions.length === 0 || rawFrames.length === 0) return findings;
767
+ const lt = config.thresholds.lag;
768
+ for (const interaction of interactions) {
769
+ let firstFrame = null;
770
+ for (const frame of rawFrames) {
771
+ if (frame.t > interaction.t) {
772
+ firstFrame = frame;
773
+ break;
774
+ }
775
+ }
776
+ if (!firstFrame) continue;
777
+ const delay = firstFrame.t - interaction.t;
778
+ if (delay < lt.minDelay) continue;
779
+ const severity = delay >= lt.highDelay ? 'high' : delay >= lt.medDelay ? 'medium' : 'low';
780
+ const firstChange = firstFrame.changes[0];
781
+ const label = firstChange && elements[firstChange.id] || {
782
+ tag: '?',
783
+ cls: '',
784
+ text: ''
785
+ };
786
+ findings.push(makeFinding('lag', severity, firstChange?.id || '?', label, interaction.type, `${delay}ms between ${interaction.type} at t=${interaction.t}ms and first visual change at t=${firstFrame.t}ms`, {
787
+ lag: {
788
+ interactionType: interaction.type,
789
+ interactionT: interaction.t,
790
+ firstChangeT: firstFrame.t,
791
+ delay
792
+ }
793
+ }));
794
+ }
795
+ return findings;
796
+ }
739
797
  function deduplicateShivers(findings) {
740
798
  const shiverFindings = findings.filter(f => f.type === 'shiver');
741
799
  const otherFindings = findings.filter(f => f.type !== 'shiver');
@@ -769,6 +827,7 @@ export function createDejitterRecorder() {
769
827
  findings = findings.concat(detectJumpFindings(propStats, elements));
770
828
  findings = findings.concat(detectStutterFindings(propStats, elements));
771
829
  findings = findings.concat(detectStuckFindings(propStats, elements));
830
+ findings = findings.concat(detectLagFindings(elements));
772
831
  findings = deduplicateShivers(findings);
773
832
  const sevOrder = {
774
833
  high: 0,
@@ -810,6 +869,7 @@ export function createDejitterRecorder() {
810
869
  start() {
811
870
  rawFrames = [];
812
871
  mutations = [];
872
+ interactions = [];
813
873
  lastSeen = new Map();
814
874
  nextElemId = 0;
815
875
  recording = true;
@@ -818,6 +878,7 @@ export function createDejitterRecorder() {
818
878
  hasSeenChange = false;
819
879
  onStopCallbacks = [];
820
880
  startMutationObserver();
881
+ startInteractionListeners();
821
882
  rafId = requestAnimationFrame(loop);
822
883
  if (config.maxDuration > 0) {
823
884
  stopTimer = setTimeout(() => api.stop(), config.maxDuration);
@@ -830,6 +891,7 @@ export function createDejitterRecorder() {
830
891
  if (rafId) cancelAnimationFrame(rafId);
831
892
  if (stopTimer) clearTimeout(stopTimer);
832
893
  mutationObserver?.disconnect();
894
+ interactionAbort?.abort();
833
895
  const msg = `Stopped. ${rawFrames.length} raw frames, ${mutations.length} mutation events.`;
834
896
  for (const cb of onStopCallbacks) {
835
897
  try {
@@ -892,7 +954,8 @@ export function createDejitterRecorder() {
892
954
  getRaw() {
893
955
  return {
894
956
  rawFrames,
895
- mutations
957
+ mutations,
958
+ interactions
896
959
  };
897
960
  },
898
961
  toJSON() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treelocator/runtime",
3
- "version": "0.4.7",
3
+ "version": "0.5.2",
4
4
  "description": "TreeLocatorJS runtime for component ancestry tracking. Alt+click any element to copy its component tree to clipboard. Exposes window.__treelocator__ API for browser automation (Playwright, Puppeteer, Selenium, Cypress).",
5
5
  "keywords": [
6
6
  "locator",
@@ -20,11 +20,12 @@ export interface DejitterConfig {
20
20
  stutter: { velocityRatio: number; maxFrames: number; minVelocity: number };
21
21
  stuck: { minStillFrames: number; maxDelta: number; minSurroundingVelocity: number; highDuration: number; medDuration: number };
22
22
  outlier: { ratioThreshold: number };
23
+ lag: { minDelay: number; highDelay: number; medDelay: number };
23
24
  };
24
25
  }
25
26
 
26
27
  export interface DejitterFinding {
27
- type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier';
28
+ type: 'jitter' | 'flicker' | 'shiver' | 'jump' | 'stutter' | 'stuck' | 'outlier' | 'lag';
28
29
  severity: 'high' | 'medium' | 'low' | 'info';
29
30
  elem: string;
30
31
  elemLabel: { tag: string; cls: string; text: string };
@@ -50,7 +51,7 @@ export interface DejitterAPI {
50
51
  getData(): any;
51
52
  summary(json?: boolean): string | DejitterSummary;
52
53
  findings(json?: boolean): string | DejitterFinding[];
53
- getRaw(): { rawFrames: any[]; mutations: any[] };
54
+ getRaw(): { rawFrames: any[]; mutations: any[]; interactions: any[] };
54
55
  toJSON(): string;
55
56
  }
56
57
 
@@ -70,6 +71,7 @@ export function createDejitterRecorder(): DejitterAPI {
70
71
  stutter: { velocityRatio: 0.3, maxFrames: 3, minVelocity: 0.5 },
71
72
  stuck: { minStillFrames: 3, maxDelta: 0.5, minSurroundingVelocity: 1, highDuration: 500, medDuration: 200 },
72
73
  outlier: { ratioThreshold: 3 },
74
+ lag: { minDelay: 50, highDelay: 200, medDelay: 100 },
73
75
  },
74
76
  };
75
77
 
@@ -79,6 +81,7 @@ export function createDejitterRecorder(): DejitterAPI {
79
81
  let rafId: number | null = null;
80
82
  let stopTimer: ReturnType<typeof setTimeout> | null = null;
81
83
  let mutationObserver: MutationObserver | null = null;
84
+ let interactionAbort: AbortController | null = null;
82
85
  let onStopCallbacks: Array<() => void> = [];
83
86
 
84
87
  let lastSeen = new Map<string, Record<string, any>>();
@@ -86,6 +89,7 @@ export function createDejitterRecorder(): DejitterAPI {
86
89
  let lastChangeTime = 0;
87
90
  let hasSeenChange = false;
88
91
  let mutations: any[] = [];
92
+ let interactions: Array<{ t: number; type: string }> = [];
89
93
  let nextElemId = 0;
90
94
 
91
95
  function elemId(el: any): string {
@@ -212,6 +216,19 @@ export function createDejitterRecorder(): DejitterAPI {
212
216
  });
213
217
  }
214
218
 
219
+ function startInteractionListeners(): void {
220
+ interactionAbort = new AbortController();
221
+ const opts: AddEventListenerOptions & { signal: AbortSignal } = { capture: true, signal: interactionAbort.signal };
222
+ const handler = (e: Event): void => {
223
+ if (!recording) return;
224
+ const t = Math.round(performance.now() - startTime);
225
+ interactions.push({ t, type: e.type });
226
+ };
227
+ for (const evt of ['click', 'pointerdown', 'keydown'] as const) {
228
+ document.addEventListener(evt, handler, opts);
229
+ }
230
+ }
231
+
215
232
  // --- Downsampling ---
216
233
 
217
234
  function downsample(): any[] {
@@ -778,6 +795,49 @@ export function createDejitterRecorder(): DejitterAPI {
778
795
  return findings;
779
796
  }
780
797
 
798
+ function detectLagFindings(elements: Record<string, any>): DejitterFinding[] {
799
+ const findings: DejitterFinding[] = [];
800
+ if (interactions.length === 0 || rawFrames.length === 0) return findings;
801
+
802
+ const lt = config.thresholds.lag;
803
+
804
+ for (const interaction of interactions) {
805
+ let firstFrame: { t: number; changes: any[] } | null = null;
806
+ for (const frame of rawFrames) {
807
+ if (frame.t > interaction.t) {
808
+ firstFrame = frame;
809
+ break;
810
+ }
811
+ }
812
+
813
+ if (!firstFrame) continue;
814
+
815
+ const delay = firstFrame.t - interaction.t;
816
+ if (delay < lt.minDelay) continue;
817
+
818
+ const severity = delay >= lt.highDelay ? 'high' : delay >= lt.medDelay ? 'medium' : 'low';
819
+
820
+ const firstChange = firstFrame.changes[0];
821
+ const label = (firstChange && elements[firstChange.id]) || { tag: '?', cls: '', text: '' };
822
+
823
+ findings.push(makeFinding(
824
+ 'lag', severity,
825
+ firstChange?.id || '?', label, interaction.type,
826
+ `${delay}ms between ${interaction.type} at t=${interaction.t}ms and first visual change at t=${firstFrame.t}ms`,
827
+ {
828
+ lag: {
829
+ interactionType: interaction.type,
830
+ interactionT: interaction.t,
831
+ firstChangeT: firstFrame.t,
832
+ delay,
833
+ },
834
+ }
835
+ ));
836
+ }
837
+
838
+ return findings;
839
+ }
840
+
781
841
  function deduplicateShivers(findings: DejitterFinding[]): DejitterFinding[] {
782
842
  const shiverFindings = findings.filter((f) => f.type === 'shiver');
783
843
  const otherFindings = findings.filter((f) => f.type !== 'shiver');
@@ -814,6 +874,7 @@ export function createDejitterRecorder(): DejitterAPI {
814
874
  findings = findings.concat(detectJumpFindings(propStats, elements));
815
875
  findings = findings.concat(detectStutterFindings(propStats, elements));
816
876
  findings = findings.concat(detectStuckFindings(propStats, elements));
877
+ findings = findings.concat(detectLagFindings(elements));
817
878
  findings = deduplicateShivers(findings);
818
879
 
819
880
  const sevOrder: Record<string, number> = { high: 0, medium: 1, low: 2, info: 3 };
@@ -842,6 +903,7 @@ export function createDejitterRecorder(): DejitterAPI {
842
903
  start() {
843
904
  rawFrames = [];
844
905
  mutations = [];
906
+ interactions = [];
845
907
  lastSeen = new Map();
846
908
  nextElemId = 0;
847
909
  recording = true;
@@ -851,6 +913,7 @@ export function createDejitterRecorder(): DejitterAPI {
851
913
  onStopCallbacks = [];
852
914
 
853
915
  startMutationObserver();
916
+ startInteractionListeners();
854
917
  rafId = requestAnimationFrame(loop);
855
918
 
856
919
  if (config.maxDuration > 0) {
@@ -865,6 +928,7 @@ export function createDejitterRecorder(): DejitterAPI {
865
928
  if (rafId) cancelAnimationFrame(rafId);
866
929
  if (stopTimer) clearTimeout(stopTimer);
867
930
  mutationObserver?.disconnect();
931
+ interactionAbort?.abort();
868
932
 
869
933
  const msg = `Stopped. ${rawFrames.length} raw frames, ${mutations.length} mutation events.`;
870
934
 
@@ -921,7 +985,7 @@ export function createDejitterRecorder(): DejitterAPI {
921
985
  },
922
986
 
923
987
  getRaw() {
924
- return { rawFrames, mutations };
988
+ return { rawFrames, mutations, interactions };
925
989
  },
926
990
 
927
991
  toJSON() {