@buoy-gg/highlight-updates 7.0.38 → 7.0.40

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.
Files changed (103) hide show
  1. package/lib/commonjs/highlight-updates/HighlightUpdatesOverlay.js +281 -1
  2. package/lib/commonjs/highlight-updates/components/HighlightFilterView.js +1474 -1
  3. package/lib/commonjs/highlight-updates/components/HighlightUpdatesModal.js +641 -1
  4. package/lib/commonjs/highlight-updates/components/IdentifierBadge.js +273 -1
  5. package/lib/commonjs/highlight-updates/components/IsolatedRenderList.js +188 -1
  6. package/lib/commonjs/highlight-updates/components/ModalHeaderContent.js +293 -1
  7. package/lib/commonjs/highlight-updates/components/RenderCauseBadge.js +508 -1
  8. package/lib/commonjs/highlight-updates/components/RenderDetailView.js +846 -1
  9. package/lib/commonjs/highlight-updates/components/RenderHistoryViewer.js +915 -1
  10. package/lib/commonjs/highlight-updates/components/RenderListItem.js +235 -1
  11. package/lib/commonjs/highlight-updates/components/RendersCopySettingsView.js +469 -1
  12. package/lib/commonjs/highlight-updates/components/StatsDisplay.js +73 -1
  13. package/lib/commonjs/highlight-updates/types/copySettings.js +107 -1
  14. package/lib/commonjs/highlight-updates/utils/CommitProfiler.js +446 -1
  15. package/lib/commonjs/highlight-updates/utils/HighlightUpdatesController.js +1920 -1
  16. package/lib/commonjs/highlight-updates/utils/PerformanceLogger.js +359 -1
  17. package/lib/commonjs/highlight-updates/utils/ProfilerInterceptor.js +371 -1
  18. package/lib/commonjs/highlight-updates/utils/RenderCauseDetector.js +1840 -1
  19. package/lib/commonjs/highlight-updates/utils/RenderTracker.js +952 -1
  20. package/lib/commonjs/highlight-updates/utils/ViewTypeMapper.js +264 -1
  21. package/lib/commonjs/highlight-updates/utils/copySettingsStorage.js +32 -1
  22. package/lib/commonjs/highlight-updates/utils/devtoolsComponentNames.js +188 -1
  23. package/lib/commonjs/highlight-updates/utils/namedComposite.js +41 -1
  24. package/lib/commonjs/highlight-updates/utils/ownStateChanged.js +103 -1
  25. package/lib/commonjs/highlight-updates/utils/rawEventEmitter.js +50 -1
  26. package/lib/commonjs/highlight-updates/utils/renderExportFormatter.js +58 -1
  27. package/lib/commonjs/highlight-updates/utils/rendersExportFormatter.js +485 -1
  28. package/lib/commonjs/highlight-updates/utils/screenElements.js +1248 -1
  29. package/lib/commonjs/highlight-updates/utils/tapMatch.js +83 -1
  30. package/lib/commonjs/highlight-updates/utils/testIDs.js +68 -1
  31. package/lib/commonjs/highlight-updates/utils/touchCapture.js +277 -1
  32. package/lib/commonjs/index.js +429 -1
  33. package/lib/commonjs/preset.js +278 -1
  34. package/lib/commonjs/sync/highlightUpdatesSyncAdapter.js +235 -1
  35. package/lib/module/highlight-updates/HighlightUpdatesOverlay.js +274 -1
  36. package/lib/module/highlight-updates/components/HighlightFilterView.js +1468 -1
  37. package/lib/module/highlight-updates/components/HighlightUpdatesModal.js +634 -1
  38. package/lib/module/highlight-updates/components/IdentifierBadge.js +265 -1
  39. package/lib/module/highlight-updates/components/IsolatedRenderList.js +183 -1
  40. package/lib/module/highlight-updates/components/ModalHeaderContent.js +288 -1
  41. package/lib/module/highlight-updates/components/RenderCauseBadge.js +499 -1
  42. package/lib/module/highlight-updates/components/RenderDetailView.js +840 -1
  43. package/lib/module/highlight-updates/components/RenderHistoryViewer.js +908 -1
  44. package/lib/module/highlight-updates/components/RenderListItem.js +230 -1
  45. package/lib/module/highlight-updates/components/RendersCopySettingsView.js +465 -1
  46. package/lib/module/highlight-updates/components/StatsDisplay.js +70 -1
  47. package/lib/module/highlight-updates/types/copySettings.js +102 -1
  48. package/lib/module/highlight-updates/utils/CommitProfiler.js +442 -1
  49. package/lib/module/highlight-updates/utils/HighlightUpdatesController.js +1916 -1
  50. package/lib/module/highlight-updates/utils/PerformanceLogger.js +353 -1
  51. package/lib/module/highlight-updates/utils/ProfilerInterceptor.js +358 -1
  52. package/lib/module/highlight-updates/utils/RenderCauseDetector.js +1830 -1
  53. package/lib/module/highlight-updates/utils/RenderTracker.js +949 -1
  54. package/lib/module/highlight-updates/utils/ViewTypeMapper.js +255 -1
  55. package/lib/module/highlight-updates/utils/copySettingsStorage.js +26 -1
  56. package/lib/module/highlight-updates/utils/devtoolsComponentNames.js +178 -1
  57. package/lib/module/highlight-updates/utils/namedComposite.js +36 -1
  58. package/lib/module/highlight-updates/utils/ownStateChanged.js +98 -1
  59. package/lib/module/highlight-updates/utils/rawEventEmitter.js +46 -1
  60. package/lib/module/highlight-updates/utils/renderExportFormatter.js +54 -1
  61. package/lib/module/highlight-updates/utils/rendersExportFormatter.js +478 -1
  62. package/lib/module/highlight-updates/utils/screenElements.js +1242 -1
  63. package/lib/module/highlight-updates/utils/tapMatch.js +76 -1
  64. package/lib/module/highlight-updates/utils/testIDs.js +60 -1
  65. package/lib/module/highlight-updates/utils/touchCapture.js +270 -1
  66. package/lib/module/index.js +130 -1
  67. package/lib/module/preset.js +272 -1
  68. package/lib/module/sync/highlightUpdatesSyncAdapter.js +230 -1
  69. package/lib/typescript/highlight-updates/HighlightUpdatesOverlay.d.ts.map +1 -0
  70. package/lib/typescript/highlight-updates/components/HighlightFilterView.d.ts.map +1 -0
  71. package/lib/typescript/highlight-updates/components/HighlightUpdatesModal.d.ts.map +1 -0
  72. package/lib/typescript/highlight-updates/components/IdentifierBadge.d.ts.map +1 -0
  73. package/lib/typescript/highlight-updates/components/IsolatedRenderList.d.ts.map +1 -0
  74. package/lib/typescript/highlight-updates/components/ModalHeaderContent.d.ts.map +1 -0
  75. package/lib/typescript/highlight-updates/components/RenderCauseBadge.d.ts.map +1 -0
  76. package/lib/typescript/highlight-updates/components/RenderDetailView.d.ts.map +1 -0
  77. package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts.map +1 -0
  78. package/lib/typescript/highlight-updates/components/RenderListItem.d.ts.map +1 -0
  79. package/lib/typescript/highlight-updates/components/RendersCopySettingsView.d.ts.map +1 -0
  80. package/lib/typescript/highlight-updates/components/StatsDisplay.d.ts.map +1 -0
  81. package/lib/typescript/highlight-updates/types/copySettings.d.ts.map +1 -0
  82. package/lib/typescript/highlight-updates/utils/CommitProfiler.d.ts.map +1 -0
  83. package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts.map +1 -0
  84. package/lib/typescript/highlight-updates/utils/PerformanceLogger.d.ts.map +1 -0
  85. package/lib/typescript/highlight-updates/utils/ProfilerInterceptor.d.ts.map +1 -0
  86. package/lib/typescript/highlight-updates/utils/RenderCauseDetector.d.ts.map +1 -0
  87. package/lib/typescript/highlight-updates/utils/RenderTracker.d.ts.map +1 -0
  88. package/lib/typescript/highlight-updates/utils/ViewTypeMapper.d.ts.map +1 -0
  89. package/lib/typescript/highlight-updates/utils/copySettingsStorage.d.ts.map +1 -0
  90. package/lib/typescript/highlight-updates/utils/devtoolsComponentNames.d.ts.map +1 -0
  91. package/lib/typescript/highlight-updates/utils/namedComposite.d.ts.map +1 -0
  92. package/lib/typescript/highlight-updates/utils/ownStateChanged.d.ts.map +1 -0
  93. package/lib/typescript/highlight-updates/utils/rawEventEmitter.d.ts.map +1 -0
  94. package/lib/typescript/highlight-updates/utils/renderExportFormatter.d.ts.map +1 -0
  95. package/lib/typescript/highlight-updates/utils/rendersExportFormatter.d.ts.map +1 -0
  96. package/lib/typescript/highlight-updates/utils/screenElements.d.ts.map +1 -0
  97. package/lib/typescript/highlight-updates/utils/tapMatch.d.ts.map +1 -0
  98. package/lib/typescript/highlight-updates/utils/testIDs.d.ts.map +1 -0
  99. package/lib/typescript/highlight-updates/utils/touchCapture.d.ts.map +1 -0
  100. package/lib/typescript/index.d.ts.map +1 -0
  101. package/lib/typescript/preset.d.ts.map +1 -0
  102. package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts.map +1 -0
  103. package/package.json +7 -7
@@ -1 +1,83 @@
1
- "use strict";function isOnScreenTap(e){return!!e&&e.x>=-.1&&e.x<=1.1&&e.y>=-.1&&e.y<=1.1}function isRectCenterOnScreen(e,t,r){if(!e||t<=0||r<=0)return!1;const s=e.x+e.width/2,a=e.y+e.height/2,n=.1*t,o=.1*r;return s>=-n&&s<=t+n&&a>=-o&&a<=r+o}function looseText(e){return String(e??"").toLowerCase().replace(/\s+/g," ").replace(/(\d)[,\u00a0\u202f](?=\d{3}\b)/g,"$1").trim()}function scoreTapMatch(e,t){if(null!=t.nativeTag&&e.nativeTag===t.nativeTag)return 1e3;const r=String(t.query??t.testID??"").trim().toLowerCase();if(!r)return 0;const s=[e.testID,e.label,e.text,e.name].filter(Boolean).map(e=>e.toLowerCase());let a=0;for(const e of s)e===r?a=Math.max(a,100):e.startsWith(r)?a=Math.max(a,50):e.includes(r)&&(a=Math.max(a,25));const n=looseText(r);if(n)for(const e of s){const t=looseText(e);t===n?a=Math.max(a,90):t.startsWith(n)?a=Math.max(a,45):t.includes(n)&&(a=Math.max(a,22))}return t.testID&&e.testID&&e.testID.toLowerCase()===t.testID.toLowerCase()&&(a=Math.max(a,200)),0===a?0:((e.testID??"").toLowerCase().startsWith("buoy-dial-")&&(a+=40),e.tap&&(isOnScreenTap(e.tap)?a+=20:a-=40),a)}Object.defineProperty(exports,"__esModule",{value:!0}),exports.isOnScreenTap=isOnScreenTap,exports.isRectCenterOnScreen=isRectCenterOnScreen,exports.looseText=looseText,exports.scoreTapMatch=scoreTapMatch;
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isOnScreenTap = isOnScreenTap;
7
+ exports.isRectCenterOnScreen = isRectCenterOnScreen;
8
+ exports.looseText = looseText;
9
+ exports.scoreTapMatch = scoreTapMatch;
10
+ /**
11
+ * Shared tap-target scoring for describe_screen / tap_element.
12
+ *
13
+ * Ranking used to be substring-on-testID only, so `tap: "ASSETS"` preferred
14
+ * `nav-assets-tool-test` (score 25) over a visible dial icon, and off-screen
15
+ * Expo Router screens (y > 1) stayed in the candidate list.
16
+ */
17
+
18
+ /** True when the measured center is inside the window (10% slack for home-indicator / portal hosts). */
19
+ function isOnScreenTap(tap) {
20
+ if (!tap) return false;
21
+ return tap.x >= -0.1 && tap.x <= 1.1 && tap.y >= -0.1 && tap.y <= 1.1;
22
+ }
23
+
24
+ /**
25
+ * Point-in-window test in *points* (measureInWindow coords), with slack so
26
+ * the floating menu and rows clipped at the home indicator still count.
27
+ * Previous Expo Router screens typically measure at y > 1.2× window height.
28
+ */
29
+ function isRectCenterOnScreen(rect, screenW, screenH) {
30
+ if (!rect || screenW <= 0 || screenH <= 0) return false;
31
+ const cx = rect.x + rect.width / 2;
32
+ const cy = rect.y + rect.height / 2;
33
+ const slackX = screenW * 0.1;
34
+ const slackY = screenH * 0.1;
35
+ return cx >= -slackX && cx <= screenW + slackX && cy >= -slackY && cy <= screenH + slackY;
36
+ }
37
+
38
+ /**
39
+ * The comparable form of a piece of text.
40
+ *
41
+ * "1,450", `1450` and "1,450 Points" are one request as far as a person is
42
+ * concerned. Measured on a live QA run: waits for BOTH "1,450" and 1450 failed
43
+ * against a balance that was on screen, because the thousands separator made
44
+ * every comparison miss. Lowercased, whitespace collapsed, digit-group
45
+ * separators dropped. A number arrives here as a number often enough that the
46
+ * coercion is not optional — `.trim()` on one is a crash.
47
+ */
48
+ function looseText(value) {
49
+ return String(value ?? "").toLowerCase().replace(/\s+/g, " ").replace(/(\d)[,\u00a0\u202f](?=\d{3}\b)/g, "$1").trim();
50
+ }
51
+ function scoreTapMatch(c, params) {
52
+ if (params.nativeTag != null && c.nativeTag === params.nativeTag) return 1000;
53
+ const q = String(params.query ?? params.testID ?? "").trim().toLowerCase();
54
+ if (!q) return 0;
55
+ const fields = [c.testID, c.label, c.text, c.name].filter(Boolean).map(s => s.toLowerCase());
56
+ let best = 0;
57
+ for (const f of fields) {
58
+ if (f === q) best = Math.max(best, 100);else if (f.startsWith(q)) best = Math.max(best, 50);else if (f.includes(q)) best = Math.max(best, 25);
59
+ }
60
+ // Same tiers on the loose form, one notch lower so an exact hit still wins
61
+ // when both match. Skipped when the loose query is empty, or `includes("")`
62
+ // would score every candidate on the screen.
63
+ const looseQuery = looseText(q);
64
+ if (looseQuery) {
65
+ for (const f of fields) {
66
+ const loose = looseText(f);
67
+ if (loose === looseQuery) best = Math.max(best, 90);else if (loose.startsWith(looseQuery)) best = Math.max(best, 45);else if (loose.includes(looseQuery)) best = Math.max(best, 22);
68
+ }
69
+ }
70
+ if (params.testID && c.testID && c.testID.toLowerCase() === params.testID.toLowerCase()) {
71
+ best = Math.max(best, 200);
72
+ }
73
+ if (best === 0) return 0;
74
+ const id = (c.testID ?? "").toLowerCase();
75
+ // Dial icons are the thing agents mean when they tap a tool name. (No
76
+ // penalties for app testID patterns — this ships in the library, and we
77
+ // don't know consumer apps' naming conventions.)
78
+ if (id.startsWith("buoy-dial-")) best += 40;
79
+ if (c.tap) {
80
+ if (isOnScreenTap(c.tap)) best += 20;else best -= 40;
81
+ }
82
+ return best;
83
+ }
@@ -1 +1,68 @@
1
- "use strict";function identifierSlug(e){return String(e).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unknown"}function renderRowTestID(e){return`buoy-highlight-row-${identifierSlug(e)}`}function detectedItemTestID(e,t){return`buoy-highlight-detected-${identifierSlug(e)}-${identifierSlug(t)}`}function patternChipTestID(e,t,i){return`buoy-highlight-${e}-chip-${identifierSlug(t)}-${identifierSlug(i)}`}function settingTestID(e){return`buoy-highlight-setting-${e}`}Object.defineProperty(exports,"__esModule",{value:!0}),exports.detectedItemTestID=detectedItemTestID,exports.identifierSlug=identifierSlug,exports.patternChipTestID=patternChipTestID,exports.renderRowTestID=renderRowTestID,exports.settingTestID=settingTestID;
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.detectedItemTestID = detectedItemTestID;
7
+ exports.identifierSlug = identifierSlug;
8
+ exports.patternChipTestID = patternChipTestID;
9
+ exports.renderRowTestID = renderRowTestID;
10
+ exports.settingTestID = settingTestID;
11
+ /**
12
+ * Stable e2e handles for the Render Highlighter (highlight-updates).
13
+ *
14
+ * This tool's list is the hardest thing in Buoy to address, because the thing
15
+ * it lists is a NATIVE VIEW and the only key it has for one is the fiber's
16
+ * `nativeTag` — a per-session counter that changes on every launch, on every
17
+ * remount, and (on a dynamic screen) between two runs of the same test. A
18
+ * handle built from it would name a different component every time.
19
+ *
20
+ * So rows are addressed by their IDENTIFIER, in exactly the priority the list
21
+ * item itself uses to label them: testID → nativeID → component name →
22
+ * accessibility label → nativeTag. That is the same string the developer sees
23
+ * on the row, it is stable as long as the component is, and it is what a test
24
+ * can name up front. `nativeTag` remains the last resort for an anonymous host
25
+ * view, which no test should be naming anyway.
26
+ *
27
+ * Slug rules are mirrored in `example-dev-build/utils/renderBench.js` so the
28
+ * fixture and the tool agree about what a row is called; the suite's
29
+ * `fixture.js` spot-checks that they still do.
30
+ */
31
+
32
+ /** Any identifier string → a testID-safe slug. */
33
+ function identifierSlug(value) {
34
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
35
+ }
36
+
37
+ /**
38
+ * `buoy-highlight-row-<slug>` for one row of the render list.
39
+ *
40
+ * Sub-handles hang off it: `-name`, `-count`, `-type`, `-cause`, `-identifier`,
41
+ * `-time`. The count is the tool's whole claim, so it gets its own handle
42
+ * rather than living inside the row's merged label.
43
+ */
44
+ function renderRowTestID(identifier) {
45
+ return `buoy-highlight-row-${identifierSlug(identifier)}`;
46
+ }
47
+
48
+ /**
49
+ * `buoy-highlight-detected-<type>-<slug>` for one chip in DETECTED ITEMS.
50
+ *
51
+ * The type is in the handle because the same string legitimately appears under
52
+ * two categories — a component called `Pressable` and a viewType called
53
+ * `Pressable` are different filters with the same name, and a suite that added
54
+ * the wrong one would still see "a pattern was added".
55
+ */
56
+ function detectedItemTestID(type, value) {
57
+ return `buoy-highlight-detected-${identifierSlug(type)}-${identifierSlug(value)}`;
58
+ }
59
+
60
+ /** `buoy-highlight-<mode>-chip-<type>-<slug>` for an active filter pattern. */
61
+ function patternChipTestID(mode, type, value) {
62
+ return `buoy-highlight-${mode}-chip-${identifierSlug(type)}-${identifierSlug(value)}`;
63
+ }
64
+
65
+ /** `buoy-highlight-setting-<key>` for a settings row's control. */
66
+ function settingTestID(key) {
67
+ return `buoy-highlight-setting-${key}`;
68
+ }
@@ -1 +1,277 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.clearTouchCapture=clearTouchCapture,exports.getTouchCaptureStatus=getTouchCaptureStatus,exports.readTouchCapture=readTouchCapture,exports.startTouchCapture=startTouchCapture,exports.stopTouchCapture=stopTouchCapture;var _screenElements=require("./screenElements"),_rawEventEmitter=require("./rawEventEmitter");const TAP_SLOP_PT=14,LONG_PRESS_MS=550,MAX_TAP_MS=4e3,TEXT_COALESCE_MS=8e3,MAX_RECORDS=600;let emitter=null,emitterResolved=!1,unavailableReason=null,subscriptions=[],status="idle",records=[],seq=0,pending=null;function getEmitter(){if(emitterResolved)return emitter;emitterResolved=!0;const t=(0,_rawEventEmitter.resolveRawEventEmitter)();return emitter=t.emitter,unavailableReason=t.reason,emitter}function num(t){return"number"==typeof t&&Number.isFinite(t)?t:null}function tagOf(t){const e=t?.nativeEvent;return e?num(e.target)??num(e.tag):null}function pointOf(t){const e=t?.nativeEvent;if(!e)return null;const n={x:num(e.pageX),y:num(e.pageY)};if(null!==n.x&&null!==n.y)return{x:n.x,y:n.y};for(const t of["changedTouches","touches"]){const n=e[t];if(Array.isArray(n)&&n.length){const t=n[0],e=num(t?.pageX),r=num(t?.pageY);if(null!==e&&null!==r)return{x:e,y:r}}}return null}function push(t){records.push({...t,seq:++seq}),records.length>600&&(records=records.slice(-600))}function onTouchStart(t){const e=tagOf(t);if(null===e)return;const n=pointOf(t);let r=null;try{r=(0,_screenElements.resolveTouchTarget)(e)}catch{}pending={tag:e,x:n?.x??0,y:n?.y??0,t:Date.now(),drift:0,target:r}}function onTouchMove(t){if(!pending)return;const e=pointOf(t);if(!e)return;const n=e.x-pending.x,r=e.y-pending.y;pending.drift=Math.max(pending.drift,Math.sqrt(n*n+r*r))}function onTouchEnd(t){const e=pending;if(pending=null,!e||!e.target)return;const n=pointOf(t);if(n){const t=n.x-e.x,r=n.y-e.y;e.drift=Math.max(e.drift,Math.sqrt(t*t+r*r))}if(e.drift>14)return;const r=Date.now()-e.t;r>4e3||"text"!==e.target.control&&"toggle"!==e.target.control&&"slider"!==e.target.control&&push({t:Date.now(),kind:r>=550?"longPress":"press",target:e.target})}function onTouchCancel(){pending=null}function onChange(t){const e=tagOf(t);if(null===e)return;const n=t.nativeEvent;let r,u;if("string"==typeof n.text)r="text",u=n.text;else if("boolean"==typeof n.value)r="toggle",u=n.value;else{if("number"!=typeof n.value)return;r="slider",u=n.value}const o=records[records.length-1];if(o&&o.kind===r&&o.target.nativeTag===e&&Date.now()-o.t<8e3)return o.value=u,void(o.t=Date.now());let a=null;try{a=(0,_screenElements.resolveTouchTarget)(e)}catch{}a&&push({t:Date.now(),kind:r,target:a,value:u})}function startTouchCapture(){if("capturing"===status)return getTouchCaptureStatus();const t=getEmitter();if(!t)return status="unavailable",getTouchCaptureStatus();try{subscriptions=[t.addListener("topTouchStart",onTouchStart),t.addListener("topTouchMove",onTouchMove),t.addListener("topTouchEnd",onTouchEnd),t.addListener("topTouchCancel",onTouchCancel),t.addListener("topChange",onChange),t.addListener("topValueChange",onChange),t.addListener("topSlidingComplete",onChange)],pending=null,status="capturing"}catch(t){subscriptions=[],status="unavailable",unavailableReason=t?.message??"addListener failed"}return getTouchCaptureStatus()}function stopTouchCapture(){subscriptions.forEach(t=>{try{t.remove()}catch{}}),subscriptions=[],pending=null,"capturing"===status&&(status="idle")}function clearTouchCapture(){records=[],seq=0}function getTouchCaptureStatus(){return{status:status,reason:"unavailable"===status?unavailableReason:null}}function readTouchCapture(t=0){return{...getTouchCaptureStatus(),records:records.filter(e=>e.seq>t),seq:seq}}
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.clearTouchCapture = clearTouchCapture;
7
+ exports.getTouchCaptureStatus = getTouchCaptureStatus;
8
+ exports.readTouchCapture = readTouchCapture;
9
+ exports.startTouchCapture = startTouchCapture;
10
+ exports.stopTouchCapture = stopTouchCapture;
11
+ var _screenElements = require("./screenElements");
12
+ var _rawEventEmitter = require("./rawEventEmitter");
13
+ /**
14
+ * touchCapture — what a HUMAN is doing to the app, as replayable intents.
15
+ *
16
+ * This is the recording half of the pair whose playback half is `tapElement`:
17
+ * it watches real finger input and names each interaction in exactly the
18
+ * vocabulary `tapElement` searches by, so a recorded macro can be replayed
19
+ * without a second guess about what "that button" meant.
20
+ *
21
+ * Why RawEventEmitter and not the obvious alternatives:
22
+ * - A capture-phase responder on a root View would be the textbook answer, but
23
+ * Buoy mounts as a SIBLING of the app tree, not a parent. There is no root to
24
+ * wrap without changing every host app's integration.
25
+ * - Patching `Pressability`, or rewriting `onPress` props on the fly, breaks the
26
+ * moment a component re-renders (props are rebuilt every render) and misses
27
+ * everything driven by react-native-gesture-handler anyway.
28
+ * - RawEventEmitter mirrors every dispatched event before React's plugins see
29
+ * it, needs zero app instrumentation, and is already proven in-tree by
30
+ * focus-inspector's TV focus observer.
31
+ *
32
+ * Three rules the recorder above depends on:
33
+ *
34
+ * THE TARGET IS RESOLVED AT touchStart, NOT touchEnd. A press handler can
35
+ * navigate synchronously, so resolving after the fact names an element on the
36
+ * screen the app moved TO. We resolve while the finger is still down and emit
37
+ * when it lifts.
38
+ *
39
+ * A DRAG IS NOT A TAP. Scrolling a list produces the same touchStart the
40
+ * button above it does. Movement past a threshold, or an explicit
41
+ * touchCancel when a ScrollView steals the responder, drops the record —
42
+ * otherwise every recording is half scroll noise.
43
+ *
44
+ * KEYSTROKES ARE NOT INTENTS. Typing "SAVE10" fires six change events; a
45
+ * macro step wants the finished string. Consecutive edits to the same input
46
+ * collapse into the latest value as they arrive.
47
+ */
48
+
49
+ /** Points of finger travel past which the gesture stops being a tap. */
50
+ const TAP_SLOP_PT = 14;
51
+ /** Hold time at or past which a tap is recorded as a long-press. */
52
+ const LONG_PRESS_MS = 550;
53
+ /** A tap that outlasts this was a gesture we don't model — drop it. */
54
+ const MAX_TAP_MS = 4_000;
55
+ /** Consecutive edits to the same input within this window are one intent. */
56
+ const TEXT_COALESCE_MS = 8_000;
57
+ /** Ring buffer size. A recording longer than this is not a macro anyway. */
58
+ const MAX_RECORDS = 600;
59
+ let emitter = null;
60
+ let emitterResolved = false;
61
+ let unavailableReason = null;
62
+ let subscriptions = [];
63
+ let status = "idle";
64
+ let records = [];
65
+ let seq = 0;
66
+ let pending = null;
67
+ function getEmitter() {
68
+ if (emitterResolved) return emitter;
69
+ emitterResolved = true;
70
+ const resolution = (0, _rawEventEmitter.resolveRawEventEmitter)();
71
+ emitter = resolution.emitter;
72
+ unavailableReason = resolution.reason;
73
+ return emitter;
74
+ }
75
+ function num(v) {
76
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
77
+ }
78
+
79
+ /** Fabric puts the touched view on `target`; `tag` is the older shape. */
80
+ function tagOf(event) {
81
+ const native = event?.nativeEvent;
82
+ if (!native) return null;
83
+ return num(native.target) ?? num(native.tag);
84
+ }
85
+
86
+ /**
87
+ * Touch coordinates. A touch event carries them at the top level on the
88
+ * synthesized event and inside `touches`/`changedTouches` on the raw one, and
89
+ * which of those is populated differs by platform and architecture — so read
90
+ * whichever answers rather than betting on one.
91
+ */
92
+ function pointOf(event) {
93
+ const native = event?.nativeEvent;
94
+ if (!native) return null;
95
+ const direct = {
96
+ x: num(native.pageX),
97
+ y: num(native.pageY)
98
+ };
99
+ if (direct.x !== null && direct.y !== null) return {
100
+ x: direct.x,
101
+ y: direct.y
102
+ };
103
+ for (const key of ["changedTouches", "touches"]) {
104
+ const list = native[key];
105
+ if (Array.isArray(list) && list.length) {
106
+ const first = list[0];
107
+ const x = num(first?.pageX);
108
+ const y = num(first?.pageY);
109
+ if (x !== null && y !== null) return {
110
+ x,
111
+ y
112
+ };
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ function push(record) {
118
+ records.push({
119
+ ...record,
120
+ seq: ++seq
121
+ });
122
+ if (records.length > MAX_RECORDS) records = records.slice(-MAX_RECORDS);
123
+ }
124
+ function onTouchStart(event) {
125
+ const tag = tagOf(event);
126
+ if (tag === null) return;
127
+ const point = pointOf(event);
128
+ let target = null;
129
+ try {
130
+ target = (0, _screenElements.resolveTouchTarget)(tag);
131
+ } catch {
132
+ // A fiber walk that throws must never break the app's touch handling.
133
+ }
134
+ pending = {
135
+ tag,
136
+ x: point?.x ?? 0,
137
+ y: point?.y ?? 0,
138
+ t: Date.now(),
139
+ drift: 0,
140
+ target
141
+ };
142
+ }
143
+ function onTouchMove(event) {
144
+ if (!pending) return;
145
+ const point = pointOf(event);
146
+ if (!point) return;
147
+ const dx = point.x - pending.x;
148
+ const dy = point.y - pending.y;
149
+ pending.drift = Math.max(pending.drift, Math.sqrt(dx * dx + dy * dy));
150
+ }
151
+ function onTouchEnd(event) {
152
+ const p = pending;
153
+ pending = null;
154
+ if (!p || !p.target) return;
155
+ const point = pointOf(event);
156
+ if (point) {
157
+ const dx = point.x - p.x;
158
+ const dy = point.y - p.y;
159
+ p.drift = Math.max(p.drift, Math.sqrt(dx * dx + dy * dy));
160
+ }
161
+ if (p.drift > TAP_SLOP_PT) return; // a drag, a scroll, a swipe — not a step
162
+
163
+ const held = Date.now() - p.t;
164
+ if (held > MAX_TAP_MS) return;
165
+
166
+ // A text input's tap is just "focus this field"; the intent is the text it
167
+ // ends up with, which arrives separately as a change event. Recording the
168
+ // focus tap too would put a step in every macro that does nothing on replay.
169
+ if (p.target.control === "text") return;
170
+ // Same for a toggle/slider: the tap and the value change are one intent, and
171
+ // the change event is the half that carries what it became.
172
+ if (p.target.control === "toggle" || p.target.control === "slider") return;
173
+ push({
174
+ t: Date.now(),
175
+ kind: held >= LONG_PRESS_MS ? "longPress" : "press",
176
+ target: p.target
177
+ });
178
+ }
179
+ function onTouchCancel() {
180
+ // A ScrollView (or any ancestor) taking the responder terminates the touch.
181
+ pending = null;
182
+ }
183
+
184
+ /**
185
+ * `topChange` is the shared channel for "this control's value became X" —
186
+ * TextInput sends `text`, Switch sends `value`. Sliders arrive on their own
187
+ * channel. All three are the same idea: a control the user set.
188
+ */
189
+ function onChange(event) {
190
+ const tag = tagOf(event);
191
+ if (tag === null) return;
192
+ const native = event.nativeEvent;
193
+ let kind;
194
+ let value;
195
+ if (typeof native.text === "string") {
196
+ kind = "text";
197
+ value = native.text;
198
+ } else if (typeof native.value === "boolean") {
199
+ kind = "toggle";
200
+ value = native.value;
201
+ } else if (typeof native.value === "number") {
202
+ kind = "slider";
203
+ value = native.value;
204
+ } else {
205
+ return;
206
+ }
207
+ const last = records[records.length - 1];
208
+ if (last && last.kind === kind && last.target.nativeTag === tag && Date.now() - last.t < TEXT_COALESCE_MS) {
209
+ // Same control, still typing/dragging: this IS the previous intent, further
210
+ // along. Replace rather than append, or "SAVE10" becomes six macro steps.
211
+ last.value = value;
212
+ last.t = Date.now();
213
+ return;
214
+ }
215
+ let target = null;
216
+ try {
217
+ target = (0, _screenElements.resolveTouchTarget)(tag);
218
+ } catch {
219
+ /* see onTouchStart */
220
+ }
221
+ if (!target) return;
222
+ push({
223
+ t: Date.now(),
224
+ kind,
225
+ target,
226
+ value
227
+ });
228
+ }
229
+ function startTouchCapture() {
230
+ if (status === "capturing") return getTouchCaptureStatus();
231
+ const raw = getEmitter();
232
+ if (!raw) {
233
+ status = "unavailable";
234
+ return getTouchCaptureStatus();
235
+ }
236
+ try {
237
+ subscriptions = [raw.addListener("topTouchStart", onTouchStart), raw.addListener("topTouchMove", onTouchMove), raw.addListener("topTouchEnd", onTouchEnd), raw.addListener("topTouchCancel", onTouchCancel), raw.addListener("topChange", onChange), raw.addListener("topValueChange", onChange), raw.addListener("topSlidingComplete", onChange)];
238
+ pending = null;
239
+ status = "capturing";
240
+ } catch (e) {
241
+ subscriptions = [];
242
+ status = "unavailable";
243
+ unavailableReason = e?.message ?? "addListener failed";
244
+ }
245
+ return getTouchCaptureStatus();
246
+ }
247
+ function stopTouchCapture() {
248
+ subscriptions.forEach(s => {
249
+ try {
250
+ s.remove();
251
+ } catch {
252
+ /* already gone is fine */
253
+ }
254
+ });
255
+ subscriptions = [];
256
+ pending = null;
257
+ if (status === "capturing") status = "idle";
258
+ }
259
+ function clearTouchCapture() {
260
+ records = [];
261
+ seq = 0;
262
+ }
263
+ function getTouchCaptureStatus() {
264
+ return {
265
+ status,
266
+ reason: status === "unavailable" ? unavailableReason : null
267
+ };
268
+ }
269
+
270
+ /** Everything recorded after `sinceSeq`, plus where the caller now is. */
271
+ function readTouchCapture(sinceSeq = 0) {
272
+ return {
273
+ ...getTouchCaptureStatus(),
274
+ records: records.filter(r => r.seq > sinceSeq),
275
+ seq
276
+ };
277
+ }