@buoy-gg/redux 7.0.29 → 7.0.31

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 (44) hide show
  1. package/lib/commonjs/index.js +25 -0
  2. package/lib/commonjs/redux/components/ReduxActionButton.js +3 -1
  3. package/lib/commonjs/redux/components/ReduxActionDetailContent.js +60 -6
  4. package/lib/commonjs/redux/components/ReduxActionInfoView.js +61 -12
  5. package/lib/commonjs/redux/components/ReduxActionItem.js +20 -5
  6. package/lib/commonjs/redux/components/ReduxDetailViewToggle.js +7 -0
  7. package/lib/commonjs/redux/components/ReduxModal.js +168 -32
  8. package/lib/commonjs/redux/utils/activeStoreRegistry.js +31 -0
  9. package/lib/commonjs/redux/utils/autoInstrument.js +23 -40
  10. package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +36 -9
  11. package/lib/commonjs/redux/utils/devtoolsHook.js +4 -0
  12. package/lib/commonjs/redux/utils/testIDs.js +75 -0
  13. package/lib/module/index.js +5 -0
  14. package/lib/module/redux/components/ReduxActionButton.js +3 -1
  15. package/lib/module/redux/components/ReduxActionDetailContent.js +60 -6
  16. package/lib/module/redux/components/ReduxActionInfoView.js +63 -14
  17. package/lib/module/redux/components/ReduxActionItem.js +20 -5
  18. package/lib/module/redux/components/ReduxDetailViewToggle.js +7 -0
  19. package/lib/module/redux/components/ReduxModal.js +170 -33
  20. package/lib/module/redux/utils/activeStoreRegistry.js +29 -0
  21. package/lib/module/redux/utils/autoInstrument.js +8 -40
  22. package/lib/module/redux/utils/buoyReduxMiddleware.js +32 -10
  23. package/lib/module/redux/utils/devtoolsHook.js +5 -1
  24. package/lib/module/redux/utils/testIDs.js +68 -0
  25. package/lib/typescript/index.d.ts +1 -0
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/lib/typescript/redux/components/ReduxActionButton.d.ts +2 -1
  28. package/lib/typescript/redux/components/ReduxActionButton.d.ts.map +1 -1
  29. package/lib/typescript/redux/components/ReduxActionDetailContent.d.ts.map +1 -1
  30. package/lib/typescript/redux/components/ReduxActionInfoView.d.ts.map +1 -1
  31. package/lib/typescript/redux/components/ReduxActionItem.d.ts +15 -0
  32. package/lib/typescript/redux/components/ReduxActionItem.d.ts.map +1 -1
  33. package/lib/typescript/redux/components/ReduxDetailViewToggle.d.ts.map +1 -1
  34. package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
  35. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts +27 -0
  36. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -1
  37. package/lib/typescript/redux/utils/autoInstrument.d.ts +7 -11
  38. package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -1
  39. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts +17 -3
  40. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -1
  41. package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -1
  42. package/lib/typescript/redux/utils/testIDs.d.ts +51 -0
  43. package/lib/typescript/redux/utils/testIDs.d.ts.map +1 -0
  44. package/package.json +4 -4
@@ -21,6 +21,12 @@ Object.defineProperty(exports, "FREE_TIER_ACTION_LIMIT", {
21
21
  return _ReduxModal.FREE_TIER_ACTION_LIMIT;
22
22
  }
23
23
  });
24
+ Object.defineProperty(exports, "REDUX_TEST_ID", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _testIDs.REDUX_TEST_ID;
28
+ }
29
+ });
24
30
  Object.defineProperty(exports, "ReduxActionDetailContent", {
25
31
  enumerable: true,
26
32
  get: function () {
@@ -69,6 +75,18 @@ Object.defineProperty(exports, "ReduxToolView", {
69
75
  return _ReduxModal.ReduxToolView;
70
76
  }
71
77
  });
78
+ Object.defineProperty(exports, "actionRowTestID", {
79
+ enumerable: true,
80
+ get: function () {
81
+ return _testIDs.actionRowTestID;
82
+ }
83
+ });
84
+ Object.defineProperty(exports, "buildActionRowTestIDs", {
85
+ enumerable: true,
86
+ get: function () {
87
+ return _testIDs.buildActionRowTestIDs;
88
+ }
89
+ });
72
90
  Object.defineProperty(exports, "buoyDevtoolsEnhancer", {
73
91
  enumerable: true,
74
92
  get: function () {
@@ -183,6 +201,12 @@ Object.defineProperty(exports, "replayAction", {
183
201
  return _buoyReduxMiddleware.replayAction;
184
202
  }
185
203
  });
204
+ Object.defineProperty(exports, "slugForTestID", {
205
+ enumerable: true,
206
+ get: function () {
207
+ return _testIDs.slugForTestID;
208
+ }
209
+ });
186
210
  Object.defineProperty(exports, "uninstrumentStore", {
187
211
  enumerable: true,
188
212
  get: function () {
@@ -228,4 +252,5 @@ var _ReduxActionDetailView = require("./redux/components/ReduxActionDetailView")
228
252
  var _ReduxActionDetailContent = require("./redux/components/ReduxActionDetailContent");
229
253
  var _ReduxIcon = require("./redux/components/ReduxIcon");
230
254
  var _ReduxAutoInstrument = require("./redux/components/ReduxAutoInstrument");
255
+ var _testIDs = require("./redux/utils/testIDs");
231
256
  var _reduxActionStore = require("./redux/utils/reduxActionStore");
@@ -57,10 +57,12 @@ function ReduxActionButton({
57
57
  onPress,
58
58
  text,
59
59
  type,
60
- disabled = false
60
+ disabled = false,
61
+ testID
61
62
  }) {
62
63
  const config = buttonConfigs[type];
63
64
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
65
+ testID: testID,
64
66
  disabled: disabled,
65
67
  onPress: onPress,
66
68
  style: [styles.button, {
@@ -11,6 +11,8 @@ var _sharedUi = require("@buoy-gg/shared-ui");
11
11
  var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
12
12
  var _ReduxDetailViewToggle = require("./ReduxDetailViewToggle");
13
13
  var _ReduxActionInfoView = require("./ReduxActionInfoView");
14
+ var _testIDs = require("../utils/testIDs");
15
+ var _reduxActionStore = require("../utils/reduxActionStore");
14
16
  var _jsxRuntime = require("react/jsx-runtime");
15
17
  /**
16
18
  * ReduxActionDetailContent
@@ -89,6 +91,20 @@ function ReduxActionDetailContent({
89
91
  const [activeView, setActiveView] = (0, _react.useState)(initialView);
90
92
  const [diffMode, setDiffMode] = (0, _react.useState)("tree");
91
93
  const totalActions = actions.length;
94
+ /**
95
+ * The state trees this action was recorded with are gone.
96
+ *
97
+ * Not a failure: the store keeps raw prev/next state for the most recent
98
+ * actions only, because with structural sharing broken (a store switch, a
99
+ * rehydration) each retained action pins a distinct multi-MB subtree and a
100
+ * few dozen of them are enough to OOM-kill a large app. But it has to be
101
+ * SAID. The sentinel left behind is an ordinary object, so a state view that
102
+ * just rendered it showed `{ __buoyStateEvicted: true, … }` under the
103
+ * heading "STATE AFTER ACTION" — which reads as the app's actual state — and
104
+ * a diff of two identical sentinels reported "no changes", which is a lie
105
+ * about the app rather than about the recording.
106
+ */
107
+ const stateEvicted = (0, _reduxActionStore.isEvictedState)(action.nextState);
92
108
  const actionColor = getActionColor(action.type);
93
109
  const actionLabel = getActionLabel(action.type);
94
110
  const stateType = getValueType((0, _sharedUi.parseValue)(action.nextState));
@@ -139,9 +155,26 @@ function ReduxActionDetailContent({
139
155
 
140
156
  // Render state view
141
157
  const renderStateView = (0, _react.useCallback)(() => {
158
+ if (stateEvicted) {
159
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
160
+ style: styles.contentCard,
161
+ testID: `${_testIDs.REDUX_TEST_ID}-state-evicted`,
162
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
163
+ style: styles.valueHeader,
164
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
165
+ style: styles.valueLabel,
166
+ children: "STATE AFTER ACTION"
167
+ })
168
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
169
+ style: styles.evictedText,
170
+ children: `Not retained. Raw state is kept for the ${_reduxActionStore.STATE_RETENTION_LIMIT} most recent actions, so this action's before/after trees have been released.`
171
+ })]
172
+ });
173
+ }
142
174
  const parsed = (0, _sharedUi.parseValue)(action.nextState);
143
175
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
144
176
  style: styles.contentCard,
177
+ testID: `${_testIDs.REDUX_TEST_ID}-state-card`,
145
178
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
146
179
  style: styles.valueHeader,
147
180
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -163,6 +196,7 @@ function ReduxActionDetailContent({
163
196
  style: styles.typeBadge,
164
197
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
165
198
  style: styles.typeText,
199
+ testID: `${_testIDs.REDUX_TEST_ID}-state-type`,
166
200
  children: stateType.toUpperCase()
167
201
  })
168
202
  })]
@@ -178,7 +212,7 @@ function ReduxActionDetailContent({
178
212
  })
179
213
  })]
180
214
  });
181
- }, [action.nextState, actionColor, actionLabel, stateType]);
215
+ }, [action.nextState, actionColor, actionLabel, stateType, stateEvicted]);
182
216
 
183
217
  // Render diff content
184
218
  const renderDiffContent = (0, _react.useCallback)(() => {
@@ -218,7 +252,15 @@ function ReduxActionDetailContent({
218
252
  const handleFooterNext = (0, _react.useCallback)(() => {
219
253
  onIndexChange(Math.max(0, selectedIndex - 1));
220
254
  }, [selectedIndex, onIndexChange]);
221
- const diffDisabled = totalActions <= 1;
255
+
256
+ // A diff needs two real trees, and both live on THIS action
257
+ // (prevState/nextState) — no siblings needed, so the action count says
258
+ // nothing (gating on it greyed DIFF out for every redux event opened from
259
+ // the Events timeline, which passes a one-element array). Eviction of
260
+ // EITHER side does disable it: two identical sentinels diff cleanly to
261
+ // "no changes", which reads as a lie.
262
+ const diffDisabled = stateEvicted || (0, _reduxActionStore.isEvictedState)(action.prevState) || action.prevState === undefined;
263
+ const showInternalFooter = !disableInternalFooter && totalActions > 1;
222
264
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
223
265
  style: styles.container,
224
266
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxDetailViewToggle.ReduxDetailViewToggle, {
@@ -226,7 +268,7 @@ function ReduxActionDetailContent({
226
268
  onViewChange: setActiveView,
227
269
  diffDisabled: diffDisabled
228
270
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
229
- style: styles.contentArea,
271
+ style: [styles.contentArea, showInternalFooter && styles.contentAreaFooterPad],
230
272
  children: [activeView === "action" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionInfoView.ReduxActionInfoView, {
231
273
  action: action
232
274
  }), activeView === "state" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -234,19 +276,22 @@ function ReduxActionDetailContent({
234
276
  children: renderStateView()
235
277
  }), activeView === "diff" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
236
278
  style: styles.diffContainer,
279
+ testID: `${_testIDs.REDUX_TEST_ID}-diff`,
237
280
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.DiffModeTabs, {
238
281
  tabs: DIFF_MODE_TABS,
239
282
  activeTab: diffMode,
240
- onTabChange: setDiffMode
283
+ onTabChange: setDiffMode,
284
+ testIDPrefix: `${_testIDs.REDUX_TEST_ID}-diff-tab`
241
285
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CompareBar, {
242
286
  leftEvent: leftEvent,
243
287
  rightEvent: rightEvent
244
288
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
245
289
  style: styles.diffContent,
290
+ testID: `${_testIDs.REDUX_TEST_ID}-diff-content`,
246
291
  children: renderDiffContent()
247
292
  })]
248
293
  })]
249
- }), !disableInternalFooter && totalActions > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.EventStepperFooter, {
294
+ }), showInternalFooter && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.EventStepperFooter, {
250
295
  currentIndex: chronologicalNumber - 1,
251
296
  totalItems: totalActions,
252
297
  onPrevious: handleFooterPrevious,
@@ -286,7 +331,11 @@ const styles = _reactNative.StyleSheet.create({
286
331
  backgroundColor: "transparent"
287
332
  },
288
333
  contentArea: {
289
- flex: 1,
334
+ flex: 1
335
+ },
336
+ // Clearance for the absolute EventStepperFooter — only when it renders,
337
+ // otherwise hosts like the Events tool get a dead band at the sheet bottom.
338
+ contentAreaFooterPad: {
290
339
  paddingBottom: 96
291
340
  },
292
341
  // State view styles
@@ -350,6 +399,11 @@ const styles = _reactNative.StyleSheet.create({
350
399
  borderColor: _sharedUi.night.color.border,
351
400
  padding: 8
352
401
  },
402
+ evictedText: {
403
+ fontSize: 12,
404
+ color: _sharedUi.night.color.textSecondary,
405
+ lineHeight: 18
406
+ },
353
407
  valueText: {
354
408
  fontSize: 12,
355
409
  color: _sharedUi.night.color.text,
@@ -12,6 +12,7 @@ var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
12
12
  var _reduxActionStore = require("../utils/reduxActionStore");
13
13
  var _buoyReduxMiddleware = require("../utils/buoyReduxMiddleware");
14
14
  var _ReduxActionButton = require("./ReduxActionButton");
15
+ var _testIDs = require("../utils/testIDs");
15
16
  var _jsxRuntime = require("react/jsx-runtime");
16
17
  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); }
17
18
  /**
@@ -89,10 +90,12 @@ function formatDuration(duration) {
89
90
  function SectionHeader({
90
91
  icon: Icon,
91
92
  title,
92
- badge
93
+ badge,
94
+ testID
93
95
  }) {
94
96
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
95
97
  style: styles.sectionHeader,
98
+ testID: testID,
96
99
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
97
100
  style: styles.sectionHeaderLeft,
98
101
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(Icon, {
@@ -113,7 +116,8 @@ function InfoRow({
113
116
  label,
114
117
  value,
115
118
  valueColor,
116
- mono = false
119
+ mono = false,
120
+ testID
117
121
  }) {
118
122
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
119
123
  style: styles.infoRow,
@@ -123,9 +127,14 @@ function InfoRow({
123
127
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
124
128
  style: [styles.infoValue, mono && styles.infoValueMono, valueColor && {
125
129
  color: valueColor
126
- }],
130
+ }]
131
+ // `ellipsizeMode="middle"` truncates the RENDERED text and iOS reports
132
+ // the truncated string back, so anything a test reads off one of these
133
+ // rows has to be short enough to fit on the line.
134
+ ,
127
135
  numberOfLines: 1,
128
136
  ellipsizeMode: "middle",
137
+ testID: testID,
129
138
  children: value
130
139
  })]
131
140
  });
@@ -156,6 +165,18 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
156
165
  }
157
166
  }, [showProModal, isPro]);
158
167
  const categoryInfo = getCategoryInfo(action.category);
168
+ // Time travel needs a REAL tree. The store releases raw state for older
169
+ // actions (see ReduxActionDetailContent), and `jumpToState` refuses the
170
+ // sentinel — but it refused into a console warning, so from the screen the
171
+ // button simply did nothing at all. A disabled button that says why is the
172
+ // difference between a limit and a bug.
173
+ const stateEvicted = (0, _reduxActionStore.isEvictedState)(action.nextState);
174
+ // Whether this app wired time travel at all. Capture and time travel are
175
+ // separate: an app can be fully captured through `buoyReduxMiddleware` and
176
+ // still have no reducer that answers a jump, in which case the press
177
+ // dispatched, was ignored, and left the screen exactly as it was.
178
+ const canTimeTravel = (0, _buoyReduxMiddleware.isTimeTravelAvailable)();
179
+ const jumpDisabled = stateEvicted || !canTimeTravel;
159
180
  const hasPayload = action.payload !== undefined;
160
181
  const hasMeta = action.meta && Object.keys(action.meta).length > 0;
161
182
  const hasError = action.error !== undefined;
@@ -196,11 +217,13 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
196
217
  }
197
218
  }, [action]);
198
219
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
220
+ testID: `${_testIDs.REDUX_TEST_ID}-detail-scroll`,
199
221
  style: styles.container,
200
222
  contentContainerStyle: styles.contentContainer,
201
223
  showsVerticalScrollIndicator: false,
202
224
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
203
225
  style: styles.card,
226
+ testID: `${_testIDs.REDUX_TEST_ID}-info-card`,
204
227
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SectionHeader, {
205
228
  icon: _sharedUi.Info,
206
229
  title: "ACTION INFO"
@@ -209,15 +232,18 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
209
232
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(InfoRow, {
210
233
  label: "Type",
211
234
  value: action.type,
212
- mono: true
235
+ mono: true,
236
+ testID: `${_testIDs.REDUX_TEST_ID}-info-type`
213
237
  }), action.sliceName && /*#__PURE__*/(0, _jsxRuntime.jsx)(InfoRow, {
214
238
  label: "Slice",
215
239
  value: action.sliceName,
216
- mono: true
240
+ mono: true,
241
+ testID: `${_testIDs.REDUX_TEST_ID}-info-slice`
217
242
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(InfoRow, {
218
243
  label: "Action",
219
244
  value: action.actionName,
220
- mono: true
245
+ mono: true,
246
+ testID: `${_testIDs.REDUX_TEST_ID}-info-action`
221
247
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
222
248
  style: styles.badgeRow,
223
249
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -228,6 +254,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
228
254
  style: [styles.categoryText, {
229
255
  color: categoryInfo.color
230
256
  }],
257
+ testID: `${_testIDs.REDUX_TEST_ID}-info-category`,
231
258
  children: categoryInfo.label
232
259
  })
233
260
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -237,6 +264,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
237
264
  color: action.isSlowAction ? _sharedUi.buoyColors.error : _sharedUi.night.color.textSecondary
238
265
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
239
266
  style: [styles.durationText, action.isSlowAction && styles.durationTextSlow],
267
+ testID: `${_testIDs.REDUX_TEST_ID}-info-duration`,
240
268
  children: formatDuration(action.duration)
241
269
  }), action.isSlowAction && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertTriangle, {
242
270
  size: 10,
@@ -249,12 +277,14 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
249
277
  color: _sharedUi.buoyColors.success
250
278
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
251
279
  style: styles.changesText,
280
+ testID: `${_testIDs.REDUX_TEST_ID}-info-changes`,
252
281
  children: action.diffSummary || "changed"
253
282
  })]
254
283
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
255
284
  style: styles.noChangesBadge,
256
285
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
257
286
  style: styles.noChangesText,
287
+ testID: `${_testIDs.REDUX_TEST_ID}-info-no-changes`,
258
288
  children: "no change"
259
289
  })
260
290
  })]
@@ -262,6 +292,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
262
292
  })]
263
293
  }), hasRelatedActions && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
264
294
  style: styles.card,
295
+ testID: `${_testIDs.REDUX_TEST_ID}-async-card`,
265
296
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SectionHeader, {
266
297
  icon: _sharedUi.Hash,
267
298
  title: "ASYNC FLOW",
@@ -269,6 +300,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
269
300
  style: styles.asyncBadge,
270
301
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
271
302
  style: styles.asyncBadgeText,
303
+ testID: `${_testIDs.REDUX_TEST_ID}-async-count`,
272
304
  children: [relatedActions.length, " ACTIONS"]
273
305
  })
274
306
  })
@@ -276,6 +308,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
276
308
  style: styles.cardContent,
277
309
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
278
310
  style: styles.baseActionType,
311
+ testID: `${_testIDs.REDUX_TEST_ID}-async-base`,
279
312
  children: baseActionType
280
313
  }), totalDuration !== null && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
281
314
  style: styles.totalDurationRow,
@@ -284,6 +317,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
284
317
  children: "Total Duration"
285
318
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
286
319
  style: styles.totalDurationValue,
320
+ testID: `${_testIDs.REDUX_TEST_ID}-async-total`,
287
321
  children: [totalDuration, "ms"]
288
322
  })]
289
323
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -296,6 +330,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
296
330
  onPress: () => onNavigateToAction?.(relatedAction),
297
331
  disabled: isCurrent || !onNavigateToAction,
298
332
  activeOpacity: 0.7,
333
+ testID: `${_testIDs.REDUX_TEST_ID}-async-step-${index}`,
299
334
  children: [index < relatedActions.length - 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
300
335
  style: styles.timelineConnector
301
336
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -314,10 +349,12 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
314
349
  style: [styles.timelineStatusText, {
315
350
  color: relatedCategoryInfo.color
316
351
  }],
352
+ testID: `${_testIDs.REDUX_TEST_ID}-async-step-${index}-status`,
317
353
  children: relatedCategoryInfo.label
318
354
  })
319
355
  }), isCurrent && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
320
356
  style: styles.currentBadge,
357
+ testID: `${_testIDs.REDUX_TEST_ID}-async-step-${index}-current`,
321
358
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
322
359
  style: styles.currentBadgeText,
323
360
  children: "CURRENT"
@@ -334,6 +371,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
334
371
  })]
335
372
  }), hasPayload && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
336
373
  style: styles.card,
374
+ testID: `${_testIDs.REDUX_TEST_ID}-payload-card`,
337
375
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SectionHeader, {
338
376
  icon: _sharedUi.Box,
339
377
  title: "PAYLOAD",
@@ -356,6 +394,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
356
394
  })]
357
395
  }), hasMeta && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
358
396
  style: styles.card,
397
+ testID: `${_testIDs.REDUX_TEST_ID}-meta-card`,
359
398
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SectionHeader, {
360
399
  icon: _sharedUi.Hash,
361
400
  title: "ASYNC METADATA",
@@ -379,7 +418,8 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
379
418
  }), action.meta?.requestStatus && /*#__PURE__*/(0, _jsxRuntime.jsx)(InfoRow, {
380
419
  label: "Status",
381
420
  value: action.meta.requestStatus.toUpperCase(),
382
- valueColor: categoryInfo.color
421
+ valueColor: categoryInfo.color,
422
+ testID: `${_testIDs.REDUX_TEST_ID}-meta-status`
383
423
  }), action.meta?.arg !== undefined && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
384
424
  style: styles.argSection,
385
425
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -414,6 +454,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
414
454
  })]
415
455
  }), hasError && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
416
456
  style: [styles.card, styles.errorCard],
457
+ testID: `${_testIDs.REDUX_TEST_ID}-error-card`,
417
458
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SectionHeader, {
418
459
  icon: _sharedUi.AlertCircle,
419
460
  title: "ERROR",
@@ -421,6 +462,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
421
462
  style: styles.errorBadge,
422
463
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
423
464
  style: styles.errorBadgeText,
465
+ testID: `${_testIDs.REDUX_TEST_ID}-error-badge`,
424
466
  children: "REJECTED"
425
467
  })
426
468
  })
@@ -439,6 +481,7 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
439
481
  })]
440
482
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
441
483
  style: styles.card,
484
+ testID: `${_testIDs.REDUX_TEST_ID}-full-action-card`,
442
485
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SectionHeader, {
443
486
  icon: _sharedUi.FileText,
444
487
  title: "FULL ACTION OBJECT"
@@ -461,7 +504,8 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
461
504
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionButton.ReduxActionButton, {
462
505
  type: "replay",
463
506
  text: "REPLAY",
464
- onPress: handleReplay
507
+ onPress: handleReplay,
508
+ testID: `${_testIDs.REDUX_TEST_ID}-replay`
465
509
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
466
510
  style: styles.actionDescription,
467
511
  children: "Dispatch again"
@@ -471,17 +515,21 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
471
515
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionButton.ReduxActionButton, {
472
516
  type: "jump",
473
517
  text: "JUMP",
474
- onPress: handleJump
518
+ onPress: handleJump,
519
+ disabled: jumpDisabled,
520
+ testID: `${_testIDs.REDUX_TEST_ID}-jump`
475
521
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
476
522
  style: styles.actionDescription,
477
- children: "Restore this state"
523
+ testID: `${_testIDs.REDUX_TEST_ID}-jump-description`,
524
+ children: stateEvicted ? "State not retained" : canTimeTravel ? "Restore this state" : "Time travel not wired"
478
525
  })]
479
526
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
480
527
  style: styles.actionButtonWrapper,
481
528
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionButton.ReduxActionButton, {
482
529
  type: "copy",
483
530
  text: "COPY ACTION",
484
- onPress: handleCopyAction
531
+ onPress: handleCopyAction,
532
+ testID: `${_testIDs.REDUX_TEST_ID}-copy-action`
485
533
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
486
534
  style: styles.actionDescription,
487
535
  children: isPro ? "Copy to clipboard" : "Pro feature"
@@ -491,7 +539,8 @@ const ReduxActionInfoView = exports.ReduxActionInfoView = /*#__PURE__*/(0, _reac
491
539
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionButton.ReduxActionButton, {
492
540
  type: "copy",
493
541
  text: "COPY PAYLOAD",
494
- onPress: handleCopyPayload
542
+ onPress: handleCopyPayload,
543
+ testID: `${_testIDs.REDUX_TEST_ID}-copy-payload`
495
544
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
496
545
  style: styles.actionDescription,
497
546
  children: isPro ? "Copy payload only" : "Pro feature"
@@ -214,7 +214,9 @@ function getPrimaryText(action) {
214
214
  // tick re-renders only that Text, not this row.
215
215
  const ReduxActionItem = exports.ReduxActionItem = /*#__PURE__*/(0, _react.memo)(function ReduxActionItem({
216
216
  action,
217
- onPress
217
+ onPress,
218
+ linkedCount,
219
+ testID
218
220
  }) {
219
221
  const sliceName = action.sliceName ?? undefined;
220
222
  const statusColor = getStatusColor(action.category, action.hasStateChange, sliceName);
@@ -223,10 +225,11 @@ const ReduxActionItem = exports.ReduxActionItem = /*#__PURE__*/(0, _react.memo)(
223
225
  const primaryText = getPrimaryText(action);
224
226
  const actionBadge = getActionBadge(action);
225
227
 
226
- // Check for linked async actions
227
- const linkedActions = _reduxActionStore.reduxActionStore.getLinkedActions(action);
228
- const linkedCount = linkedActions.length;
229
- const hasLinkedActions = linkedCount > 1;
228
+ // Check for linked async actions. The COUNT comes from the list (see the
229
+ // prop's doc); the ordered list is only needed to say where in the flow this
230
+ // action sits, and only when there is a flow at all.
231
+ const hasLinkedActions = (linkedCount ?? 0) > 1;
232
+ const linkedActions = hasLinkedActions ? _reduxActionStore.reduxActionStore.getLinkedActions(action) : [];
230
233
 
231
234
  // Get position in async sequence (1/2, 2/2, etc.)
232
235
  const asyncPosition = hasLinkedActions ? linkedActions.findIndex(a => a.id === action.id) + 1 : 0;
@@ -251,10 +254,12 @@ const ReduxActionItem = exports.ReduxActionItem = /*#__PURE__*/(0, _react.memo)(
251
254
  style: [styles.asyncSequenceText, {
252
255
  color: opColor.text
253
256
  }],
257
+ testID: testID ? `${testID}-seq` : undefined,
254
258
  children: [asyncPosition, "/", linkedCount, " #", operationIndex]
255
259
  })
256
260
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
257
261
  style: styles.slowBadge,
262
+ testID: testID ? `${testID}-slow` : undefined,
258
263
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Clock, {
259
264
  size: 12,
260
265
  color: _sharedUi.buoyColors.error
@@ -273,10 +278,12 @@ const ReduxActionItem = exports.ReduxActionItem = /*#__PURE__*/(0, _react.memo)(
273
278
  style: [styles.asyncSequenceText, {
274
279
  color: opColor.text
275
280
  }],
281
+ testID: testID ? `${testID}-seq` : undefined,
276
282
  children: [asyncPosition, "/", linkedCount, " #", operationIndex]
277
283
  })
278
284
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
279
285
  style: styles.errorBadge,
286
+ testID: testID ? `${testID}-error` : undefined,
280
287
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertCircle, {
281
288
  size: 12,
282
289
  color: _sharedUi.buoyColors.error
@@ -295,10 +302,12 @@ const ReduxActionItem = exports.ReduxActionItem = /*#__PURE__*/(0, _react.memo)(
295
302
  style: [styles.asyncSequenceText, {
296
303
  color: opColor.text
297
304
  }],
305
+ testID: testID ? `${testID}-seq` : undefined,
298
306
  children: [asyncPosition, "/", linkedCount, " #", operationIndex]
299
307
  })
300
308
  }), action.hasStateChange && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
301
309
  style: styles.changeBadge,
310
+ testID: testID ? `${testID}-change` : undefined,
302
311
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
303
312
  size: 12,
304
313
  color: _sharedUi.buoyColors.warning
@@ -313,9 +322,11 @@ const ReduxActionItem = exports.ReduxActionItem = /*#__PURE__*/(0, _react.memo)(
313
322
  style: [styles.actionBadgeText, {
314
323
  color: statusColor
315
324
  }],
325
+ testID: testID ? `${testID}-badge` : undefined,
316
326
  children: actionBadge
317
327
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
318
328
  style: styles.changeBadge,
329
+ testID: testID ? `${testID}-change` : undefined,
319
330
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
320
331
  size: 12,
321
332
  color: _sharedUi.buoyColors.warning
@@ -324,6 +335,10 @@ const ReduxActionItem = exports.ReduxActionItem = /*#__PURE__*/(0, _react.memo)(
324
335
  });
325
336
  }
326
337
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CompactRow, {
338
+ testID: testID,
339
+ statusTestID: testID ? `${testID}-status` : undefined,
340
+ statusSublabelTestID: testID ? `${testID}-sublabel` : undefined,
341
+ accessibilityLabel: `${primaryText}, ${statusLabel}, ${sublabel}`,
327
342
  statusDotColor: statusColor,
328
343
  statusLabel: statusLabel,
329
344
  statusSublabel: sublabel,
@@ -55,6 +55,13 @@ const ReduxDetailViewToggle = exports.ReduxDetailViewToggle = /*#__PURE__*/(0, _
55
55
  onPress: () => !isDisabled && onViewChange(config.key),
56
56
  activeOpacity: isDisabled ? 1 : 0.8,
57
57
  disabled: isDisabled,
58
+ testID: `buoy-redux-view-${config.key}`,
59
+ accessibilityRole: "button",
60
+ accessibilityLabel: config.label,
61
+ accessibilityState: {
62
+ selected: isActive,
63
+ disabled: isDisabled
64
+ },
58
65
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
59
66
  style: styles.cardContent,
60
67
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(IconComponent, {