@buoy-gg/highlight-updates 7.0.28 → 7.0.29

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 (57) hide show
  1. package/lib/commonjs/highlight-updates/components/HighlightFilterView.js +80 -0
  2. package/lib/commonjs/highlight-updates/components/HighlightUpdatesModal.js +5 -1
  3. package/lib/commonjs/highlight-updates/components/IdentifierBadge.js +7 -1
  4. package/lib/commonjs/highlight-updates/components/IsolatedRenderList.js +6 -0
  5. package/lib/commonjs/highlight-updates/components/ModalHeaderContent.js +26 -3
  6. package/lib/commonjs/highlight-updates/components/RenderCauseBadge.js +7 -1
  7. package/lib/commonjs/highlight-updates/components/RenderDetailView.js +40 -1
  8. package/lib/commonjs/highlight-updates/components/RenderHistoryViewer.js +29 -6
  9. package/lib/commonjs/highlight-updates/components/RenderListItem.js +15 -0
  10. package/lib/commonjs/highlight-updates/components/RendersCopySettingsView.js +11 -0
  11. package/lib/commonjs/highlight-updates/components/StatsDisplay.js +3 -0
  12. package/lib/commonjs/highlight-updates/utils/CommitProfiler.js +6 -1
  13. package/lib/commonjs/highlight-updates/utils/HighlightUpdatesController.js +10 -0
  14. package/lib/commonjs/highlight-updates/utils/RenderCauseDetector.js +15 -3
  15. package/lib/commonjs/highlight-updates/utils/ownStateChanged.js +103 -0
  16. package/lib/commonjs/highlight-updates/utils/testIDs.js +68 -0
  17. package/lib/commonjs/index.js +34 -0
  18. package/lib/module/highlight-updates/components/HighlightFilterView.js +80 -0
  19. package/lib/module/highlight-updates/components/HighlightUpdatesModal.js +5 -1
  20. package/lib/module/highlight-updates/components/IdentifierBadge.js +7 -1
  21. package/lib/module/highlight-updates/components/IsolatedRenderList.js +6 -0
  22. package/lib/module/highlight-updates/components/ModalHeaderContent.js +26 -3
  23. package/lib/module/highlight-updates/components/RenderCauseBadge.js +7 -1
  24. package/lib/module/highlight-updates/components/RenderDetailView.js +40 -1
  25. package/lib/module/highlight-updates/components/RenderHistoryViewer.js +29 -6
  26. package/lib/module/highlight-updates/components/RenderListItem.js +15 -0
  27. package/lib/module/highlight-updates/components/RendersCopySettingsView.js +11 -0
  28. package/lib/module/highlight-updates/components/StatsDisplay.js +3 -0
  29. package/lib/module/highlight-updates/utils/CommitProfiler.js +5 -1
  30. package/lib/module/highlight-updates/utils/HighlightUpdatesController.js +10 -0
  31. package/lib/module/highlight-updates/utils/RenderCauseDetector.js +15 -3
  32. package/lib/module/highlight-updates/utils/ownStateChanged.js +98 -0
  33. package/lib/module/highlight-updates/utils/testIDs.js +60 -0
  34. package/lib/module/index.js +5 -1
  35. package/lib/typescript/highlight-updates/components/HighlightFilterView.d.ts.map +1 -1
  36. package/lib/typescript/highlight-updates/components/HighlightUpdatesModal.d.ts.map +1 -1
  37. package/lib/typescript/highlight-updates/components/IdentifierBadge.d.ts +7 -1
  38. package/lib/typescript/highlight-updates/components/IdentifierBadge.d.ts.map +1 -1
  39. package/lib/typescript/highlight-updates/components/IsolatedRenderList.d.ts.map +1 -1
  40. package/lib/typescript/highlight-updates/components/ModalHeaderContent.d.ts.map +1 -1
  41. package/lib/typescript/highlight-updates/components/RenderCauseBadge.d.ts +8 -1
  42. package/lib/typescript/highlight-updates/components/RenderCauseBadge.d.ts.map +1 -1
  43. package/lib/typescript/highlight-updates/components/RenderDetailView.d.ts.map +1 -1
  44. package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts.map +1 -1
  45. package/lib/typescript/highlight-updates/components/RenderListItem.d.ts.map +1 -1
  46. package/lib/typescript/highlight-updates/components/RendersCopySettingsView.d.ts.map +1 -1
  47. package/lib/typescript/highlight-updates/components/StatsDisplay.d.ts.map +1 -1
  48. package/lib/typescript/highlight-updates/utils/CommitProfiler.d.ts.map +1 -1
  49. package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts.map +1 -1
  50. package/lib/typescript/highlight-updates/utils/RenderCauseDetector.d.ts.map +1 -1
  51. package/lib/typescript/highlight-updates/utils/ownStateChanged.d.ts +66 -0
  52. package/lib/typescript/highlight-updates/utils/ownStateChanged.d.ts.map +1 -0
  53. package/lib/typescript/highlight-updates/utils/testIDs.d.ts +44 -0
  54. package/lib/typescript/highlight-updates/utils/testIDs.d.ts.map +1 -0
  55. package/lib/typescript/index.d.ts +1 -0
  56. package/lib/typescript/index.d.ts.map +1 -1
  57. package/package.json +7 -7
@@ -95,21 +95,25 @@ function RenderDetailView({
95
95
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
96
96
  style: styles.headerLeft,
97
97
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
98
+ testID: "buoy-highlight-detail-name",
98
99
  style: styles.componentName,
99
100
  numberOfLines: 1,
100
101
  children: componentName
101
102
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
102
103
  style: styles.nativeTypeBadge,
103
104
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
105
+ testID: "buoy-highlight-detail-type",
104
106
  style: styles.nativeTypeText,
105
107
  children: nativeType
106
108
  })
107
109
  })]
108
110
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CopyButton, {
109
111
  value: copyData,
110
- size: 14
112
+ size: 14,
113
+ testID: "buoy-highlight-detail-copy"
111
114
  })]
112
115
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
116
+ testID: "buoy-highlight-detail-scroll",
113
117
  style: styles.scrollView,
114
118
  contentContainerStyle: [styles.scrollContent, showHistoryFooter && {
115
119
  paddingBottom: 100
@@ -122,11 +126,13 @@ function RenderDetailView({
122
126
  render: render,
123
127
  rendersPerSec: rendersPerSec
124
128
  }), hiddenEventCount > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
129
+ testID: "buoy-highlight-detail-locked",
125
130
  style: styles.lockedBanner,
126
131
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
127
132
  size: 14,
128
133
  color: _sharedUi.night.color.warning
129
134
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
135
+ testID: "buoy-highlight-detail-locked-text",
130
136
  style: styles.lockedBannerText,
131
137
  children: [hiddenEventCount, " ", hiddenEventCount === 1 ? 'event' : 'events', " hidden"]
132
138
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -171,10 +177,12 @@ function AnswerCard({
171
177
  return "Changed:";
172
178
  };
173
179
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
180
+ testID: "buoy-highlight-detail-cause",
174
181
  style: styles.answerCard,
175
182
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
176
183
  style: styles.causeBadgeRow,
177
184
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
185
+ testID: "buoy-highlight-detail-cause-label",
178
186
  style: styles.causeLabel,
179
187
  children: getLabel()
180
188
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -182,6 +190,7 @@ function AnswerCard({
182
190
  backgroundColor: config.color + "20"
183
191
  }],
184
192
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
193
+ testID: "buoy-highlight-detail-cause-badge",
185
194
  style: [styles.causeBadgeLargeText, {
186
195
  color: config.color
187
196
  }],
@@ -213,6 +222,7 @@ function AnswerCard({
213
222
  children: cause.changedKeys.filter(k => !k.includes("(ref only)")).slice(0, 5).map((key, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
214
223
  style: styles.propChangeChip,
215
224
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
225
+ testID: `buoy-highlight-detail-changed-${index}`,
216
226
  style: styles.propChangeText,
217
227
  children: key
218
228
  })
@@ -234,6 +244,7 @@ function DetailsSection({
234
244
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
235
245
  style: styles.detailsSection,
236
246
  children: [hasIdentifiers && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
247
+ testID: "buoy-highlight-detail-identifiers",
237
248
  style: styles.detailsCard,
238
249
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
239
250
  style: styles.detailsCardTitle,
@@ -249,10 +260,13 @@ function DetailsSection({
249
260
  value: render.accessibilityLabel
250
261
  })]
251
262
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
263
+ testID: "buoy-highlight-detail-stats",
252
264
  style: styles.statsRow,
253
265
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
266
+ testID: "buoy-highlight-detail-stat-renders",
254
267
  style: styles.statItem,
255
268
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
269
+ testID: "buoy-highlight-detail-stat-renders-value",
256
270
  style: [styles.statValue, {
257
271
  color: render.color
258
272
  }],
@@ -264,8 +278,10 @@ function DetailsSection({
264
278
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
265
279
  style: styles.statDivider
266
280
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
281
+ testID: "buoy-highlight-detail-stat-rate",
267
282
  style: styles.statItem,
268
283
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
284
+ testID: "buoy-highlight-detail-stat-rate-value",
269
285
  style: styles.statValue,
270
286
  children: rendersPerSec
271
287
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -275,8 +291,10 @@ function DetailsSection({
275
291
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
276
292
  style: styles.statDivider
277
293
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
294
+ testID: "buoy-highlight-detail-stat-tag",
278
295
  style: styles.statItem,
279
296
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
297
+ testID: "buoy-highlight-detail-stat-tag-value",
280
298
  style: styles.statValue,
281
299
  children: render.nativeTag
282
300
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -285,6 +303,7 @@ function DetailsSection({
285
303
  })]
286
304
  })]
287
305
  }), hasMeasurements && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
306
+ testID: "buoy-highlight-detail-measure",
288
307
  style: styles.measurementsRow,
289
308
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
290
309
  style: styles.measurementItem,
@@ -292,6 +311,7 @@ function DetailsSection({
292
311
  style: styles.measurementLabel,
293
312
  children: "x"
294
313
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
314
+ testID: "buoy-highlight-detail-measure-x",
295
315
  style: styles.measurementValue,
296
316
  children: Math.round(render.measurements.x)
297
317
  })]
@@ -301,6 +321,7 @@ function DetailsSection({
301
321
  style: styles.measurementLabel,
302
322
  children: "y"
303
323
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
324
+ testID: "buoy-highlight-detail-measure-y",
304
325
  style: styles.measurementValue,
305
326
  children: Math.round(render.measurements.y)
306
327
  })]
@@ -310,6 +331,7 @@ function DetailsSection({
310
331
  style: styles.measurementLabel,
311
332
  children: "w"
312
333
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
334
+ testID: "buoy-highlight-detail-measure-w",
313
335
  style: styles.measurementValue,
314
336
  children: Math.round(render.measurements.width)
315
337
  })]
@@ -319,6 +341,7 @@ function DetailsSection({
319
341
  style: styles.measurementLabel,
320
342
  children: "h"
321
343
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
344
+ testID: "buoy-highlight-detail-measure-h",
322
345
  style: styles.measurementValue,
323
346
  children: Math.round(render.measurements.height)
324
347
  })]
@@ -334,12 +357,15 @@ function DetailRow({
334
357
  label,
335
358
  value
336
359
  }) {
360
+ const handle = `buoy-highlight-detail-id-${label.toLowerCase()}`;
337
361
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
362
+ testID: handle,
338
363
  style: styles.detailRow,
339
364
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
340
365
  style: styles.detailLabel,
341
366
  children: label
342
367
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
368
+ testID: `${handle}-value`,
343
369
  style: styles.detailValue,
344
370
  numberOfLines: 1,
345
371
  children: value
@@ -421,6 +447,7 @@ function QuickActionsSection({
421
447
  }
422
448
  }, [selectedOption, onAddFilter]);
423
449
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
450
+ testID: "buoy-highlight-quick",
424
451
  style: styles.quickActionsSection,
425
452
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
426
453
  style: styles.quickActionsTitle,
@@ -428,6 +455,7 @@ function QuickActionsSection({
428
455
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
429
456
  style: styles.filterOptionsList,
430
457
  children: filterOptions.map((option, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(FilterOptionCard, {
458
+ testID: `buoy-highlight-quick-${option.type.toLowerCase()}`,
431
459
  label: option.label,
432
460
  value: option.value,
433
461
  isSelected: selectedIndex === index,
@@ -436,6 +464,9 @@ function QuickActionsSection({
436
464
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
437
465
  style: styles.filterActionButtons,
438
466
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
467
+ testID: "buoy-highlight-quick-include",
468
+ accessibilityRole: "button",
469
+ accessibilityLabel: "Only show this",
439
470
  style: [styles.filterActionButton, styles.filterActionButtonInclude, !selectedOption && styles.filterActionButtonDisabled],
440
471
  onPress: handleInclude,
441
472
  disabled: !selectedOption,
@@ -450,6 +481,9 @@ function QuickActionsSection({
450
481
  children: "Only Show This"
451
482
  })]
452
483
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
484
+ testID: "buoy-highlight-quick-exclude",
485
+ accessibilityRole: "button",
486
+ accessibilityLabel: "Hide this",
453
487
  style: [styles.filterActionButton, styles.filterActionButtonExclude, !selectedOption && styles.filterActionButtonDisabled],
454
488
  onPress: handleExclude,
455
489
  disabled: !selectedOption,
@@ -472,12 +506,16 @@ function QuickActionsSection({
472
506
  * FilterOptionCard - A selectable filter option card
473
507
  */
474
508
  function FilterOptionCard({
509
+ testID,
475
510
  label,
476
511
  value,
477
512
  isSelected,
478
513
  onSelect
479
514
  }) {
480
515
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
516
+ testID: testID,
517
+ accessibilityRole: "button",
518
+ accessibilityLabel: `${label}, ${value}`,
481
519
  style: [styles.filterOptionCard, isSelected && styles.filterOptionCardSelected],
482
520
  onPress: onSelect,
483
521
  activeOpacity: 0.7,
@@ -485,6 +523,7 @@ function FilterOptionCard({
485
523
  style: [styles.filterOptionLabel, isSelected && styles.filterOptionLabelSelected],
486
524
  children: label
487
525
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
526
+ testID: testID ? `${testID}-value` : undefined,
488
527
  style: [styles.filterOptionValue, isSelected && styles.filterOptionValueSelected],
489
528
  numberOfLines: 1,
490
529
  children: value
@@ -22,15 +22,22 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
22
22
  */
23
23
 
24
24
  /**
25
- * Format timestamp with milliseconds
25
+ * `HH:MM:SS.mmm` — the same shape Redux, Zustand and Jotai print for the same
26
+ * job, and for the same reason: the milliseconds are the point.
27
+ *
28
+ * Built by hand rather than from `toLocaleTimeString`, which appends the day
29
+ * period on a 12-hour locale — so concatenating the milliseconds to the end of
30
+ * it produced "8:21:44 AM.351", with the fraction attached to the wrong part
31
+ * of the time. It also made the string depend on the hour of day, which is a
32
+ * fine way to have a test that only passes after 10am.
26
33
  */
27
34
  function formatTimeWithMs(timestamp) {
28
35
  const date = new Date(timestamp);
29
- return date.toLocaleTimeString([], {
30
- hour: "2-digit",
31
- minute: "2-digit",
32
- second: "2-digit"
33
- }) + `.${date.getMilliseconds().toString().padStart(3, "0")}`;
36
+ const h = date.getHours().toString().padStart(2, "0");
37
+ const m = date.getMinutes().toString().padStart(2, "0");
38
+ const s = date.getSeconds().toString().padStart(2, "0");
39
+ const ms = date.getMilliseconds().toString().padStart(3, "0");
40
+ return `${h}:${m}:${s}.${ms}`;
34
41
  }
35
42
 
36
43
  /**
@@ -90,14 +97,17 @@ function RenderHistoryViewer({
90
97
  // If no history, show empty state
91
98
  if (totalEvents === 0) {
92
99
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
100
+ testID: "buoy-highlight-history-empty",
93
101
  style: styles.emptyContainer,
94
102
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Clock, {
95
103
  size: 32,
96
104
  color: _sharedUi.night.color.textTertiary
97
105
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
106
+ testID: "buoy-highlight-history-empty-title",
98
107
  style: styles.emptyTitle,
99
108
  children: "No Render History"
100
109
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
110
+ testID: "buoy-highlight-history-empty-text",
101
111
  style: styles.emptyText,
102
112
  children: "Enable render history tracking in settings to see render events here."
103
113
  })]
@@ -109,11 +119,13 @@ function RenderHistoryViewer({
109
119
  paddingBottom: !disableInternalFooter && totalEvents > 1 ? 80 : 0
110
120
  }],
111
121
  children: [lockedEventCount > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
122
+ testID: "buoy-highlight-history-locked",
112
123
  style: styles.lockedBanner,
113
124
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
114
125
  size: 14,
115
126
  color: _sharedUi.night.color.warning
116
127
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
128
+ testID: "buoy-highlight-history-locked-text",
117
129
  style: styles.lockedBannerText,
118
130
  children: [lockedEventCount, " older ", lockedEventCount === 1 ? 'render' : 'renders', " locked"]
119
131
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -123,6 +135,9 @@ function RenderHistoryViewer({
123
135
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
124
136
  style: styles.viewToggleContainer,
125
137
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
138
+ testID: "buoy-highlight-history-mode-current",
139
+ accessibilityRole: "button",
140
+ accessibilityLabel: "Current state",
126
141
  style: [styles.viewToggleCard, activeView === "current" && styles.viewToggleCardActive],
127
142
  onPress: () => setActiveView("current"),
128
143
  activeOpacity: 0.8,
@@ -142,6 +157,8 @@ function RenderHistoryViewer({
142
157
  children: ["View render #", currentEvent?.renderNumber ?? 0, " details"]
143
158
  })]
144
159
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
160
+ testID: "buoy-highlight-history-mode-diff",
161
+ accessibilityLabel: "Diff view, coming soon",
145
162
  style: [styles.viewToggleCard, styles.viewToggleCardDisabled],
146
163
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
147
164
  style: styles.viewToggleContent,
@@ -166,6 +183,7 @@ function RenderHistoryViewer({
166
183
  })]
167
184
  })]
168
185
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
186
+ testID: "buoy-highlight-history-scroll",
169
187
  style: styles.contentScroll,
170
188
  contentContainerStyle: styles.contentContainer,
171
189
  showsVerticalScrollIndicator: false,
@@ -198,6 +216,7 @@ function CurrentStateView({
198
216
  }) {
199
217
  if (!event) {
200
218
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
219
+ testID: "buoy-highlight-history-no-event",
201
220
  style: styles.noEventContainer,
202
221
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
203
222
  style: styles.noEventText,
@@ -213,9 +232,11 @@ function CurrentStateView({
213
232
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
214
233
  style: styles.eventHeaderLeft,
215
234
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
235
+ testID: "buoy-highlight-history-event-title",
216
236
  style: styles.eventTitle,
217
237
  children: ["Render #", event.renderNumber]
218
238
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
239
+ testID: "buoy-highlight-history-event-time",
219
240
  style: styles.eventTime,
220
241
  children: formatTimeWithMs(event.timestamp)
221
242
  })]
@@ -228,6 +249,7 @@ function CurrentStateView({
228
249
  backgroundColor: render.color
229
250
  }]
230
251
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
252
+ testID: "buoy-highlight-history-event-cause",
231
253
  style: [styles.eventBadgeText, {
232
254
  color: render.color
233
255
  }],
@@ -268,6 +290,7 @@ function CurrentStateView({
268
290
  showsHorizontalScrollIndicator: false,
269
291
  style: styles.propsScrollView,
270
292
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
293
+ testID: "buoy-highlight-history-props",
271
294
  style: styles.propsJson,
272
295
  children: JSON.stringify(event.capturedProps, null, 2)
273
296
  })
@@ -9,6 +9,7 @@ var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _IdentifierBadge = require("./IdentifierBadge");
11
11
  var _RenderCauseBadge = require("./RenderCauseBadge");
12
+ var _testIDs = require("../utils/testIDs");
12
13
  var _jsxRuntime = require("react/jsx-runtime");
13
14
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
14
15
  /**
@@ -59,7 +60,15 @@ function RenderListItemInner({
59
60
 
60
61
  // Format time since last render (live-updating)
61
62
  const timeSinceRender = (0, _sharedUi.useRelativeTime)(render.lastRenderTime);
63
+
64
+ // The row is addressed by the identifier it SHOWS, never by nativeTag — see
65
+ // utils/testIDs.ts for why a tag can't be a handle. The label spells the row
66
+ // out for assistive tech, which otherwise gets a coloured bar and a count.
67
+ const rowTestID = (0, _testIDs.renderRowTestID)(identifier.value);
62
68
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
69
+ testID: rowTestID,
70
+ accessibilityRole: "button",
71
+ accessibilityLabel: `${render.displayName}, ${identifier.type} ${identifier.value}, ${render.renderCount} renders`,
63
72
  style: styles.container,
64
73
  onPress: () => onPress(render),
65
74
  activeOpacity: 0.7,
@@ -74,6 +83,7 @@ function RenderListItemInner({
74
83
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
75
84
  style: styles.viewTypeContainer,
76
85
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
86
+ testID: `${rowTestID}-name`,
77
87
  style: styles.viewType,
78
88
  numberOfLines: 1,
79
89
  children: render.displayName
@@ -83,6 +93,7 @@ function RenderListItemInner({
83
93
  compact: true,
84
94
  shortLabel: true
85
95
  }) : render.displayName !== render.viewType ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
96
+ testID: `${rowTestID}-type`,
86
97
  style: styles.nativeType,
87
98
  numberOfLines: 1,
88
99
  children: render.viewType
@@ -92,6 +103,7 @@ function RenderListItemInner({
92
103
  backgroundColor: render.color + "30"
93
104
  }],
94
105
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
106
+ testID: `${rowTestID}-count`,
95
107
  style: [styles.renderCount, {
96
108
  color: render.color
97
109
  }],
@@ -101,6 +113,7 @@ function RenderListItemInner({
101
113
  }), render.lastRenderCause && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
102
114
  style: styles.causeRow,
103
115
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_RenderCauseBadge.RenderCauseBadge, {
116
+ testID: `${rowTestID}-cause`,
104
117
  cause: render.lastRenderCause,
105
118
  compact: true,
106
119
  showKeys: true,
@@ -111,12 +124,14 @@ function RenderListItemInner({
111
124
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
112
125
  style: styles.identifierContainer,
113
126
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_IdentifierBadge.IdentifierBadge, {
127
+ testID: `${rowTestID}-identifier`,
114
128
  type: identifier.type,
115
129
  value: identifier.value,
116
130
  compact: true,
117
131
  shortLabel: true
118
132
  })
119
133
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
134
+ testID: `${rowTestID}-time`,
120
135
  style: styles.timing,
121
136
  children: timeSinceRender
122
137
  })]
@@ -200,6 +200,9 @@ function RendersCopySettingsView({
200
200
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
201
201
  style: styles.collapsedPreview,
202
202
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
203
+ testID: "buoy-highlight-copy-preview-show",
204
+ accessibilityRole: "button",
205
+ accessibilityLabel: "Show preview",
203
206
  style: styles.expandButton,
204
207
  onPress: () => setIsPreviewExpanded(true),
205
208
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -213,6 +216,7 @@ function RendersCopySettingsView({
213
216
  style: styles.expandButtonTitle,
214
217
  children: "Show Preview"
215
218
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
219
+ testID: "buoy-highlight-copy-preview-summary",
216
220
  style: styles.expandButtonSubtitle,
217
221
  children: [totalComponents, " component", totalComponents !== 1 ? "s" : "", " \xB7 ", totalRenders, " renders \xB7 ~", formatBytes(estimatedSize), !hasLiveData ? " (mock data)" : ""]
218
222
  })]
@@ -222,6 +226,7 @@ function RendersCopySettingsView({
222
226
  })]
223
227
  })
224
228
  }), sizeWarningLevel !== "none" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
229
+ testID: "buoy-highlight-copy-size-warning",
225
230
  style: [styles.sizeWarning, sizeWarningLevel === "danger" && styles.sizeWarningDanger],
226
231
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertTriangle, {
227
232
  size: 12,
@@ -238,6 +243,9 @@ function RendersCopySettingsView({
238
243
  const exportText = generateCopyText();
239
244
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
240
245
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
246
+ testID: "buoy-highlight-copy-preview-hide",
247
+ accessibilityRole: "button",
248
+ accessibilityLabel: "Hide preview",
241
249
  style: styles.collapseButton,
242
250
  onPress: () => setIsPreviewExpanded(false),
243
251
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -248,9 +256,11 @@ function RendersCopySettingsView({
248
256
  color: _sharedUi.night.color.textSecondary
249
257
  })]
250
258
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
259
+ testID: "buoy-highlight-copy-preview-scroll",
251
260
  style: styles.previewScroll,
252
261
  nestedScrollEnabled: true,
253
262
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
263
+ testID: "buoy-highlight-copy-preview-text",
254
264
  style: styles.previewText,
255
265
  selectable: true,
256
266
  children: exportText
@@ -265,6 +275,7 @@ function RendersCopySettingsView({
265
275
  const statusLabel = hasLiveData ? `${totalComponents} components` : "Mock data (no renders captured)";
266
276
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
267
277
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
278
+ testID: "buoy-highlight-copy-status",
268
279
  style: styles.statusLabel,
269
280
  children: statusLabel
270
281
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
@@ -36,11 +36,14 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
36
36
  }, []);
37
37
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
38
38
  nativeID: "__rn_buoy__stats-row",
39
+ testID: "buoy-highlight-stats",
40
+ accessibilityLabel: `${stats.totalComponents} components tracked`,
39
41
  style: styles.headerChip,
40
42
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Activity, {
41
43
  size: 12,
42
44
  color: _sharedUi.night.color.accent
43
45
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
46
+ testID: "buoy-highlight-stats-value",
44
47
  style: [styles.headerChipValue, {
45
48
  color: _sharedUi.night.color.accent
46
49
  }],
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.CommitProfiler = void 0;
7
7
  var _devtoolsComponentNames = require("./devtoolsComponentNames");
8
+ var _ownStateChanged = require("./ownStateChanged");
8
9
  /**
9
10
  * CommitProfiler — per-component render counts + durations across a capture
10
11
  * window, React-DevTools-profiler style, without React DevTools attached.
@@ -23,6 +24,10 @@ var _devtoolsComponentNames = require("./devtoolsComponentNames");
23
24
  * registry (see the registration in this package's index.tsx).
24
25
  */
25
26
 
27
+ // One implementation, shared with the overlay's RenderCauseDetector — the two
28
+ // classifiers are separate on purpose, but this question is not. See the file
29
+ // for why the obvious identity check is wrong.
30
+
26
31
  // React work tags for components that render user code.
27
32
  const FunctionComponent = 0;
28
33
  const ClassComponent = 1;
@@ -209,7 +214,7 @@ function detectCause(fiber, wantKeys) {
209
214
  }
210
215
 
211
216
  // No prop change of substance. Own state/hooks next.
212
- if (prev.memoizedState !== fiber.memoizedState) return {
217
+ if ((0, _ownStateChanged.ownStateChanged)(prev, fiber)) return {
213
218
  cause: "hooks"
214
219
  };
215
220
  return {
@@ -1626,6 +1626,16 @@ function disable() {
1626
1626
  devToolsNodeCache.clear(); // Clear detection cache
1627
1627
  (0, _RenderCauseDetector.clearRenderCauseState)(); // Clear render cause state to prevent memory leaks
1628
1628
 
1629
+ // Freeze is a view state on a LIVE overlay, and the overlay is going away:
1630
+ // the boxes it was holding open were just cleared with the tags above. Left
1631
+ // latched it outlived the thing it froze — switch tracking off and back on
1632
+ // and the button still read "Unfreeze frame" on a tool with nothing frozen,
1633
+ // while new highlights (correctly) refused to fade.
1634
+ if (isFrozen) {
1635
+ isFrozen = false;
1636
+ notifyFreezeListeners();
1637
+ }
1638
+
1629
1639
  // Only unsubscribe if background tracking doesn't need it
1630
1640
  if (!backgroundTrackingEnabled) {
1631
1641
  unsubscribeFromTraceUpdates();
@@ -36,6 +36,11 @@ exports.detectRenderCause = detectRenderCause;
36
36
  exports.getRenderCauseStats = getRenderCauseStats;
37
37
  exports.removeRenderCauseState = removeRenderCauseState;
38
38
  exports.safeCloneForHistory = safeCloneForHistory;
39
+ var _ownStateChanged = require("./ownStateChanged");
40
+ // One implementation of "did this component's own state actually change?",
41
+ // shared with CommitProfiler. The two classifiers are separate on purpose;
42
+ // this question is not, and both of them got it wrong the same way.
43
+
39
44
  // ============================================================================
40
45
  // TYPE DEFINITIONS
41
46
  // Based on React Native source: packages/react-native/Libraries/Renderer/
@@ -660,9 +665,16 @@ function detectComponentCause(componentFiber) {
660
665
 
661
666
  // Check if state changed (for hooks, memoizedState is a linked list)
662
667
  // This means useState/useReducer triggered a re-render
663
- const stateChanged = prevMemoizedState !== currentFiber.memoizedState;
664
- if (stateChanged) {
665
- // Phase 3: Compare hook states to get actual value changes
668
+ // NOT `prevMemoizedState !== currentFiber.memoizedState`, which is what this
669
+ // used to be — see utils/ownStateChanged.ts for why that is wrong and what
670
+ // it cost. Shared with `CommitProfiler`, which reached the same wrong line
671
+ // independently; the two classifiers stay separate, this one question does
672
+ // not.
673
+ if ((0, _ownStateChanged.hookStateChanged)(prevMemoizedState, currentFiber.memoizedState, {
674
+ isClassComponent: currentFiber.tag === 1
675
+ })) {
676
+ // Phase 3: the actual before/after values, for the UI. Only needed once
677
+ // the cause is settled, and only the overlay displays them.
666
678
  const hookChanges = compareHookStates(prevExtractedHooks, currentHooks);
667
679
  return {
668
680
  cause: "state",
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.hookStateChanged = hookStateChanged;
7
+ exports.ownStateChanged = ownStateChanged;
8
+ /**
9
+ * "Did this component's own state actually change?" — one implementation.
10
+ *
11
+ * This package classifies render causes twice, on purpose. `RenderCauseDetector`
12
+ * serves the overlay and rides React DevTools' `traceUpdates`, which hands it
13
+ * fibers at an ambiguous moment — hence its WeakMap fallbacks and fiber-swap
14
+ * detection. `CommitProfiler` serves Bench's render capture from inside
15
+ * `onCommitFiberRoot`, where `fiber` is current and `fiber.alternate` is
16
+ * previous by definition, and where it runs on every composite fiber of every
17
+ * commit and so counts allocations. Those are genuinely different problems and
18
+ * the two classifiers should stay separate.
19
+ *
20
+ * This ONE question is not different between them, and it is the question they
21
+ * both got wrong:
22
+ *
23
+ * `prev.memoizedState !== next.memoizedState`
24
+ *
25
+ * For a function component `memoizedState` is the head of the hook linked list,
26
+ * and React rebuilds that list on every update — `updateWorkInProgressHook`
27
+ * clones each node — so the head is a fresh object whether or not a single
28
+ * hook's value moved. Read as "state changed" it makes "state"/"hooks" the
29
+ * answer for every component that uses ANY hook (a lone `useEffect` is enough)
30
+ * and leaves "parent" reachable only by the vanishingly rare component that
31
+ * uses none.
32
+ *
33
+ * That is the one distinction in either tool a developer acts on: "parent"
34
+ * means memoize it or lift the state, "state" means it genuinely updated and
35
+ * should be left alone. Backwards, it sends you to optimise the thing that was
36
+ * already fine — and in Bench it also silently shrank `wastedRenders`, the
37
+ * number that says whether there is anything to fix at all.
38
+ *
39
+ * Both tools reached that wrong line independently, which is exactly why the
40
+ * answer lives here now rather than in two places that happen to agree.
41
+ */
42
+
43
+ /** React work tag for a class component — its `memoizedState` IS the state. */
44
+ const CLASS_COMPONENT = 1;
45
+
46
+ /** Guardrail on the hook-list walk; deep hook lists are pathological. */
47
+ const MAX_HOOKS_WALKED = 64;
48
+
49
+ /** Does this look like React's hook linked list rather than class state? */
50
+ function isHookList(head) {
51
+ return !!head && typeof head === "object" && ("next" in head || "queue" in head || "memoizedState" in head);
52
+ }
53
+
54
+ /**
55
+ * True when a STATE hook's value actually moved between two hook-list heads.
56
+ *
57
+ * `queue` is the marker: `useState` and `useReducer` — and everything built on
58
+ * them — own one; effects, memos, callbacks and refs do not. An effect's
59
+ * `memoizedState` is a brand-new object on every single render, which is the
60
+ * specific thing that broke the identity check.
61
+ *
62
+ * Note that a `useRef` mutation is deliberately NOT a state change here. It
63
+ * cannot cause a render, so a render attributed to one is a render blamed on
64
+ * something that did not do it.
65
+ *
66
+ * Conservative where it cannot tell: a class component, or a `memoizedState`
67
+ * that is not a recognisable hook list, falls back to the identity comparison.
68
+ * There the identity IS meaningful (or is the best signal available), so the
69
+ * old behaviour is the right behaviour.
70
+ *
71
+ * Takes the HEADS rather than the fibers because only one of the two callers
72
+ * has a previous fiber to hand — the overlay may be working from a WeakMap
73
+ * snapshot of the previous `memoizedState` instead. Use `ownStateChanged` when
74
+ * you do have both fibers.
75
+ */
76
+ function hookStateChanged(prevHead, nextHead, options) {
77
+ if (prevHead === nextHead) return false;
78
+ if (options?.isClassComponent) return true;
79
+ if (!isHookList(nextHead)) return true;
80
+ let p = prevHead;
81
+ let n = nextHead;
82
+ for (let depth = 0; n != null && depth < MAX_HOOKS_WALKED; depth++) {
83
+ if (n.queue != null) {
84
+ // A hook the previous render did not have is a real change.
85
+ if (p == null) return true;
86
+ if (!Object.is(p.memoizedState, n.memoizedState)) return true;
87
+ }
88
+ n = n.next;
89
+ p = p != null ? p.next : null;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ /**
95
+ * `hookStateChanged` for callers holding both fibers — the common case inside
96
+ * `onCommitFiberRoot`, where `fiber.alternate` is the previous fiber by
97
+ * definition.
98
+ */
99
+ function ownStateChanged(prev, next) {
100
+ return hookStateChanged(prev?.memoizedState, next?.memoizedState, {
101
+ isClassComponent: next?.tag === CLASS_COMPONENT
102
+ });
103
+ }