@buoy-gg/storage 7.0.28 → 7.0.30

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 (54) hide show
  1. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +51 -1
  2. package/lib/commonjs/storage/components/SelectionActionBar.js +10 -0
  3. package/lib/commonjs/storage/components/StorageActionButtons.js +16 -0
  4. package/lib/commonjs/storage/components/StorageEventActionButton.js +1 -0
  5. package/lib/commonjs/storage/components/StorageEventCard.js +5 -2
  6. package/lib/commonjs/storage/components/StorageEventFilterView.js +18 -0
  7. package/lib/commonjs/storage/components/StorageFilterCards.js +9 -0
  8. package/lib/commonjs/storage/components/StorageKeyEditorScreen.js +26 -5
  9. package/lib/commonjs/storage/components/StorageKeyRow.js +48 -1
  10. package/lib/commonjs/storage/components/StorageKeySection.js +23 -7
  11. package/lib/commonjs/storage/components/StorageModalWithTabs.js +26 -7
  12. package/lib/commonjs/storage/components/StorageScalarKeyEditor.js +16 -1
  13. package/lib/commonjs/storage/components/StorageTypeFilterBar.js +2 -0
  14. package/lib/commonjs/storage/components/StorageValueEditModal.js +18 -2
  15. package/lib/commonjs/storage/hooks/useTreeDraft.js +12 -1
  16. package/lib/commonjs/storage/utils/deleteStorageKeys.js +34 -5
  17. package/lib/module/storage/components/GameUIStorageBrowser.js +51 -1
  18. package/lib/module/storage/components/SelectionActionBar.js +10 -0
  19. package/lib/module/storage/components/StorageActionButtons.js +16 -0
  20. package/lib/module/storage/components/StorageEventActionButton.js +1 -0
  21. package/lib/module/storage/components/StorageEventCard.js +5 -2
  22. package/lib/module/storage/components/StorageEventFilterView.js +18 -0
  23. package/lib/module/storage/components/StorageFilterCards.js +9 -0
  24. package/lib/module/storage/components/StorageKeyEditorScreen.js +27 -6
  25. package/lib/module/storage/components/StorageKeyRow.js +49 -2
  26. package/lib/module/storage/components/StorageKeySection.js +23 -7
  27. package/lib/module/storage/components/StorageModalWithTabs.js +26 -7
  28. package/lib/module/storage/components/StorageScalarKeyEditor.js +16 -1
  29. package/lib/module/storage/components/StorageTypeFilterBar.js +2 -0
  30. package/lib/module/storage/components/StorageValueEditModal.js +18 -2
  31. package/lib/module/storage/hooks/useTreeDraft.js +12 -1
  32. package/lib/module/storage/utils/deleteStorageKeys.js +34 -5
  33. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  34. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  35. package/lib/typescript/storage/components/StorageActionButtons.d.ts.map +1 -1
  36. package/lib/typescript/storage/components/StorageEventActionButton.d.ts.map +1 -1
  37. package/lib/typescript/storage/components/StorageEventCard.d.ts +5 -0
  38. package/lib/typescript/storage/components/StorageEventCard.d.ts.map +1 -1
  39. package/lib/typescript/storage/components/StorageEventFilterView.d.ts.map +1 -1
  40. package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
  41. package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts.map +1 -1
  42. package/lib/typescript/storage/components/StorageKeyRow.d.ts +9 -0
  43. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  44. package/lib/typescript/storage/components/StorageKeySection.d.ts +3 -1
  45. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  46. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  47. package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts.map +1 -1
  48. package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +1 -1
  49. package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -1
  50. package/lib/typescript/storage/hooks/useTreeDraft.d.ts +9 -1
  51. package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -1
  52. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts +15 -5
  53. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +1 -1
  54. package/package.json +5 -5
@@ -20,6 +20,7 @@ var _StorageActionButtons = require("./StorageActionButtons");
20
20
  var _SelectionActionBar = require("./SelectionActionBar");
21
21
  var _MMKVListener = require("../utils/MMKVListener");
22
22
  var _storageBrowserRefresh = require("../utils/storageBrowserRefresh");
23
+ var _deleteStorageKeys = require("../utils/deleteStorageKeys");
23
24
  var _setStorageValue = require("../utils/setStorageValue");
24
25
  var _license = require("@buoy-gg/license");
25
26
  var _jsxRuntime = require("react/jsx-runtime");
@@ -256,6 +257,42 @@ function GameUIStorageBrowser({
256
257
  });
257
258
  }, [handleSaveValue]);
258
259
 
260
+ /**
261
+ * Delete ONE key, from its own card.
262
+ *
263
+ * Routed through `deleteStorageKeys` exactly like the selection bar, so the
264
+ * per-backend fan-out keeps having a single implementation — the last time
265
+ * that fan-out was wrong (an MMKV method renamed upstream) it was wrong in
266
+ * silence, and a second copy is how one backend quietly stops being deleted.
267
+ *
268
+ * Pro is checked here rather than on the button because it's an upsell, not
269
+ * a capability: the row shows the action and explains the wall on press.
270
+ */
271
+ const handleDeleteKey = (0, _react.useCallback)(storageKey => {
272
+ if (!isPro) {
273
+ setShowUpgradeModal(true);
274
+ return;
275
+ }
276
+ _reactNative.Alert.alert("Delete key", `Delete "${storageKey.key}"? This cannot be undone.`, [{
277
+ text: "Cancel",
278
+ style: "cancel"
279
+ }, {
280
+ text: "Delete",
281
+ style: "destructive",
282
+ onPress: async () => {
283
+ try {
284
+ await (0, _deleteStorageKeys.deleteStorageKeys)([storageKey], mmkvInstances.map(inst => ({
285
+ id: inst.id,
286
+ instance: inst.instance
287
+ })));
288
+ refresh();
289
+ } catch (error) {
290
+ _reactNative.Alert.alert("Error", `Failed to delete key: ${error}`);
291
+ }
292
+ }
293
+ }]);
294
+ }, [isPro, mmkvInstances, refresh]);
295
+
259
296
  /** Permanent, per-key reasons a value can't be edited (never the licence). */
260
297
  const editBlockedFor = (0, _react.useCallback)(storageKey => (0, _setStorageValue.storageEditBlockedReason)(storageKey, mmkvInstances), [mmkvInstances]);
261
298
 
@@ -644,6 +681,9 @@ function GameUIStorageBrowser({
644
681
  contentContainerStyle: styles.instanceNavbarScroll,
645
682
  children: [(isPro || mmkvInstances.length === 1) && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
646
683
  onPress: () => setSelectedMMKVInstance(null),
684
+ testID: "buoy-storage-instance-all",
685
+ accessibilityRole: "button",
686
+ accessibilityLabel: "All MMKV instances",
647
687
  style: [styles.instanceNavbarButton, selectedMMKVInstance === null && styles.instanceNavbarButtonActive],
648
688
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.HardDrive, {
649
689
  size: 12,
@@ -661,6 +701,9 @@ function GameUIStorageBrowser({
661
701
  }), mmkvInstances.map((inst, index) => {
662
702
  const isLocked = index >= mmkvInstanceLimit;
663
703
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
704
+ testID: `buoy-storage-instance-${inst.id}`,
705
+ accessibilityRole: "button",
706
+ accessibilityLabel: `MMKV instance ${inst.id}`,
664
707
  onPress: () => {
665
708
  if (isLocked) {
666
709
  setShowUpgradeModal(true);
@@ -728,6 +771,8 @@ function GameUIStorageBrowser({
728
771
  style: styles.limitBanner,
729
772
  onPress: () => setShowUpgradeModal(true),
730
773
  activeOpacity: 0.8,
774
+ testID: "buoy-storage-locked-banner",
775
+ accessibilityRole: "button",
731
776
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
732
777
  style: styles.limitBannerContent,
733
778
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -765,7 +810,11 @@ function GameUIStorageBrowser({
765
810
  });
766
811
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
767
812
  style: styles.root,
768
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
813
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView
814
+ // Named because `by.type("RCTScrollView")` doesn't match on Fabric
815
+ // (renamed class) — automation scrolls below-the-fold rows by this id.
816
+ , {
817
+ testID: "buoy-storage-scroll",
769
818
  style: styles.scrollContainer,
770
819
  contentContainerStyle: styles.container,
771
820
  showsVerticalScrollIndicator: false,
@@ -815,6 +864,7 @@ function GameUIStorageBrowser({
815
864
  onHideKey: key => handleHideKeys([key]),
816
865
  onEditKey: setEditingKey,
817
866
  onToggleKey: handleToggleKey,
867
+ onDeleteKey: handleDeleteKey,
818
868
  editBlockedFor: editBlockedFor,
819
869
  pinnedKeys: pinnedKeys,
820
870
  onTogglePin: onTogglePin
@@ -86,6 +86,9 @@ function SelectionActionBar({
86
86
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
87
87
  style: styles.selectAllButton,
88
88
  onPress: allSelected ? onClearSelection : onSelectAll,
89
+ testID: "buoy-storage-select-all",
90
+ accessibilityLabel: allSelected ? "Deselect all keys" : "Select all keys",
91
+ accessibilityRole: "button",
89
92
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CheckSquare, {
90
93
  size: 14,
91
94
  color: allSelected ? _sharedUi.night.color.accent : _sharedUi.night.color.textSecondary
@@ -105,6 +108,7 @@ function SelectionActionBar({
105
108
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CopyButton, {
106
109
  value: getCopyData(),
107
110
  size: 14,
111
+ testID: "buoy-storage-selection-copy",
108
112
  buttonStyle: styles.actionButton,
109
113
  colors: {
110
114
  idle: _sharedUi.night.color.accent,
@@ -114,6 +118,9 @@ function SelectionActionBar({
114
118
  }), onHideKeys && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
115
119
  style: styles.actionButton,
116
120
  onPress: handleHideSelected,
121
+ testID: "buoy-storage-selection-hide",
122
+ accessibilityLabel: "Hide selected keys",
123
+ accessibilityRole: "button",
117
124
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.EyeOff, {
118
125
  size: 14,
119
126
  color: _sharedUi.night.color.warning
@@ -121,6 +128,9 @@ function SelectionActionBar({
121
128
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
122
129
  style: styles.actionButton,
123
130
  onPress: handleDeleteSelected,
131
+ testID: "buoy-storage-selection-delete",
132
+ accessibilityLabel: "Delete selected keys",
133
+ accessibilityRole: "button",
124
134
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
125
135
  size: 14,
126
136
  color: _sharedUi.night.color.danger
@@ -133,6 +133,9 @@ function StorageActionButtons({
133
133
  style: styles.container,
134
134
  children: [onToggleSelectMode && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
135
135
  onPress: onToggleSelectMode,
136
+ testID: "buoy-storage-select-mode",
137
+ accessibilityLabel: isSelectMode ? "Exit selection mode" : "Select keys",
138
+ accessibilityRole: "button",
136
139
  style: [styles.actionButton, isSelectMode && styles.actionButtonActive],
137
140
  children: isSelectMode ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
138
141
  style: styles.selectModeActiveContainer,
@@ -150,6 +153,7 @@ function StorageActionButtons({
150
153
  }), !isSelectMode && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CopyButton, {
151
154
  value: copyValue,
152
155
  size: 16,
156
+ testID: "buoy-storage-export-copy",
153
157
  buttonStyle: styles.actionButton,
154
158
  colors: {
155
159
  idle: _sharedUi.night.color.textSecondary,
@@ -158,6 +162,9 @@ function StorageActionButtons({
158
162
  }
159
163
  }), !isSelectMode && showClearAll && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
160
164
  onPress: handleClearAll,
165
+ testID: "buoy-storage-clear-all",
166
+ accessibilityLabel: "Clear all storage",
167
+ accessibilityRole: "button",
161
168
  style: styles.actionButton,
162
169
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
163
170
  size: 16,
@@ -165,6 +172,9 @@ function StorageActionButtons({
165
172
  })
166
173
  }), !isSelectMode && showClearAsync && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
167
174
  onPress: handleClearAsyncStorage,
175
+ testID: "buoy-storage-clear-async",
176
+ accessibilityLabel: "Clear AsyncStorage",
177
+ accessibilityRole: "button",
168
178
  style: styles.actionButton,
169
179
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
170
180
  size: 16,
@@ -172,6 +182,9 @@ function StorageActionButtons({
172
182
  })
173
183
  }), !isSelectMode && showClearAllMMKV && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
174
184
  onPress: handleClearAllMMKV,
185
+ testID: "buoy-storage-clear-mmkv-all",
186
+ accessibilityLabel: "Clear all MMKV instances",
187
+ accessibilityRole: "button",
175
188
  style: styles.actionButton,
176
189
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
177
190
  size: 16,
@@ -179,6 +192,9 @@ function StorageActionButtons({
179
192
  })
180
193
  }), !isSelectMode && showClearMMKVIndividual && mmkvInstances.length === 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
181
194
  onPress: () => handleClearMMKV(mmkvInstances[0].id, mmkvInstances[0].id),
195
+ testID: "buoy-storage-clear-mmkv",
196
+ accessibilityLabel: "Clear MMKV instance",
197
+ accessibilityRole: "button",
182
198
  style: styles.actionButton,
183
199
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
184
200
  size: 16,
@@ -53,6 +53,7 @@ function StorageEventActionButton({
53
53
  opacity: disabled ? 0.5 : 1
54
54
  }],
55
55
  activeOpacity: 0.7,
56
+ testID: `buoy-storage-action-${type}`,
56
57
  accessibilityRole: "button",
57
58
  accessibilityLabel: text,
58
59
  accessibilityState: {
@@ -82,7 +82,8 @@ const StorageEventCard = exports.StorageEventCard = /*#__PURE__*/(0, _react.memo
82
82
  data,
83
83
  onPress,
84
84
  isSelected = false,
85
- showChevron = true
85
+ showChevron = true,
86
+ testID
86
87
  }) {
87
88
  const actionColor = getActionColor(data.lastAction);
88
89
  const actionLabel = (0, _storageActionHelpers.translateStorageAction)(data.lastAction);
@@ -109,6 +110,8 @@ const StorageEventCard = exports.StorageEventCard = /*#__PURE__*/(0, _react.memo
109
110
  // Interaction
110
111
  ,
111
112
  isSelected: isSelected,
112
- onPress: onPress
113
+ onPress: onPress,
114
+ testID: testID,
115
+ accessibilityLabel: `${actionLabel} ${data.key}`
113
116
  });
114
117
  });
@@ -53,6 +53,12 @@ function StorageEventFilterView({
53
53
  borderColor: _sharedUi.macOSColors.semantic.warning + '40'
54
54
  }],
55
55
  onPress: () => onToggleStorageType('async'),
56
+ testID: "buoy-storage-backend-async",
57
+ accessibilityRole: "button",
58
+ accessibilityLabel: "Toggle AsyncStorage capture",
59
+ accessibilityState: {
60
+ selected: enabledStorageTypes.has('async')
61
+ },
56
62
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Database, {
57
63
  size: 16,
58
64
  color: enabledStorageTypes.has('async') ? _sharedUi.macOSColors.semantic.warning : _sharedUi.night.color.textTertiary
@@ -67,6 +73,12 @@ function StorageEventFilterView({
67
73
  borderColor: _sharedUi.macOSColors.semantic.info + '40'
68
74
  }],
69
75
  onPress: () => onToggleStorageType('mmkv'),
76
+ testID: "buoy-storage-backend-mmkv",
77
+ accessibilityRole: "button",
78
+ accessibilityLabel: "Toggle MMKV capture",
79
+ accessibilityState: {
80
+ selected: enabledStorageTypes.has('mmkv')
81
+ },
70
82
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.HardDrive, {
71
83
  size: 16,
72
84
  color: enabledStorageTypes.has('mmkv') ? _sharedUi.macOSColors.semantic.info : _sharedUi.night.color.textTertiary
@@ -81,6 +93,12 @@ function StorageEventFilterView({
81
93
  borderColor: _sharedUi.macOSColors.semantic.success + '40'
82
94
  }],
83
95
  onPress: () => onToggleStorageType('secure'),
96
+ testID: "buoy-storage-backend-secure",
97
+ accessibilityRole: "button",
98
+ accessibilityLabel: "Toggle Secure Storage capture",
99
+ accessibilityState: {
100
+ selected: enabledStorageTypes.has('secure')
101
+ },
84
102
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Shield, {
85
103
  size: 16,
86
104
  color: enabledStorageTypes.has('secure') ? _sharedUi.macOSColors.semantic.success : _sharedUi.night.color.textTertiary
@@ -48,6 +48,9 @@ function StorageFilterCards({
48
48
  }]],
49
49
  onPress: () => onFilterChange?.("all"),
50
50
  activeOpacity: 0.8,
51
+ testID: "buoy-storage-status-all",
52
+ accessibilityRole: "button",
53
+ accessibilityLabel: "Valid keys",
51
54
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
52
55
  style: [styles.filterValue, {
53
56
  color: _sharedUi.night.color.text
@@ -65,6 +68,9 @@ function StorageFilterCards({
65
68
  }]],
66
69
  onPress: () => onFilterChange?.("missing"),
67
70
  activeOpacity: 0.8,
71
+ testID: "buoy-storage-status-missing",
72
+ accessibilityRole: "button",
73
+ accessibilityLabel: "Missing keys",
68
74
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
69
75
  style: [styles.filterValue, {
70
76
  color: displayStats.missingCount > 0 ? _sharedUi.macOSColors.semantic.error : _sharedUi.night.color.textTertiary
@@ -82,6 +88,9 @@ function StorageFilterCards({
82
88
  }]],
83
89
  onPress: () => onFilterChange?.("issues"),
84
90
  activeOpacity: 0.8,
91
+ testID: "buoy-storage-status-issues",
92
+ accessibilityRole: "button",
93
+ accessibilityLabel: "Issues keys",
85
94
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
86
95
  style: [styles.filterValue, {
87
96
  color: displayStats.issuesCount > 0 ? _sharedUi.macOSColors.semantic.warning : _sharedUi.night.color.textTertiary
@@ -92,7 +92,7 @@ function StorageTreeEditor({
92
92
  } = editor;
93
93
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
94
94
  style: styles.screen,
95
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(EditorHeader, {
95
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolBackground, {}), /*#__PURE__*/(0, _jsxRuntime.jsx)(EditorHeader, {
96
96
  storageKey: storageKey,
97
97
  state: draft.isSaving ? "Saving…" : draft.isDirty ? "Draft · unsaved" : "In sync with device",
98
98
  dirty: draft.isDirty,
@@ -111,6 +111,7 @@ function StorageTreeEditor({
111
111
  children: summarize(draft.value)
112
112
  })]
113
113
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
114
+ testID: "buoy-storage-editor-scroll",
114
115
  style: styles.treeScroll,
115
116
  contentContainerStyle: styles.treeContent,
116
117
  keyboardShouldPersistTaps: "handled",
@@ -128,7 +129,11 @@ function StorageTreeEditor({
128
129
  ,
129
130
  showTypeFilter: false,
130
131
  onSelectionChange: editor.setSelection,
131
- selectPath: draft.selectPath,
132
+ selectPath: draft.selectPath
133
+ // Its own handle root: the card that opened this editor is still
134
+ // mounted underneath, rendering the same value in its preview tree.
135
+ ,
136
+ testIDPrefix: "buoy-storage-editor-row",
132
137
  onRequestInlineEdit: sel => editor.openValueEditor(sel.path)
133
138
  })
134
139
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -156,6 +161,8 @@ function StorageTreeEditor({
156
161
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
157
162
  onPress: draft.discard,
158
163
  disabled: draft.isSaving,
164
+ testID: "buoy-storage-editor-discard",
165
+ accessibilityRole: "button",
159
166
  accessibilityLabel: "Discard changes",
160
167
  style: styles.discardButton,
161
168
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -165,6 +172,8 @@ function StorageTreeEditor({
165
172
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
166
173
  onPress: editor.saveAndClose,
167
174
  disabled: draft.isSaving,
175
+ testID: "buoy-storage-editor-save",
176
+ accessibilityRole: "button",
168
177
  accessibilityLabel: "Save changes",
169
178
  style: [styles.saveButton, draft.isSaving && styles.saveDisabled],
170
179
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -200,6 +209,8 @@ function EditorHeader({
200
209
  style: styles.header,
201
210
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
202
211
  onPress: onClose,
212
+ testID: "buoy-storage-editor-close",
213
+ accessibilityRole: "button",
203
214
  accessibilityLabel: "Close editor",
204
215
  style: styles.headerButton,
205
216
  hitSlop: {
@@ -214,7 +225,13 @@ function EditorHeader({
214
225
  })
215
226
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
216
227
  style: styles.headerTitle,
217
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
228
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text
229
+ // Which key this editor is pointed at. Worth a handle of its own:
230
+ // the editor is reached by expanding a card in a list of hundreds,
231
+ // and opening it on the wrong one looks exactly like opening it on
232
+ // the right one until a write lands somewhere unexpected.
233
+ , {
234
+ testID: "buoy-storage-editor-key",
218
235
  style: styles.headerKey,
219
236
  numberOfLines: 1,
220
237
  children: storageKey.key
@@ -224,6 +241,8 @@ function EditorHeader({
224
241
  })]
225
242
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
226
243
  onPress: onRaw,
244
+ testID: "buoy-storage-editor-raw",
245
+ accessibilityRole: "button",
227
246
  accessibilityLabel: "Edit raw JSON",
228
247
  style: styles.headerButton,
229
248
  hitSlop: {
@@ -269,8 +288,10 @@ function storageTypeColor(storageType) {
269
288
  const styles = _reactNative.StyleSheet.create({
270
289
  screen: {
271
290
  flex: 1,
272
- // Transparent so the night sheet's star field shows through.
273
- backgroundColor: "transparent"
291
+ // Opaque on purpose: transparency here showed the whole key list through
292
+ // the editor, not just the sheet's star field. The screen paints its own
293
+ // ToolBackground instead — see the mount site above.
294
+ backgroundColor: _sharedUi.night.color.bg
274
295
  },
275
296
  header: {
276
297
  flexDirection: "row",
@@ -92,6 +92,7 @@ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(func
92
92
  onTogglePin,
93
93
  onEditKey,
94
94
  onToggleKey,
95
+ onDeleteKey,
95
96
  editBlockedReason
96
97
  }) {
97
98
  const canEdit = onEditKey != null && editBlockedReason == null;
@@ -141,6 +142,9 @@ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(func
141
142
  children: String(eventCount)
142
143
  }), eventCount > 1 && onViewHistory && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
143
144
  onPress: onViewHistory,
145
+ testID: "buoy-storage-key-history",
146
+ accessibilityLabel: `View history for ${storageKey.key}`,
147
+ accessibilityRole: "button",
144
148
  style: styles.viewHistoryButton,
145
149
  hitSlop: {
146
150
  top: 6,
@@ -220,7 +224,14 @@ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(func
220
224
  }) : null, onTogglePin || onHideKey || canEdit ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
221
225
  style: styles.actionRow,
222
226
  children: [canEdit ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
223
- style: styles.editButton,
227
+ style: styles.editButton
228
+ // One handle for both jobs on purpose: this is the card's single
229
+ // "change it" affordance, and which of the two it performs is the
230
+ // key's type, not a different control.
231
+ ,
232
+ testID: "buoy-storage-key-edit",
233
+ accessibilityLabel: isBoolean && onToggleKey ? `Toggle ${storageKey.key}` : `Edit ${storageKey.key}`,
234
+ accessibilityRole: "button",
224
235
  onPress: () => isBoolean && onToggleKey ? onToggleKey(storageKey) : onEditKey?.(storageKey),
225
236
  hitSlop: {
226
237
  top: 6,
@@ -237,6 +248,9 @@ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(func
237
248
  })]
238
249
  }) : null, onTogglePin ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
239
250
  style: [styles.actionChip, isPinned && styles.actionChipActive],
251
+ testID: "buoy-storage-key-pin",
252
+ accessibilityLabel: isPinned ? `Unpin ${storageKey.key}` : `Pin ${storageKey.key} to top`,
253
+ accessibilityRole: "button",
240
254
  onPress: () => onTogglePin(storageKey.key),
241
255
  hitSlop: {
242
256
  top: 6,
@@ -256,6 +270,9 @@ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(func
256
270
  })]
257
271
  }) : null, onHideKey ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
258
272
  style: styles.actionChip,
273
+ testID: "buoy-storage-key-hide",
274
+ accessibilityLabel: `Hide ${storageKey.key} from list`,
275
+ accessibilityRole: "button",
259
276
  onPress: () => onHideKey(storageKey),
260
277
  hitSlop: {
261
278
  top: 6,
@@ -272,6 +289,27 @@ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(func
272
289
  }],
273
290
  children: "Hide from list"
274
291
  })]
292
+ }) : null, canEdit && onDeleteKey ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
293
+ style: [styles.actionChip, styles.dangerChip],
294
+ testID: "buoy-storage-key-delete",
295
+ accessibilityLabel: `Delete key ${storageKey.key}`,
296
+ accessibilityRole: "button",
297
+ onPress: () => onDeleteKey(storageKey),
298
+ hitSlop: {
299
+ top: 6,
300
+ bottom: 6,
301
+ left: 6,
302
+ right: 6
303
+ },
304
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
305
+ size: 12,
306
+ color: _sharedUi.night.color.danger
307
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
308
+ style: [styles.actionChipText, {
309
+ color: _sharedUi.night.color.danger
310
+ }],
311
+ children: "Delete key"
312
+ })]
275
313
  }) : null]
276
314
  }) : null]
277
315
  });
@@ -284,6 +322,12 @@ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(func
284
322
  // Create checkbox element for select mode
285
323
  const selectionCheckbox = isSelectMode ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
286
324
  onPress: handleCheckboxPress,
325
+ testID: `buoy-storage-checkbox-${storageKey.storageType}-${storageKey.key}`,
326
+ accessibilityLabel: `Select ${storageKey.key}`,
327
+ accessibilityRole: "checkbox",
328
+ accessibilityState: {
329
+ checked: isSelected
330
+ },
287
331
  style: styles.checkboxContainer,
288
332
  hitSlop: {
289
333
  top: 8,
@@ -457,6 +501,9 @@ const styles = _reactNative.StyleSheet.create({
457
501
  borderWidth: 1,
458
502
  borderColor: _sharedUi.night.color.border
459
503
  },
504
+ dangerChip: {
505
+ borderColor: _sharedUi.night.color.danger + "55"
506
+ },
460
507
  actionChipActive: {
461
508
  backgroundColor: _sharedUi.night.color.accentSoft,
462
509
  borderColor: _sharedUi.night.color.accentBorderStrong
@@ -33,17 +33,32 @@ function StorageKeySection({
33
33
  onTogglePin,
34
34
  onEditKey,
35
35
  onToggleKey,
36
+ onDeleteKey,
36
37
  editBlockedFor
37
38
  }) {
38
- const [expandedKey, setExpandedKey] = (0, _react.useState)(null);
39
- const handleKeyPress = (0, _react.useCallback)(storageKey => {
40
- setExpandedKey(prev => prev === storageKey.key ? null : storageKey.key);
41
- }, []);
42
-
43
- // Generate unique identifier for a storage key
39
+ /**
40
+ * Identifies a ROW, not a key name.
41
+ *
42
+ * The same name routinely exists in more than one place — an app that keeps
43
+ * `auth.refreshToken` in both MMKV and the keychain has two rows called
44
+ * that, and they are different values in different stores.
45
+ */
44
46
  const getKeyIdentifier = (0, _react.useCallback)(storageKey => {
45
47
  return storageKey.instanceId ? `${storageKey.storageType}-${storageKey.instanceId}-${storageKey.key}` : `${storageKey.storageType}-${storageKey.key}`;
46
48
  }, []);
49
+
50
+ /**
51
+ * Which ROW is expanded — by identifier, not by key name.
52
+ *
53
+ * Keyed by name, tapping one of a pair of same-named rows opened BOTH of
54
+ * them: two cards, two value trees, and two Edit buttons, one of which
55
+ * writes to a store you weren't looking at.
56
+ */
57
+ const [expandedKey, setExpandedKey] = (0, _react.useState)(null);
58
+ const handleKeyPress = (0, _react.useCallback)(storageKey => {
59
+ const id = getKeyIdentifier(storageKey);
60
+ setExpandedKey(prev => prev === id ? null : id);
61
+ }, [getKeyIdentifier]);
47
62
  if (keys.length === 0 && title === "Required Keys") {
48
63
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
49
64
  style: styles.sectionContainer,
@@ -80,7 +95,7 @@ function StorageKeySection({
80
95
  const uniqueKey = getKeyIdentifier(storageKey);
81
96
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_StorageKeyRow.StorageKeyRow, {
82
97
  storageKey: storageKey,
83
- isExpanded: !isSelectMode && expandedKey === storageKey.key,
98
+ isExpanded: !isSelectMode && expandedKey === uniqueKey,
84
99
  onPress: handleKeyPress,
85
100
  isSelectMode: isSelectMode,
86
101
  isSelected: selectedKeys.has(uniqueKey),
@@ -90,6 +105,7 @@ function StorageKeySection({
90
105
  onHideKey: onHideKey,
91
106
  onEditKey: onEditKey,
92
107
  onToggleKey: onToggleKey,
108
+ onDeleteKey: onDeleteKey,
93
109
  editBlockedReason: editBlockedFor?.(storageKey),
94
110
  isPinned: pinnedKeys?.has(storageKey.key),
95
111
  onTogglePin: onTogglePin
@@ -550,7 +550,8 @@ function StorageModalWithTabs({
550
550
  };
551
551
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_StorageEventCard.StorageEventCard, {
552
552
  data: cardData,
553
- onPress: () => handleRawEventPress(item)
553
+ onPress: () => handleRawEventPress(item),
554
+ testID: `buoy-storage-event-${item.id}`
554
555
  });
555
556
  }, [handleRawEventPress]);
556
557
  if (!visible) return null;
@@ -587,6 +588,7 @@ function StorageModalWithTabs({
587
588
  const ascSortedEvents = [...selectedHistoryConversation.events].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
588
589
  const newestFirstEvents = [...ascSortedEvents].reverse();
589
590
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
591
+ testID: "buoy-storage-history-list",
590
592
  style: styles.flexFill,
591
593
  data: newestFirstEvents,
592
594
  keyExtractor: item => item.id,
@@ -604,7 +606,8 @@ function StorageModalWithTabs({
604
606
  };
605
607
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_StorageEventCard.StorageEventCard, {
606
608
  data: cardData,
607
- onPress: () => setSelectedHistoryEventIndex(ascIdx >= 0 ? ascIdx : 0)
609
+ onPress: () => setSelectedHistoryEventIndex(ascIdx >= 0 ? ascIdx : 0),
610
+ testID: `buoy-storage-history-event-${item.id}`
608
611
  });
609
612
  },
610
613
  contentContainerStyle: styles.listContent,
@@ -682,6 +685,7 @@ function StorageModalWithTabs({
682
685
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
683
686
  style: styles.flexFill,
684
687
  children: [eventTypeFilterBar, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
688
+ testID: "buoy-storage-events-list",
685
689
  style: styles.flexFill,
686
690
  data: visibleEvents,
687
691
  renderItem: renderEventItem,
@@ -759,6 +763,7 @@ function StorageModalWithTabs({
759
763
  setIsSearchActive(false);
760
764
  },
761
765
  placeholder: "Search storage keys...",
766
+ testID: "buoy-storage-search",
762
767
  autoFocus: true,
763
768
  onSubmitEditing: () => setIsSearchActive(false),
764
769
  returnKeyType: "search",
@@ -773,6 +778,7 @@ function StorageModalWithTabs({
773
778
  label: `Events${filteredEvents.length > 0 ? ` (${filteredEvents.length})` : ""}`
774
779
  }],
775
780
  activeTab: activeTab,
781
+ testIDPrefix: "buoy-storage-tab",
776
782
  onTabChange: tab => {
777
783
  setActiveTab(tab);
778
784
  setSelectedHistoryKey(null);
@@ -785,13 +791,18 @@ function StorageModalWithTabs({
785
791
  children: [onRefresh && !isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
786
792
  onPress: onRefresh,
787
793
  style: styles.iconButton,
794
+ testID: "buoy-storage-refresh",
788
795
  accessibilityLabel: "Refresh storage",
796
+ accessibilityRole: "button",
789
797
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RefreshCw, {
790
798
  size: 14,
791
799
  color: _sharedUi.night.color.textSecondary
792
800
  })
793
801
  }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
794
802
  onPress: handleToggleFilters,
803
+ testID: "buoy-storage-filters",
804
+ accessibilityLabel: "Storage filters",
805
+ accessibilityRole: "button",
795
806
  style: [styles.iconButton, (ignoredPatterns.size > 0 || enabledStorageTypes.size < 3) && styles.activeFilterButton],
796
807
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
797
808
  size: 14,
@@ -800,6 +811,9 @@ function StorageModalWithTabs({
800
811
  }), activeTab === "browser" && !isSearchActive && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
801
812
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
802
813
  onPress: () => setIsSearchActive(true),
814
+ testID: "buoy-storage-search-open",
815
+ accessibilityLabel: "Open search",
816
+ accessibilityRole: "button",
803
817
  style: styles.iconButton,
804
818
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
805
819
  size: 14,
@@ -808,20 +822,24 @@ function StorageModalWithTabs({
808
822
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
809
823
  value: getStorageSnapshot,
810
824
  disabled: storageDataRef.current.length === 0,
811
- buttonStyle: styles.iconButton
825
+ buttonStyle: styles.iconButton,
826
+ testID: "buoy-storage-copy"
812
827
  })]
813
828
  }), activeTab === "events" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
814
829
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
815
830
  value: getEventsSnapshot,
816
831
  disabled: filteredEvents.length === 0,
817
- buttonStyle: styles.iconButton
832
+ buttonStyle: styles.iconButton,
833
+ testID: "buoy-storage-events-copy"
818
834
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
819
835
  isEnabled: isListening,
820
836
  onToggle: handleToggleListening,
821
- accessibilityLabel: "Toggle storage event monitoring"
837
+ accessibilityLabel: "Toggle storage event monitoring",
838
+ testID: "buoy-storage-power"
822
839
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarClearButton, {
823
840
  onPress: handleClearEvents,
824
- buttonStyle: styles.iconButton
841
+ buttonStyle: styles.iconButton,
842
+ testID: "buoy-storage-clear-events"
825
843
  })]
826
844
  })]
827
845
  })]
@@ -891,7 +909,8 @@ const styles = _reactNative.StyleSheet.create({
891
909
  paddingVertical: 8,
892
910
  borderBottomWidth: 1,
893
911
  borderBottomColor: _sharedUi.macOSColors.border.default,
894
- backgroundColor: _sharedUi.macOSColors.background.card
912
+ // Clear, like Bench — the host's hoisted sky shows through the header.
913
+ backgroundColor: "transparent"
895
914
  },
896
915
  headerSearchContainer: {
897
916
  flex: 1