@treelocator/runtime 0.4.0 → 0.4.5
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.
- package/dist/_generated_styles.js +115 -15
- package/dist/adapters/getElementInfo.js +3 -17
- package/dist/adapters/getParentsPath.js +3 -33
- package/dist/adapters/getTree.js +3 -33
- package/dist/adapters/jsx/getJSXComponentBoundingBox.js +0 -1
- package/dist/adapters/jsx/jsxAdapter.js +0 -3
- package/dist/adapters/jsx/runtimeStore.js +0 -12
- package/dist/adapters/react/reactAdapter.js +0 -8
- package/dist/adapters/resolveAdapter.d.ts +8 -0
- package/dist/adapters/resolveAdapter.js +28 -0
- package/dist/adapters/vue/vueAdapter.js +0 -14
- package/dist/browserApi.d.ts +1 -1
- package/dist/browserApi.js +8 -10
- package/dist/components/MaybeOutline.d.ts +1 -0
- package/dist/components/MaybeOutline.js +38 -29
- package/dist/components/Outline.d.ts +1 -0
- package/dist/components/Outline.js +20 -16
- package/dist/components/Runtime.js +30 -18
- package/dist/dejitter/recorder.js +8 -13
- package/dist/functions/formatAncestryChain.d.ts +6 -0
- package/dist/functions/formatAncestryChain.js +11 -0
- package/dist/functions/formatAncestryChain.test.js +75 -1
- package/dist/functions/getUsableName.js +0 -21
- package/dist/output.css +10 -40
- package/dist/types/types.d.ts +1 -32
- package/package.json +1 -1
- package/src/_generated_styles.ts +115 -15
- package/src/adapters/getElementInfo.tsx +3 -23
- package/src/adapters/getParentsPath.tsx +3 -42
- package/src/adapters/getTree.tsx +3 -42
- package/src/adapters/jsx/getJSXComponentBoundingBox.ts +0 -1
- package/src/adapters/jsx/jsxAdapter.ts +0 -2
- package/src/adapters/jsx/runtimeStore.ts +0 -11
- package/src/adapters/react/reactAdapter.ts +0 -7
- package/src/adapters/resolveAdapter.ts +38 -0
- package/src/adapters/vue/vueAdapter.ts +0 -14
- package/src/browserApi.ts +9 -12
- package/src/components/MaybeOutline.tsx +4 -2
- package/src/components/Outline.tsx +2 -1
- package/src/components/Runtime.tsx +27 -18
- package/src/dejitter/recorder.ts +51 -56
- package/src/functions/formatAncestryChain.test.ts +47 -0
- package/src/functions/formatAncestryChain.ts +11 -0
- package/src/functions/getUsableName.ts +0 -21
- package/src/types/types.ts +1 -32
- package/src/adapters/react/fiberToSimple.tsx +0 -72
- package/src/adapters/react/gatherFiberRoots.tsx +0 -36
- package/src/adapters/react/makeFiberId.tsx +0 -19
- package/src/adapters/react/searchDevtoolsRenderersForClosestTarget.ts +0 -15
- package/src/components/Button.tsx +0 -14
- package/src/components/ComponentOutline.tsx +0 -98
- package/src/components/SimpleNodeOutline.tsx +0 -27
- package/src/components/Tooltip.tsx +0 -28
- package/src/functions/cropPath.test.ts +0 -18
- package/src/functions/cropPath.ts +0 -12
- package/src/functions/evalTemplate.test.ts +0 -12
- package/src/functions/evalTemplate.ts +0 -8
- package/src/functions/findNames.ts +0 -20
- package/src/functions/getBoundingRect.tsx +0 -11
- package/src/functions/getComposedBoundingBox.tsx +0 -25
- package/src/functions/getIdsOnPathToRoot.tsx +0 -21
- package/src/functions/getMultipleElementsBoundingBox.tsx +0 -25
- package/src/functions/getPathToParent.tsx +0 -17
- package/src/functions/getUsableFileName.test.tsx +0 -24
- package/src/functions/getUsableFileName.tsx +0 -19
- package/src/functions/transformPath.test.ts +0 -28
- package/src/functions/transformPath.ts +0 -7
|
@@ -7,7 +7,7 @@ import { Targets as SetupTargets } from "../types/types";
|
|
|
7
7
|
import { MaybeOutline } from "./MaybeOutline";
|
|
8
8
|
import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
|
|
9
9
|
import { Toast } from "./Toast";
|
|
10
|
-
import { collectAncestry, formatAncestryChain } from "../functions/formatAncestryChain";
|
|
10
|
+
import { collectAncestry, formatAncestryChain, truncateAtFirstFile } from "../functions/formatAncestryChain";
|
|
11
11
|
import { enrichAncestryWithSourceMaps } from "../functions/enrichAncestrySourceMaps";
|
|
12
12
|
import { createTreeNode } from "../adapters/createTreeNode";
|
|
13
13
|
import treeIconUrl from "../_generated_tree_icon";
|
|
@@ -15,6 +15,15 @@ import { createDejitterRecorder, DejitterAPI, DejitterFinding, DejitterSummary }
|
|
|
15
15
|
import { RecordingOutline } from "./RecordingOutline";
|
|
16
16
|
import { RecordingResults, InteractionEvent } from "./RecordingResults";
|
|
17
17
|
|
|
18
|
+
const DEJITTER_CONFIG: Partial<import("../dejitter/recorder").DejitterConfig> = {
|
|
19
|
+
selector: '[data-treelocator-recording]',
|
|
20
|
+
props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
|
|
21
|
+
sampleRate: 15,
|
|
22
|
+
maxDuration: 30000,
|
|
23
|
+
idleTimeout: 0,
|
|
24
|
+
mutations: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
type RuntimeProps = {
|
|
19
28
|
adapterId?: AdapterId;
|
|
20
29
|
targets: Targets;
|
|
@@ -22,6 +31,7 @@ type RuntimeProps = {
|
|
|
22
31
|
|
|
23
32
|
function Runtime(props: RuntimeProps) {
|
|
24
33
|
const [holdingModKey, setHoldingModKey] = createSignal<boolean>(false);
|
|
34
|
+
const [holdingShift, setHoldingShift] = createSignal<boolean>(false);
|
|
25
35
|
const [currentElement, setCurrentElement] = createSignal<HTMLElement | null>(
|
|
26
36
|
null
|
|
27
37
|
);
|
|
@@ -98,15 +108,18 @@ function Runtime(props: RuntimeProps) {
|
|
|
98
108
|
|
|
99
109
|
function keyUpListener(e: KeyboardEvent) {
|
|
100
110
|
setHoldingModKey(isCombinationModifiersPressed(e));
|
|
111
|
+
setHoldingShift(e.shiftKey);
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
function keyDownListener(e: KeyboardEvent) {
|
|
104
115
|
setHoldingModKey(isCombinationModifiersPressed(e, true));
|
|
116
|
+
setHoldingShift(e.shiftKey);
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
function mouseMoveListener(e: MouseEvent) {
|
|
108
120
|
// Update modifier state from mouse events - more reliable than keydown/keyup
|
|
109
121
|
setHoldingModKey(e.altKey);
|
|
122
|
+
setHoldingShift(e.shiftKey);
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
function findElementAtPoint(e: MouseEvent): HTMLElement | null {
|
|
@@ -155,14 +168,7 @@ function Runtime(props: RuntimeProps) {
|
|
|
155
168
|
setRecordedElement(element);
|
|
156
169
|
|
|
157
170
|
dejitterInstance = createDejitterRecorder();
|
|
158
|
-
dejitterInstance.configure(
|
|
159
|
-
selector: '[data-treelocator-recording]',
|
|
160
|
-
props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
|
|
161
|
-
sampleRate: 15,
|
|
162
|
-
maxDuration: 30000,
|
|
163
|
-
idleTimeout: 0,
|
|
164
|
-
mutations: true,
|
|
165
|
-
});
|
|
171
|
+
dejitterInstance.configure(DEJITTER_CONFIG);
|
|
166
172
|
dejitterInstance.start();
|
|
167
173
|
startInteractionTracker();
|
|
168
174
|
setRecordingState('recording');
|
|
@@ -288,14 +294,7 @@ function Runtime(props: RuntimeProps) {
|
|
|
288
294
|
setRecordedElement(element);
|
|
289
295
|
|
|
290
296
|
dejitterInstance = createDejitterRecorder();
|
|
291
|
-
dejitterInstance.configure(
|
|
292
|
-
selector: '[data-treelocator-recording]',
|
|
293
|
-
props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
|
|
294
|
-
sampleRate: 15,
|
|
295
|
-
maxDuration: 30000,
|
|
296
|
-
idleTimeout: 0,
|
|
297
|
-
mutations: true,
|
|
298
|
-
});
|
|
297
|
+
dejitterInstance.configure(DEJITTER_CONFIG);
|
|
299
298
|
dejitterInstance.start();
|
|
300
299
|
setRecordingState('recording');
|
|
301
300
|
setReplaying(true);
|
|
@@ -382,6 +381,7 @@ function Runtime(props: RuntimeProps) {
|
|
|
382
381
|
setRecordedElement(null);
|
|
383
382
|
setViewingPrevious(false);
|
|
384
383
|
setRecordingState('idle');
|
|
384
|
+
try { localStorage.removeItem(STORAGE_KEY); } catch {}
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
function hasPreviousRecording(): boolean {
|
|
@@ -443,6 +443,7 @@ function Runtime(props: RuntimeProps) {
|
|
|
443
443
|
|
|
444
444
|
function mouseOverListener(e: MouseEvent) {
|
|
445
445
|
setHoldingModKey(e.altKey);
|
|
446
|
+
setHoldingShift(e.shiftKey);
|
|
446
447
|
|
|
447
448
|
// Don't update hovered element while recording -- highlight is sticky
|
|
448
449
|
if (recordingState() === 'recording') return;
|
|
@@ -501,7 +502,12 @@ function Runtime(props: RuntimeProps) {
|
|
|
501
502
|
// Copy ancestry to clipboard on alt+click
|
|
502
503
|
const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
|
|
503
504
|
if (treeNode) {
|
|
504
|
-
|
|
505
|
+
let ancestry = collectAncestry(treeNode);
|
|
506
|
+
|
|
507
|
+
// Alt+Shift: keep from bottom up to the first element with a file, discard above
|
|
508
|
+
if (e.shiftKey) {
|
|
509
|
+
ancestry = truncateAtFirstFile(ancestry);
|
|
510
|
+
}
|
|
505
511
|
|
|
506
512
|
// Write immediately with component names (preserves user gesture for clipboard API)
|
|
507
513
|
const formatted = formatAncestryChain(ancestry);
|
|
@@ -564,6 +570,8 @@ function Runtime(props: RuntimeProps) {
|
|
|
564
570
|
}
|
|
565
571
|
|
|
566
572
|
onCleanup(() => {
|
|
573
|
+
stopReplay();
|
|
574
|
+
stopInteractionTracker();
|
|
567
575
|
for (const root of roots) {
|
|
568
576
|
root.removeEventListener("keyup", keyUpListener as EventListener);
|
|
569
577
|
root.removeEventListener("keydown", keyDownListener as EventListener);
|
|
@@ -596,6 +604,7 @@ function Runtime(props: RuntimeProps) {
|
|
|
596
604
|
currentElement={currentElement()!}
|
|
597
605
|
adapterId={props.adapterId}
|
|
598
606
|
targets={props.targets}
|
|
607
|
+
dashed={holdingShift()}
|
|
599
608
|
/>
|
|
600
609
|
) : null}
|
|
601
610
|
{recordingState() === 'recording' && recordedElement() ? (
|
package/src/dejitter/recorder.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
/**
|
|
3
2
|
* Dejitter — Animation frame recorder & jank detector (vendored)
|
|
4
3
|
*
|
|
@@ -218,13 +217,13 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
218
217
|
function downsample(): any[] {
|
|
219
218
|
if (rawFrames.length === 0) return [];
|
|
220
219
|
|
|
221
|
-
const duration = rawFrames[rawFrames.length - 1]
|
|
220
|
+
const duration = rawFrames[rawFrames.length - 1]!.t;
|
|
222
221
|
const targetFrames = Math.max(1, Math.round((duration / 1000) * config.sampleRate));
|
|
223
222
|
|
|
224
223
|
const changeIndex = new Map<string, Array<{ frameIdx: number; t: number; value: any }>>();
|
|
225
224
|
|
|
226
225
|
for (let fi = 0; fi < rawFrames.length; fi++) {
|
|
227
|
-
const frame = rawFrames[fi]
|
|
226
|
+
const frame = rawFrames[fi]!;
|
|
228
227
|
for (const change of frame.changes) {
|
|
229
228
|
const { id, ...props } = change;
|
|
230
229
|
for (const [prop, value] of Object.entries(props)) {
|
|
@@ -251,7 +250,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
251
250
|
} else {
|
|
252
251
|
for (let i = 0; i < targetFrames; i++) {
|
|
253
252
|
const srcIdx = Math.round((i / (targetFrames - 1)) * (changes.length - 1));
|
|
254
|
-
const c = changes[srcIdx]
|
|
253
|
+
const c = changes[srcIdx]!;
|
|
255
254
|
outputEvents.push({ t: c.t, id, prop, value: c.value });
|
|
256
255
|
}
|
|
257
256
|
}
|
|
@@ -296,7 +295,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
296
295
|
|
|
297
296
|
function buildPropStats(): { targetFrames: number; props: any[] } {
|
|
298
297
|
const stats: Record<string, any> = {};
|
|
299
|
-
const duration = rawFrames.length ? rawFrames[rawFrames.length - 1]
|
|
298
|
+
const duration = rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0;
|
|
300
299
|
const targetFrames = Math.max(1, Math.round((duration / 1000) * config.sampleRate));
|
|
301
300
|
|
|
302
301
|
for (const f of rawFrames) {
|
|
@@ -344,7 +343,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
344
343
|
if (value === 'none' || value === '' || value === 'auto') return 0;
|
|
345
344
|
const matrixMatch = String(value).match(/^matrix\(([^)]+)\)$/);
|
|
346
345
|
if (matrixMatch) {
|
|
347
|
-
const parts = matrixMatch[1]
|
|
346
|
+
const parts = matrixMatch[1]!.split(',').map(Number);
|
|
348
347
|
const tx = parts[4] || 0;
|
|
349
348
|
const ty = parts[5] || 0;
|
|
350
349
|
return Math.abs(tx) > Math.abs(ty) ? tx : ty;
|
|
@@ -356,8 +355,8 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
356
355
|
function detectBounce(timeline: Array<{ t: number; value: any }>): any | null {
|
|
357
356
|
if (timeline.length < 3) return null;
|
|
358
357
|
|
|
359
|
-
const first = timeline[0]
|
|
360
|
-
const last = timeline[timeline.length - 1]
|
|
358
|
+
const first = timeline[0]!.value;
|
|
359
|
+
const last = timeline[timeline.length - 1]!.value;
|
|
361
360
|
if (first !== last) return null;
|
|
362
361
|
|
|
363
362
|
const firstNum = extractNumeric(first);
|
|
@@ -384,9 +383,9 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
384
383
|
peak: peakValue,
|
|
385
384
|
peakDeviation: Math.round(peakDeviation * 10) / 10,
|
|
386
385
|
peakT,
|
|
387
|
-
startT: timeline[0]
|
|
388
|
-
endT: timeline[timeline.length - 1]
|
|
389
|
-
duration: timeline[timeline.length - 1]
|
|
386
|
+
startT: timeline[0]!.t,
|
|
387
|
+
endT: timeline[timeline.length - 1]!.t,
|
|
388
|
+
duration: timeline[timeline.length - 1]!.t - timeline[0]!.t,
|
|
390
389
|
};
|
|
391
390
|
}
|
|
392
391
|
|
|
@@ -394,7 +393,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
394
393
|
const byElem: Record<string, any[]> = {};
|
|
395
394
|
for (const p of propStats.props) {
|
|
396
395
|
if (!byElem[p.elem]) byElem[p.elem] = [];
|
|
397
|
-
byElem[p.elem]
|
|
396
|
+
byElem[p.elem]!.push(p);
|
|
398
397
|
}
|
|
399
398
|
|
|
400
399
|
const outliers: any[] = [];
|
|
@@ -427,8 +426,8 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
427
426
|
function countReversals(numeric: Array<{ t: number; val: number }>): number {
|
|
428
427
|
let reversals = 0;
|
|
429
428
|
for (let i = 2; i < numeric.length; i++) {
|
|
430
|
-
const d1 = numeric[i - 1]
|
|
431
|
-
const d2 = numeric[i]
|
|
429
|
+
const d1 = numeric[i - 1]!.val - numeric[i - 2]!.val;
|
|
430
|
+
const d2 = numeric[i]!.val - numeric[i - 1]!.val;
|
|
432
431
|
if (Math.abs(d1) > config.thresholds.shiver.minDelta && Math.abs(d2) > config.thresholds.shiver.minDelta && d1 * d2 < 0) {
|
|
433
432
|
reversals++;
|
|
434
433
|
}
|
|
@@ -485,12 +484,11 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
485
484
|
return findings;
|
|
486
485
|
}
|
|
487
486
|
|
|
488
|
-
function detectShiverFindings(propStats: any, elements: any
|
|
487
|
+
function detectShiverFindings(propStats: any, elements: any): DejitterFinding[] {
|
|
489
488
|
const findings: DejitterFinding[] = [];
|
|
490
489
|
|
|
491
490
|
for (const p of propStats.props) {
|
|
492
491
|
if (p.raw < 10) continue;
|
|
493
|
-
if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
|
|
494
492
|
|
|
495
493
|
const timeline = getTimeline(p.elem, p.prop);
|
|
496
494
|
if (timeline.length < 10) continue;
|
|
@@ -512,7 +510,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
512
510
|
|
|
513
511
|
const vals = numeric.map((n) => n.val);
|
|
514
512
|
const amplitude = Math.round((Math.max(...vals) - Math.min(...vals)) * 10) / 10;
|
|
515
|
-
const hz = Math.round((reversals / ((numeric[numeric.length - 1]
|
|
513
|
+
const hz = Math.round((reversals / ((numeric[numeric.length - 1]!.t - numeric[0]!.t) / 1000)) * 10) / 10;
|
|
516
514
|
|
|
517
515
|
findings.push(makeFinding(
|
|
518
516
|
'shiver',
|
|
@@ -532,7 +530,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
532
530
|
range: [Math.min(...vals), Math.max(...vals)],
|
|
533
531
|
uniqueValues: uniqueVals.length,
|
|
534
532
|
isTwoValueFight,
|
|
535
|
-
durationMs: Math.round(numeric[numeric.length - 1]
|
|
533
|
+
durationMs: Math.round(numeric[numeric.length - 1]!.t - numeric[0]!.t),
|
|
536
534
|
},
|
|
537
535
|
}
|
|
538
536
|
));
|
|
@@ -542,33 +540,32 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
542
540
|
return findings;
|
|
543
541
|
}
|
|
544
542
|
|
|
545
|
-
function detectJumpFindings(propStats: any, elements: any
|
|
543
|
+
function detectJumpFindings(propStats: any, elements: any): DejitterFinding[] {
|
|
546
544
|
const findings: DejitterFinding[] = [];
|
|
547
545
|
|
|
548
546
|
for (const p of propStats.props) {
|
|
549
547
|
if (p.raw < 3) continue;
|
|
550
|
-
if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
|
|
551
548
|
|
|
552
549
|
const timeline = getTimeline(p.elem, p.prop);
|
|
553
550
|
if (timeline.length < 3) continue;
|
|
554
551
|
|
|
555
552
|
const deltas: Array<{ t: number; delta: number; from: any; to: any }> = [];
|
|
556
553
|
for (let i = 1; i < timeline.length; i++) {
|
|
557
|
-
const prev = extractNumeric(timeline[i - 1]
|
|
558
|
-
const curr = extractNumeric(timeline[i]
|
|
554
|
+
const prev = extractNumeric(timeline[i - 1]!.value);
|
|
555
|
+
const curr = extractNumeric(timeline[i]!.value);
|
|
559
556
|
if (prev === null || curr === null) continue;
|
|
560
557
|
deltas.push({
|
|
561
|
-
t: timeline[i]
|
|
558
|
+
t: timeline[i]!.t,
|
|
562
559
|
delta: Math.abs(curr - prev),
|
|
563
|
-
from: timeline[i - 1]
|
|
564
|
-
to: timeline[i]
|
|
560
|
+
from: timeline[i - 1]!.value,
|
|
561
|
+
to: timeline[i]!.value,
|
|
565
562
|
});
|
|
566
563
|
}
|
|
567
564
|
|
|
568
565
|
if (deltas.length < 3) continue;
|
|
569
566
|
|
|
570
567
|
const sortedDeltas = deltas.map((d) => d.delta).sort((a, b) => a - b);
|
|
571
|
-
const medianDelta = sortedDeltas[Math.floor(sortedDeltas.length / 2)]
|
|
568
|
+
const medianDelta = sortedDeltas[Math.floor(sortedDeltas.length / 2)]!;
|
|
572
569
|
if (medianDelta === 0) continue;
|
|
573
570
|
|
|
574
571
|
const jmpT = config.thresholds.jump;
|
|
@@ -598,13 +595,12 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
598
595
|
return findings;
|
|
599
596
|
}
|
|
600
597
|
|
|
601
|
-
function detectStutterFindings(propStats: any, elements: any
|
|
598
|
+
function detectStutterFindings(propStats: any, elements: any): DejitterFinding[] {
|
|
602
599
|
const findings: DejitterFinding[] = [];
|
|
603
600
|
const st = config.thresholds.stutter;
|
|
604
601
|
|
|
605
602
|
for (const p of propStats.props) {
|
|
606
603
|
if (p.raw < 6) continue;
|
|
607
|
-
if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
|
|
608
604
|
|
|
609
605
|
const timeline = getTimeline(p.elem, p.prop);
|
|
610
606
|
if (timeline.length < 6) continue;
|
|
@@ -618,7 +614,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
618
614
|
|
|
619
615
|
const deltas: number[] = [];
|
|
620
616
|
for (let i = 1; i < numeric.length; i++) {
|
|
621
|
-
deltas.push(numeric[i]
|
|
617
|
+
deltas.push(numeric[i]!.val - numeric[i - 1]!.val);
|
|
622
618
|
}
|
|
623
619
|
|
|
624
620
|
const windowSize = 5;
|
|
@@ -628,31 +624,31 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
628
624
|
if (i - winStart < 2) { i++; continue; }
|
|
629
625
|
|
|
630
626
|
let sum = 0;
|
|
631
|
-
for (let w = winStart; w < i; w++) sum += deltas[w]
|
|
627
|
+
for (let w = winStart; w < i; w++) sum += deltas[w]!;
|
|
632
628
|
const dominantDir = Math.sign(sum);
|
|
633
629
|
if (dominantDir === 0) { i++; continue; }
|
|
634
630
|
|
|
635
|
-
if (deltas[i] !== 0 && Math.sign(deltas[i]) !== dominantDir) {
|
|
631
|
+
if (deltas[i]! !== 0 && Math.sign(deltas[i]!) !== dominantDir) {
|
|
636
632
|
const reversalStart = i;
|
|
637
633
|
let reversalEnd = i;
|
|
638
634
|
while (
|
|
639
635
|
reversalEnd + 1 < deltas.length &&
|
|
640
636
|
reversalEnd - reversalStart + 1 < st.maxFrames &&
|
|
641
|
-
deltas[reversalEnd + 1] !== 0 &&
|
|
642
|
-
Math.sign(deltas[reversalEnd + 1]) !== dominantDir
|
|
637
|
+
deltas[reversalEnd + 1]! !== 0 &&
|
|
638
|
+
Math.sign(deltas[reversalEnd + 1]!) !== dominantDir
|
|
643
639
|
) {
|
|
644
640
|
reversalEnd++;
|
|
645
641
|
}
|
|
646
642
|
|
|
647
643
|
const afterIdx = reversalEnd + 1;
|
|
648
|
-
if (afterIdx >= deltas.length || Math.sign(deltas[afterIdx]) !== dominantDir) {
|
|
644
|
+
if (afterIdx >= deltas.length || Math.sign(deltas[afterIdx]!) !== dominantDir) {
|
|
649
645
|
i = reversalEnd + 1;
|
|
650
646
|
continue;
|
|
651
647
|
}
|
|
652
648
|
|
|
653
649
|
let reversalMag = 0;
|
|
654
650
|
for (let r = reversalStart; r <= reversalEnd; r++) {
|
|
655
|
-
reversalMag += Math.abs(deltas[r]);
|
|
651
|
+
reversalMag += Math.abs(deltas[r]!);
|
|
656
652
|
}
|
|
657
653
|
|
|
658
654
|
const localStart = Math.max(0, reversalStart - windowSize);
|
|
@@ -661,7 +657,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
661
657
|
let localCount = 0;
|
|
662
658
|
for (let l = localStart; l <= localEnd; l++) {
|
|
663
659
|
if (l >= reversalStart && l <= reversalEnd) continue;
|
|
664
|
-
localSum += Math.abs(deltas[l]);
|
|
660
|
+
localSum += Math.abs(deltas[l]!);
|
|
665
661
|
localCount++;
|
|
666
662
|
}
|
|
667
663
|
const localVelocity = localCount > 0 ? localSum / localCount : 0;
|
|
@@ -671,7 +667,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
671
667
|
if (ratio >= st.velocityRatio) {
|
|
672
668
|
const reversalFrameCount = reversalEnd - reversalStart + 1;
|
|
673
669
|
const severity = ratio >= 1.0 ? 'high' : ratio >= 0.5 ? 'medium' : 'low';
|
|
674
|
-
const t = numeric[reversalStart + 1]
|
|
670
|
+
const t = numeric[reversalStart + 1]!.t;
|
|
675
671
|
findings.push(makeFinding(
|
|
676
672
|
'stutter', severity,
|
|
677
673
|
p.elem, elements[p.elem], p.prop,
|
|
@@ -701,13 +697,12 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
701
697
|
return findings;
|
|
702
698
|
}
|
|
703
699
|
|
|
704
|
-
function detectStuckFindings(propStats: any, elements: any
|
|
700
|
+
function detectStuckFindings(propStats: any, elements: any): DejitterFinding[] {
|
|
705
701
|
const findings: DejitterFinding[] = [];
|
|
706
702
|
const sk = config.thresholds.stuck;
|
|
707
703
|
|
|
708
704
|
for (const p of propStats.props) {
|
|
709
705
|
if (p.raw < 6) continue;
|
|
710
|
-
if (existingFindings.some((f) => f.elem === p.elem && f.prop === p.prop)) continue;
|
|
711
706
|
|
|
712
707
|
const timeline = getTimeline(p.elem, p.prop);
|
|
713
708
|
if (timeline.length < 6) continue;
|
|
@@ -721,15 +716,15 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
721
716
|
|
|
722
717
|
const deltas: number[] = [];
|
|
723
718
|
for (let i = 1; i < numeric.length; i++) {
|
|
724
|
-
deltas.push(Math.abs(numeric[i]
|
|
719
|
+
deltas.push(Math.abs(numeric[i]!.val - numeric[i - 1]!.val));
|
|
725
720
|
}
|
|
726
721
|
|
|
727
722
|
let i = 0;
|
|
728
723
|
while (i < deltas.length) {
|
|
729
|
-
if (deltas[i] > sk.maxDelta) { i++; continue; }
|
|
724
|
+
if (deltas[i]! > sk.maxDelta) { i++; continue; }
|
|
730
725
|
|
|
731
726
|
const runStart = i;
|
|
732
|
-
while (i < deltas.length && deltas[i] <= sk.maxDelta) i++;
|
|
727
|
+
while (i < deltas.length && deltas[i]! <= sk.maxDelta) i++;
|
|
733
728
|
const runEnd = i - 1;
|
|
734
729
|
const stillCount = runEnd - runStart + 1;
|
|
735
730
|
|
|
@@ -741,13 +736,13 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
741
736
|
|
|
742
737
|
const beforeStart = Math.max(0, runStart - windowSize);
|
|
743
738
|
for (let b = beforeStart; b < runStart; b++) {
|
|
744
|
-
surroundingSum += deltas[b]
|
|
739
|
+
surroundingSum += deltas[b]!;
|
|
745
740
|
surroundingCount++;
|
|
746
741
|
}
|
|
747
742
|
|
|
748
743
|
const afterEnd = Math.min(deltas.length - 1, runEnd + windowSize);
|
|
749
744
|
for (let a = runEnd + 1; a <= afterEnd; a++) {
|
|
750
|
-
surroundingSum += deltas[a]
|
|
745
|
+
surroundingSum += deltas[a]!;
|
|
751
746
|
surroundingCount++;
|
|
752
747
|
}
|
|
753
748
|
|
|
@@ -756,8 +751,8 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
756
751
|
|
|
757
752
|
if (meanSurroundingVelocity < sk.minSurroundingVelocity) continue;
|
|
758
753
|
|
|
759
|
-
const tStart = numeric[runStart]
|
|
760
|
-
const tEnd = numeric[runEnd + 1]
|
|
754
|
+
const tStart = numeric[runStart]!.t;
|
|
755
|
+
const tEnd = numeric[runEnd + 1]!.t;
|
|
761
756
|
const duration = Math.round(tEnd - tStart);
|
|
762
757
|
|
|
763
758
|
const severity = duration >= sk.highDuration ? 'high' : duration >= sk.medDuration ? 'medium' : 'low';
|
|
@@ -773,7 +768,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
773
768
|
duration,
|
|
774
769
|
stillFrames: stillCount,
|
|
775
770
|
meanSurroundingVelocity: Math.round(meanSurroundingVelocity * 10) / 10,
|
|
776
|
-
stuckValue: numeric[runStart]
|
|
771
|
+
stuckValue: numeric[runStart]!.val,
|
|
777
772
|
},
|
|
778
773
|
}
|
|
779
774
|
));
|
|
@@ -797,10 +792,10 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
797
792
|
const deduped: DejitterFinding[] = [];
|
|
798
793
|
for (const group of shiverGroups.values()) {
|
|
799
794
|
if (group.length === 1) {
|
|
800
|
-
deduped.push(group[0]);
|
|
795
|
+
deduped.push(group[0]!);
|
|
801
796
|
} else {
|
|
802
797
|
group.sort((a, b) => b.shiver.amplitude - a.shiver.amplitude);
|
|
803
|
-
const rep = { ...group[0] };
|
|
798
|
+
const rep = { ...group[0]! };
|
|
804
799
|
(rep as any).affectedElements = group.length;
|
|
805
800
|
rep.description += ` (affects ${group.length} elements)`;
|
|
806
801
|
deduped.push(rep);
|
|
@@ -815,14 +810,14 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
815
810
|
const elements = buildElementMap();
|
|
816
811
|
|
|
817
812
|
let findings = detectOutlierFindings(propStats, elements);
|
|
818
|
-
findings = findings.concat(detectShiverFindings(propStats, elements
|
|
819
|
-
findings = findings.concat(detectJumpFindings(propStats, elements
|
|
820
|
-
findings = findings.concat(detectStutterFindings(propStats, elements
|
|
821
|
-
findings = findings.concat(detectStuckFindings(propStats, elements
|
|
813
|
+
findings = findings.concat(detectShiverFindings(propStats, elements));
|
|
814
|
+
findings = findings.concat(detectJumpFindings(propStats, elements));
|
|
815
|
+
findings = findings.concat(detectStutterFindings(propStats, elements));
|
|
816
|
+
findings = findings.concat(detectStuckFindings(propStats, elements));
|
|
822
817
|
findings = deduplicateShivers(findings);
|
|
823
818
|
|
|
824
819
|
const sevOrder: Record<string, number> = { high: 0, medium: 1, low: 2, info: 3 };
|
|
825
|
-
findings.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
|
|
820
|
+
findings.sort((a, b) => sevOrder[a.severity]! - sevOrder[b.severity]!);
|
|
826
821
|
|
|
827
822
|
return findings;
|
|
828
823
|
}
|
|
@@ -891,7 +886,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
891
886
|
|
|
892
887
|
return {
|
|
893
888
|
config: { ...config },
|
|
894
|
-
duration: rawFrames.length ? rawFrames[rawFrames.length - 1]
|
|
889
|
+
duration: rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0,
|
|
895
890
|
rawFrameCount: rawFrames.length,
|
|
896
891
|
outputFrameCount: samples.length,
|
|
897
892
|
mutationEvents: mutations.length,
|
|
@@ -910,7 +905,7 @@ export function createDejitterRecorder(): DejitterAPI {
|
|
|
910
905
|
byMode[p.mode] = (byMode[p.mode] || 0) + 1;
|
|
911
906
|
}
|
|
912
907
|
const data: DejitterSummary = {
|
|
913
|
-
duration: rawFrames.length ? rawFrames[rawFrames.length - 1]
|
|
908
|
+
duration: rawFrames.length ? rawFrames[rawFrames.length - 1]!.t : 0,
|
|
914
909
|
rawFrameCount: rawFrames.length,
|
|
915
910
|
targetOutputFrames: propStats.targetFrames,
|
|
916
911
|
mutationEvents: mutations.length,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AncestryItem,
|
|
4
4
|
formatAncestryChain,
|
|
5
5
|
collectAncestry,
|
|
6
|
+
truncateAtFirstFile,
|
|
6
7
|
} from "./formatAncestryChain";
|
|
7
8
|
|
|
8
9
|
describe("formatAncestryChain", () => {
|
|
@@ -266,4 +267,50 @@ describe("formatAncestryChain", () => {
|
|
|
266
267
|
);
|
|
267
268
|
});
|
|
268
269
|
});
|
|
270
|
+
|
|
271
|
+
describe("truncateAtFirstFile", () => {
|
|
272
|
+
it("keeps from clicked element up to first ancestor with filePath", () => {
|
|
273
|
+
// Bottom-up: clicked element first, root last
|
|
274
|
+
const items: AncestryItem[] = [
|
|
275
|
+
{ elementName: "span", componentName: "Button" },
|
|
276
|
+
{ elementName: "div", componentName: "Card" },
|
|
277
|
+
{ elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
|
|
278
|
+
{ elementName: "div", componentName: "App", filePath: "src/App.tsx", line: 1 },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const result = truncateAtFirstFile(items);
|
|
282
|
+
expect(result).toEqual([
|
|
283
|
+
{ elementName: "span", componentName: "Button" },
|
|
284
|
+
{ elementName: "div", componentName: "Card" },
|
|
285
|
+
{ elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
|
|
286
|
+
]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("returns just the clicked element when it already has a filePath", () => {
|
|
290
|
+
const items: AncestryItem[] = [
|
|
291
|
+
{ elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
|
|
292
|
+
{ elementName: "div", componentName: "Layout", filePath: "src/Layout.tsx", line: 10 },
|
|
293
|
+
{ elementName: "div", componentName: "App", filePath: "src/App.tsx", line: 1 },
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const result = truncateAtFirstFile(items);
|
|
297
|
+
expect(result).toEqual([
|
|
298
|
+
{ elementName: "button", componentName: "Button", filePath: "src/Button.tsx", line: 5 },
|
|
299
|
+
]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("returns all items when none have a filePath", () => {
|
|
303
|
+
const items: AncestryItem[] = [
|
|
304
|
+
{ elementName: "span", componentName: "A" },
|
|
305
|
+
{ elementName: "div", componentName: "B" },
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const result = truncateAtFirstFile(items);
|
|
309
|
+
expect(result).toEqual(items);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("returns empty array for empty input", () => {
|
|
313
|
+
expect(truncateAtFirstFile([])).toEqual([]);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
269
316
|
});
|
|
@@ -164,6 +164,17 @@ function getInnermostNamedComponent(item: AncestryItem | null | undefined): stri
|
|
|
164
164
|
return undefined;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Truncate ancestry from the bottom (clicked element) up to and including
|
|
169
|
+
* the first item that has a filePath. Everything above that is discarded.
|
|
170
|
+
* The ancestry array is bottom-up: index 0 = clicked element, last = root.
|
|
171
|
+
*/
|
|
172
|
+
export function truncateAtFirstFile(items: AncestryItem[]): AncestryItem[] {
|
|
173
|
+
const firstWithFile = items.findIndex((item) => item.filePath);
|
|
174
|
+
if (firstWithFile === -1) return items;
|
|
175
|
+
return items.slice(0, firstWithFile + 1);
|
|
176
|
+
}
|
|
177
|
+
|
|
167
178
|
export function formatAncestryChain(items: AncestryItem[]): string {
|
|
168
179
|
if (items.length === 0) {
|
|
169
180
|
return "";
|
|
@@ -1,26 +1,5 @@
|
|
|
1
1
|
import { Fiber } from "@locator/shared";
|
|
2
2
|
|
|
3
|
-
// function printDebugOwnerTree(fiber: Fiber): string | null {
|
|
4
|
-
// let current: Fiber | null = fiber || null;
|
|
5
|
-
// let results = [];
|
|
6
|
-
// while (current) {
|
|
7
|
-
// results.push(getUsableName(current));
|
|
8
|
-
// current = current._debugOwner || null;
|
|
9
|
-
// }
|
|
10
|
-
// console.log('DEBUG OWNER: ', results);
|
|
11
|
-
// return null;
|
|
12
|
-
// }
|
|
13
|
-
// function printReturnTree(fiber: Fiber): string | null {
|
|
14
|
-
// let current: Fiber | null = fiber || null;
|
|
15
|
-
// let results = [];
|
|
16
|
-
// while (current) {
|
|
17
|
-
// results.push(getUsableName(current));
|
|
18
|
-
// current = current.return || null;
|
|
19
|
-
// }
|
|
20
|
-
// console.log('RETURN: ', results);
|
|
21
|
-
// return null;
|
|
22
|
-
// }
|
|
23
|
-
|
|
24
3
|
export function getUsableName(fiber: Fiber | null | undefined): string {
|
|
25
4
|
if (!fiber) {
|
|
26
5
|
return "Not found";
|
package/src/types/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Target } from "@locator/shared";
|
|
2
2
|
|
|
3
3
|
export type Source = {
|
|
4
4
|
fileName: string;
|
|
@@ -7,35 +7,6 @@ export type Source = {
|
|
|
7
7
|
projectPath?: string;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
type SimpleElement = {
|
|
11
|
-
type: "element";
|
|
12
|
-
name: string;
|
|
13
|
-
uniqueId: string;
|
|
14
|
-
fiber: Fiber;
|
|
15
|
-
box: SimpleDOMRect | null;
|
|
16
|
-
element: Element | Text;
|
|
17
|
-
children: (SimpleElement | SimpleComponent)[];
|
|
18
|
-
source: Source | null;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type SimpleComponent = {
|
|
22
|
-
type: "component";
|
|
23
|
-
uniqueId: string;
|
|
24
|
-
name: string;
|
|
25
|
-
fiber: Fiber;
|
|
26
|
-
box: SimpleDOMRect | null;
|
|
27
|
-
children: (SimpleElement | SimpleComponent)[];
|
|
28
|
-
source: Source | null;
|
|
29
|
-
definitionSourceFile: string | null;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type SimpleNode = SimpleElement | SimpleComponent;
|
|
33
|
-
|
|
34
|
-
export type HighlightedNode = {
|
|
35
|
-
getNode: () => SimpleNode | null;
|
|
36
|
-
setNode: (node: SimpleNode | null) => void;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
10
|
export type SimpleDOMRect = {
|
|
40
11
|
height: number;
|
|
41
12
|
width: number;
|
|
@@ -51,5 +22,3 @@ export type LinkProps = {
|
|
|
51
22
|
line: number;
|
|
52
23
|
column: number;
|
|
53
24
|
};
|
|
54
|
-
|
|
55
|
-
export type ContextMenuState = { target: HTMLElement; x: number; y: number };
|