@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
@@ -21,6 +21,21 @@ var _jsxRuntime = require("react/jsx-runtime");
21
21
  * screen it came from.
22
22
  */
23
23
 
24
+ /**
25
+ * The text `setStorageValue` should store.
26
+ *
27
+ * A string is stored AS ITSELF — `JSON.stringify` here was adding quotes a
28
+ * plain string never had. AsyncStorage hid it (the browser reads values back
29
+ * through `JSON.parse`, so the tool agreed with itself) but the app's own
30
+ * `getItem` saw `"dark"` where it had written `dark`; MMKV stores strings
31
+ * natively and showed the quotes right in the row, compounding on every edit.
32
+ *
33
+ * Everything else keeps JSON: for a number or a boolean that IS its text, and
34
+ * containers never reach this editor.
35
+ */
36
+ function serializeScalar(value) {
37
+ return typeof value === "string" ? value : JSON.stringify(value);
38
+ }
24
39
  function StorageScalarKeyEditor({
25
40
  storageKey,
26
41
  value,
@@ -31,7 +46,7 @@ function StorageScalarKeyEditor({
31
46
  const save = (0, _react.useCallback)(next => {
32
47
  if (saving) return;
33
48
  setSaving(true);
34
- onSave(JSON.stringify(next)).then(onClose).catch(e => {
49
+ onSave(serializeScalar(next)).then(onClose).catch(e => {
35
50
  setSaving(false);
36
51
  _reactNative.Alert.alert("Couldn't save", e instanceof Error ? e.message : String(e));
37
52
  });
@@ -57,6 +57,8 @@ function StorageTypeFilterBar({
57
57
  const isActive = active === segment.id;
58
58
  const accent = segment.color ?? _sharedUi.night.color.accent;
59
59
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
60
+ testID: `buoy-storage-type-${segment.id}`,
61
+ accessibilityRole: "button",
60
62
  accessibilityLabel: `Filter by ${segment.label}`,
61
63
  accessibilityState: {
62
64
  selected: isActive
@@ -86,6 +86,8 @@ function StorageValueEditModal({
86
86
  style: styles.scrim,
87
87
  activeOpacity: 1,
88
88
  onPress: busy ? undefined : onCancel,
89
+ testID: "buoy-storage-value-scrim",
90
+ accessibilityRole: "button",
89
91
  accessibilityLabel: "Dismiss editor"
90
92
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
91
93
  style: styles.panel,
@@ -115,6 +117,8 @@ function StorageValueEditModal({
115
117
  style: styles.fieldScroll,
116
118
  keyboardShouldPersistTaps: "handled",
117
119
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
120
+ testID: "buoy-storage-value-input",
121
+ accessibilityLabel: "Value",
118
122
  value: text,
119
123
  onChangeText: setText,
120
124
  autoFocus: true,
@@ -125,8 +129,14 @@ function StorageValueEditModal({
125
129
  // Multiline for anything textual: stored values are long more
126
130
  // often than they're short, and a single line hides that.
127
131
  ,
128
- multiline: mode !== "number",
129
- keyboardType: mode === "number" ? "numeric" : "default",
132
+ multiline: mode !== "number"
133
+ // ASCII for raw JSON, because iOS Smart Punctuation is ON by
134
+ // default and rewrites the quotes you type as curly ones —
135
+ // which makes the text unparseable, disables Save, and leaves
136
+ // an "invalid JSON" message whose cause is invisible at a
137
+ // glance. An ASCII keyboard doesn't do the substitution.
138
+ ,
139
+ keyboardType: mode === "number" ? "numeric" : mode === "raw" ? "ascii-capable" : "default",
130
140
  onSubmitEditing: mode === "number" ? commit : undefined,
131
141
  placeholder: mode === "raw" ? "{ }" : "value",
132
142
  placeholderTextColor: _sharedUi.night.color.placeholder,
@@ -140,6 +150,8 @@ function StorageValueEditModal({
140
150
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
141
151
  onPress: onCancel,
142
152
  disabled: busy,
153
+ testID: "buoy-storage-value-cancel",
154
+ accessibilityRole: "button",
143
155
  accessibilityLabel: "Cancel edit",
144
156
  style: styles.cancelButton,
145
157
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -149,6 +161,8 @@ function StorageValueEditModal({
149
161
  }), mode === "boolean" ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
150
162
  onPress: commit,
151
163
  disabled: blocked,
164
+ testID: "buoy-storage-value-save",
165
+ accessibilityRole: "button",
152
166
  accessibilityLabel: saveLabel,
153
167
  style: [styles.saveButton, blocked && styles.saveDisabled],
154
168
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -169,6 +183,8 @@ function BooleanChoice({
169
183
  style: styles.boolRow,
170
184
  children: [true, false].map(option => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
171
185
  onPress: () => onPick(option),
186
+ testID: `buoy-storage-value-${option}`,
187
+ accessibilityRole: "button",
172
188
  accessibilityLabel: `Set ${option}`,
173
189
  style: [styles.boolButton, current === option && styles.boolActive],
174
190
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -66,12 +66,23 @@ function useTreeDraft(liveValue, save) {
66
66
  const selectAt = (0, _react.useCallback)(path => {
67
67
  setSelectPath(path);
68
68
  }, []);
69
+
70
+ /** Leave edit mode entirely. Only correct on the way out (after a write). */
69
71
  const reset = (0, _react.useCallback)(() => {
70
72
  setIsEditing(false);
71
73
  setDraft(undefined);
72
74
  setSelectPath(undefined);
73
75
  setError(null);
74
76
  }, []);
77
+
78
+ /** Re-seed the draft from what's stored, staying in edit mode. */
79
+ const discard = (0, _react.useCallback)(() => {
80
+ setDraft(liveValue);
81
+ setBaseline(JSON.stringify(liveValue));
82
+ setSelectPath(undefined);
83
+ setError(null);
84
+ setIsEditing(true);
85
+ }, [liveValue]);
75
86
  const commit = (0, _react.useCallback)(async () => {
76
87
  if (!isDirty || !save) {
77
88
  reset();
@@ -102,6 +113,6 @@ function useTreeDraft(liveValue, save) {
102
113
  apply,
103
114
  selectAt,
104
115
  commit,
105
- discard: reset
116
+ discard
106
117
  };
107
118
  }
@@ -11,15 +11,44 @@ var _SecureStoreRegistry = require("./SecureStoreRegistry");
11
11
  *
12
12
  * Each backend removes keys differently — AsyncStorage takes a batch,
13
13
  * SecureStore has to go through the registry (only it knows each key's
14
- * `keychainService` options), and MMKV deletes per registered instance. Two call
15
- * sites need this (the selection action bar and a single row's "Delete key"),
16
- * and a second copy of that fan-out is exactly how one backend quietly stops
17
- * being cleaned up.
14
+ * `keychainService` options), and MMKV deletes per registered instance. A
15
+ * second copy of that fan-out is exactly how one backend quietly stops being
16
+ * cleaned up — which is not hypothetical: the MMKV branch called a method
17
+ * react-native-mmkv v4 had renamed, and deleting an MMKV key silently did
18
+ * nothing until an e2e test deleted from all three backends at once.
19
+ *
20
+ * One call site today: the selection action bar. Deleting is deliberately not
21
+ * an affordance on the row itself — see `StorageKeyRow`, whose expanded card
22
+ * carries three actions and only one of them changes anything.
18
23
  *
19
24
  * Pro gating and confirmation are deliberately NOT here: they belong to the UI
20
25
  * that has the user's attention. This function assumes the caller already asked.
21
26
  */
22
27
 
28
+ /**
29
+ * Remove one key from an MMKV instance, whichever version it is.
30
+ *
31
+ * react-native-mmkv v4 renamed `delete(key)` to `remove(key)`. Calling the old
32
+ * name on a v4 instance throws, and because the callers wrap deletion in a
33
+ * try/catch that only surfaces an alert, the visible result was a key that
34
+ * refused to delete — the same shape as a permissions problem or a read-only
35
+ * instance, and nothing to point at the real cause.
36
+ *
37
+ * `storageSyncAdapter` and `snapshotProvider` already did this; this path was
38
+ * the one that missed it.
39
+ */
40
+ function removeMMKVKey(instance, key) {
41
+ if (typeof instance.remove === "function") {
42
+ instance.remove(key);
43
+ return;
44
+ }
45
+ if (typeof instance.delete === "function") {
46
+ instance.delete(key);
47
+ return;
48
+ }
49
+ throw new Error("MMKV instance exposes neither remove() nor delete()");
50
+ }
51
+
23
52
  /**
24
53
  * Removes every key from whichever backend holds it. Rejects on the first
25
54
  * failure — callers surface that, rather than reporting a partial delete as
@@ -46,7 +75,7 @@ async function deleteStorageKeys(keys, mmkvInstances) {
46
75
  Object.entries(byInstance).forEach(([instanceId, instanceKeys]) => {
47
76
  const inst = mmkvInstances.find(i => i.id === instanceId);
48
77
  if (inst) {
49
- instanceKeys.forEach(key => inst.instance.delete(key));
78
+ instanceKeys.forEach(key => removeMMKVKey(inst.instance, key));
50
79
  }
51
80
  });
52
81
  }
@@ -19,6 +19,7 @@ import { SelectionActionBar } from "./SelectionActionBar";
19
19
  // instance, including remote-backed wrappers on the desktop dashboard.
20
20
  import { addMMKVListener } from "../utils/MMKVListener";
21
21
  import { onStorageBrowserRefresh } from "../utils/storageBrowserRefresh";
22
+ import { deleteStorageKeys } from "../utils/deleteStorageKeys";
22
23
  import { setStorageValue, storageEditBlockedReason } from "../utils/setStorageValue";
23
24
 
24
25
  // Import shared Game UI components
@@ -254,6 +255,42 @@ export function GameUIStorageBrowser({
254
255
  });
255
256
  }, [handleSaveValue]);
256
257
 
258
+ /**
259
+ * Delete ONE key, from its own card.
260
+ *
261
+ * Routed through `deleteStorageKeys` exactly like the selection bar, so the
262
+ * per-backend fan-out keeps having a single implementation — the last time
263
+ * that fan-out was wrong (an MMKV method renamed upstream) it was wrong in
264
+ * silence, and a second copy is how one backend quietly stops being deleted.
265
+ *
266
+ * Pro is checked here rather than on the button because it's an upsell, not
267
+ * a capability: the row shows the action and explains the wall on press.
268
+ */
269
+ const handleDeleteKey = useCallback(storageKey => {
270
+ if (!isPro) {
271
+ setShowUpgradeModal(true);
272
+ return;
273
+ }
274
+ Alert.alert("Delete key", `Delete "${storageKey.key}"? This cannot be undone.`, [{
275
+ text: "Cancel",
276
+ style: "cancel"
277
+ }, {
278
+ text: "Delete",
279
+ style: "destructive",
280
+ onPress: async () => {
281
+ try {
282
+ await deleteStorageKeys([storageKey], mmkvInstances.map(inst => ({
283
+ id: inst.id,
284
+ instance: inst.instance
285
+ })));
286
+ refresh();
287
+ } catch (error) {
288
+ Alert.alert("Error", `Failed to delete key: ${error}`);
289
+ }
290
+ }
291
+ }]);
292
+ }, [isPro, mmkvInstances, refresh]);
293
+
257
294
  /** Permanent, per-key reasons a value can't be edited (never the licence). */
258
295
  const editBlockedFor = useCallback(storageKey => storageEditBlockedReason(storageKey, mmkvInstances), [mmkvInstances]);
259
296
 
@@ -642,6 +679,9 @@ export function GameUIStorageBrowser({
642
679
  contentContainerStyle: styles.instanceNavbarScroll,
643
680
  children: [(isPro || mmkvInstances.length === 1) && /*#__PURE__*/_jsxs(TouchableOpacity, {
644
681
  onPress: () => setSelectedMMKVInstance(null),
682
+ testID: "buoy-storage-instance-all",
683
+ accessibilityRole: "button",
684
+ accessibilityLabel: "All MMKV instances",
645
685
  style: [styles.instanceNavbarButton, selectedMMKVInstance === null && styles.instanceNavbarButtonActive],
646
686
  children: [/*#__PURE__*/_jsx(HardDrive, {
647
687
  size: 12,
@@ -659,6 +699,9 @@ export function GameUIStorageBrowser({
659
699
  }), mmkvInstances.map((inst, index) => {
660
700
  const isLocked = index >= mmkvInstanceLimit;
661
701
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
702
+ testID: `buoy-storage-instance-${inst.id}`,
703
+ accessibilityRole: "button",
704
+ accessibilityLabel: `MMKV instance ${inst.id}`,
662
705
  onPress: () => {
663
706
  if (isLocked) {
664
707
  setShowUpgradeModal(true);
@@ -726,6 +769,8 @@ export function GameUIStorageBrowser({
726
769
  style: styles.limitBanner,
727
770
  onPress: () => setShowUpgradeModal(true),
728
771
  activeOpacity: 0.8,
772
+ testID: "buoy-storage-locked-banner",
773
+ accessibilityRole: "button",
729
774
  children: [/*#__PURE__*/_jsxs(View, {
730
775
  style: styles.limitBannerContent,
731
776
  children: [/*#__PURE__*/_jsxs(View, {
@@ -763,7 +808,11 @@ export function GameUIStorageBrowser({
763
808
  });
764
809
  return /*#__PURE__*/_jsxs(View, {
765
810
  style: styles.root,
766
- children: [/*#__PURE__*/_jsxs(ScrollView, {
811
+ children: [/*#__PURE__*/_jsxs(ScrollView
812
+ // Named because `by.type("RCTScrollView")` doesn't match on Fabric
813
+ // (renamed class) — automation scrolls below-the-fold rows by this id.
814
+ , {
815
+ testID: "buoy-storage-scroll",
767
816
  style: styles.scrollContainer,
768
817
  contentContainerStyle: styles.container,
769
818
  showsVerticalScrollIndicator: false,
@@ -813,6 +862,7 @@ export function GameUIStorageBrowser({
813
862
  onHideKey: key => handleHideKeys([key]),
814
863
  onEditKey: setEditingKey,
815
864
  onToggleKey: handleToggleKey,
865
+ onDeleteKey: handleDeleteKey,
816
866
  editBlockedFor: editBlockedFor,
817
867
  pinnedKeys: pinnedKeys,
818
868
  onTogglePin: onTogglePin
@@ -83,6 +83,9 @@ export function SelectionActionBar({
83
83
  children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
84
84
  style: styles.selectAllButton,
85
85
  onPress: allSelected ? onClearSelection : onSelectAll,
86
+ testID: "buoy-storage-select-all",
87
+ accessibilityLabel: allSelected ? "Deselect all keys" : "Select all keys",
88
+ accessibilityRole: "button",
86
89
  children: [/*#__PURE__*/_jsx(CheckSquare, {
87
90
  size: 14,
88
91
  color: allSelected ? night.color.accent : night.color.textSecondary
@@ -102,6 +105,7 @@ export function SelectionActionBar({
102
105
  children: [/*#__PURE__*/_jsx(CopyButton, {
103
106
  value: getCopyData(),
104
107
  size: 14,
108
+ testID: "buoy-storage-selection-copy",
105
109
  buttonStyle: styles.actionButton,
106
110
  colors: {
107
111
  idle: night.color.accent,
@@ -111,6 +115,9 @@ export function SelectionActionBar({
111
115
  }), onHideKeys && /*#__PURE__*/_jsx(TouchableOpacity, {
112
116
  style: styles.actionButton,
113
117
  onPress: handleHideSelected,
118
+ testID: "buoy-storage-selection-hide",
119
+ accessibilityLabel: "Hide selected keys",
120
+ accessibilityRole: "button",
114
121
  children: /*#__PURE__*/_jsx(EyeOff, {
115
122
  size: 14,
116
123
  color: night.color.warning
@@ -118,6 +125,9 @@ export function SelectionActionBar({
118
125
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
119
126
  style: styles.actionButton,
120
127
  onPress: handleDeleteSelected,
128
+ testID: "buoy-storage-selection-delete",
129
+ accessibilityLabel: "Delete selected keys",
130
+ accessibilityRole: "button",
121
131
  children: /*#__PURE__*/_jsx(Trash2, {
122
132
  size: 14,
123
133
  color: night.color.danger
@@ -129,6 +129,9 @@ export function StorageActionButtons({
129
129
  style: styles.container,
130
130
  children: [onToggleSelectMode && /*#__PURE__*/_jsx(TouchableOpacity, {
131
131
  onPress: onToggleSelectMode,
132
+ testID: "buoy-storage-select-mode",
133
+ accessibilityLabel: isSelectMode ? "Exit selection mode" : "Select keys",
134
+ accessibilityRole: "button",
132
135
  style: [styles.actionButton, isSelectMode && styles.actionButtonActive],
133
136
  children: isSelectMode ? /*#__PURE__*/_jsxs(View, {
134
137
  style: styles.selectModeActiveContainer,
@@ -146,6 +149,7 @@ export function StorageActionButtons({
146
149
  }), !isSelectMode && /*#__PURE__*/_jsx(CopyButton, {
147
150
  value: copyValue,
148
151
  size: 16,
152
+ testID: "buoy-storage-export-copy",
149
153
  buttonStyle: styles.actionButton,
150
154
  colors: {
151
155
  idle: night.color.textSecondary,
@@ -154,6 +158,9 @@ export function StorageActionButtons({
154
158
  }
155
159
  }), !isSelectMode && showClearAll && /*#__PURE__*/_jsx(TouchableOpacity, {
156
160
  onPress: handleClearAll,
161
+ testID: "buoy-storage-clear-all",
162
+ accessibilityLabel: "Clear all storage",
163
+ accessibilityRole: "button",
157
164
  style: styles.actionButton,
158
165
  children: /*#__PURE__*/_jsx(Trash2, {
159
166
  size: 16,
@@ -161,6 +168,9 @@ export function StorageActionButtons({
161
168
  })
162
169
  }), !isSelectMode && showClearAsync && /*#__PURE__*/_jsx(TouchableOpacity, {
163
170
  onPress: handleClearAsyncStorage,
171
+ testID: "buoy-storage-clear-async",
172
+ accessibilityLabel: "Clear AsyncStorage",
173
+ accessibilityRole: "button",
164
174
  style: styles.actionButton,
165
175
  children: /*#__PURE__*/_jsx(Trash2, {
166
176
  size: 16,
@@ -168,6 +178,9 @@ export function StorageActionButtons({
168
178
  })
169
179
  }), !isSelectMode && showClearAllMMKV && /*#__PURE__*/_jsx(TouchableOpacity, {
170
180
  onPress: handleClearAllMMKV,
181
+ testID: "buoy-storage-clear-mmkv-all",
182
+ accessibilityLabel: "Clear all MMKV instances",
183
+ accessibilityRole: "button",
171
184
  style: styles.actionButton,
172
185
  children: /*#__PURE__*/_jsx(Trash2, {
173
186
  size: 16,
@@ -175,6 +188,9 @@ export function StorageActionButtons({
175
188
  })
176
189
  }), !isSelectMode && showClearMMKVIndividual && mmkvInstances.length === 1 && /*#__PURE__*/_jsx(TouchableOpacity, {
177
190
  onPress: () => handleClearMMKV(mmkvInstances[0].id, mmkvInstances[0].id),
191
+ testID: "buoy-storage-clear-mmkv",
192
+ accessibilityLabel: "Clear MMKV instance",
193
+ accessibilityRole: "button",
178
194
  style: styles.actionButton,
179
195
  children: /*#__PURE__*/_jsx(Trash2, {
180
196
  size: 16,
@@ -50,6 +50,7 @@ export function StorageEventActionButton({
50
50
  opacity: disabled ? 0.5 : 1
51
51
  }],
52
52
  activeOpacity: 0.7,
53
+ testID: `buoy-storage-action-${type}`,
53
54
  accessibilityRole: "button",
54
55
  accessibilityLabel: text,
55
56
  accessibilityState: {
@@ -77,7 +77,8 @@ export const StorageEventCard = /*#__PURE__*/memo(function StorageEventCard({
77
77
  data,
78
78
  onPress,
79
79
  isSelected = false,
80
- showChevron = true
80
+ showChevron = true,
81
+ testID
81
82
  }) {
82
83
  const actionColor = getActionColor(data.lastAction);
83
84
  const actionLabel = translateStorageAction(data.lastAction);
@@ -104,6 +105,8 @@ export const StorageEventCard = /*#__PURE__*/memo(function StorageEventCard({
104
105
  // Interaction
105
106
  ,
106
107
  isSelected: isSelected,
107
- onPress: onPress
108
+ onPress: onPress,
109
+ testID: testID,
110
+ accessibilityLabel: `${actionLabel} ${data.key}`
108
111
  });
109
112
  });
@@ -49,6 +49,12 @@ export function StorageEventFilterView({
49
49
  borderColor: macOSColors.semantic.warning + '40'
50
50
  }],
51
51
  onPress: () => onToggleStorageType('async'),
52
+ testID: "buoy-storage-backend-async",
53
+ accessibilityRole: "button",
54
+ accessibilityLabel: "Toggle AsyncStorage capture",
55
+ accessibilityState: {
56
+ selected: enabledStorageTypes.has('async')
57
+ },
52
58
  children: [/*#__PURE__*/_jsx(Database, {
53
59
  size: 16,
54
60
  color: enabledStorageTypes.has('async') ? macOSColors.semantic.warning : night.color.textTertiary
@@ -63,6 +69,12 @@ export function StorageEventFilterView({
63
69
  borderColor: macOSColors.semantic.info + '40'
64
70
  }],
65
71
  onPress: () => onToggleStorageType('mmkv'),
72
+ testID: "buoy-storage-backend-mmkv",
73
+ accessibilityRole: "button",
74
+ accessibilityLabel: "Toggle MMKV capture",
75
+ accessibilityState: {
76
+ selected: enabledStorageTypes.has('mmkv')
77
+ },
66
78
  children: [/*#__PURE__*/_jsx(HardDrive, {
67
79
  size: 16,
68
80
  color: enabledStorageTypes.has('mmkv') ? macOSColors.semantic.info : night.color.textTertiary
@@ -77,6 +89,12 @@ export function StorageEventFilterView({
77
89
  borderColor: macOSColors.semantic.success + '40'
78
90
  }],
79
91
  onPress: () => onToggleStorageType('secure'),
92
+ testID: "buoy-storage-backend-secure",
93
+ accessibilityRole: "button",
94
+ accessibilityLabel: "Toggle Secure Storage capture",
95
+ accessibilityState: {
96
+ selected: enabledStorageTypes.has('secure')
97
+ },
80
98
  children: [/*#__PURE__*/_jsx(Shield, {
81
99
  size: 16,
82
100
  color: enabledStorageTypes.has('secure') ? macOSColors.semantic.success : night.color.textTertiary
@@ -44,6 +44,9 @@ export function StorageFilterCards({
44
44
  }]],
45
45
  onPress: () => onFilterChange?.("all"),
46
46
  activeOpacity: 0.8,
47
+ testID: "buoy-storage-status-all",
48
+ accessibilityRole: "button",
49
+ accessibilityLabel: "Valid keys",
47
50
  children: [/*#__PURE__*/_jsx(Text, {
48
51
  style: [styles.filterValue, {
49
52
  color: night.color.text
@@ -61,6 +64,9 @@ export function StorageFilterCards({
61
64
  }]],
62
65
  onPress: () => onFilterChange?.("missing"),
63
66
  activeOpacity: 0.8,
67
+ testID: "buoy-storage-status-missing",
68
+ accessibilityRole: "button",
69
+ accessibilityLabel: "Missing keys",
64
70
  children: [/*#__PURE__*/_jsx(Text, {
65
71
  style: [styles.filterValue, {
66
72
  color: displayStats.missingCount > 0 ? macOSColors.semantic.error : night.color.textTertiary
@@ -78,6 +84,9 @@ export function StorageFilterCards({
78
84
  }]],
79
85
  onPress: () => onFilterChange?.("issues"),
80
86
  activeOpacity: 0.8,
87
+ testID: "buoy-storage-status-issues",
88
+ accessibilityRole: "button",
89
+ accessibilityLabel: "Issues keys",
81
90
  children: [/*#__PURE__*/_jsx(Text, {
82
91
  style: [styles.filterValue, {
83
92
  color: displayStats.issuesCount > 0 ? macOSColors.semantic.warning : night.color.textTertiary
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { useMemo } from "react";
21
21
  import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
22
- import { macOSColors, night, FileJson, PillBadge, useSafeAreaInsets, X } from "@buoy-gg/shared-ui";
22
+ import { macOSColors, night, FileJson, PillBadge, ToolBackground, useSafeAreaInsets, X } from "@buoy-gg/shared-ui";
23
23
  import { DataViewer, DataTreeActionDock, getAtPath } from "@buoy-gg/shared-ui/dataViewer";
24
24
  import { useKeyEditor } from "../hooks/useKeyEditor";
25
25
  import { StorageValueEditModal } from "./StorageValueEditModal";
@@ -88,7 +88,7 @@ function StorageTreeEditor({
88
88
  } = editor;
89
89
  return /*#__PURE__*/_jsxs(View, {
90
90
  style: styles.screen,
91
- children: [/*#__PURE__*/_jsx(EditorHeader, {
91
+ children: [/*#__PURE__*/_jsx(ToolBackground, {}), /*#__PURE__*/_jsx(EditorHeader, {
92
92
  storageKey: storageKey,
93
93
  state: draft.isSaving ? "Saving…" : draft.isDirty ? "Draft · unsaved" : "In sync with device",
94
94
  dirty: draft.isDirty,
@@ -107,6 +107,7 @@ function StorageTreeEditor({
107
107
  children: summarize(draft.value)
108
108
  })]
109
109
  }), /*#__PURE__*/_jsxs(ScrollView, {
110
+ testID: "buoy-storage-editor-scroll",
110
111
  style: styles.treeScroll,
111
112
  contentContainerStyle: styles.treeContent,
112
113
  keyboardShouldPersistTaps: "handled",
@@ -124,7 +125,11 @@ function StorageTreeEditor({
124
125
  ,
125
126
  showTypeFilter: false,
126
127
  onSelectionChange: editor.setSelection,
127
- selectPath: draft.selectPath,
128
+ selectPath: draft.selectPath
129
+ // Its own handle root: the card that opened this editor is still
130
+ // mounted underneath, rendering the same value in its preview tree.
131
+ ,
132
+ testIDPrefix: "buoy-storage-editor-row",
128
133
  onRequestInlineEdit: sel => editor.openValueEditor(sel.path)
129
134
  })
130
135
  }), /*#__PURE__*/_jsx(Text, {
@@ -152,6 +157,8 @@ function StorageTreeEditor({
152
157
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
153
158
  onPress: draft.discard,
154
159
  disabled: draft.isSaving,
160
+ testID: "buoy-storage-editor-discard",
161
+ accessibilityRole: "button",
155
162
  accessibilityLabel: "Discard changes",
156
163
  style: styles.discardButton,
157
164
  children: /*#__PURE__*/_jsx(Text, {
@@ -161,6 +168,8 @@ function StorageTreeEditor({
161
168
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
162
169
  onPress: editor.saveAndClose,
163
170
  disabled: draft.isSaving,
171
+ testID: "buoy-storage-editor-save",
172
+ accessibilityRole: "button",
164
173
  accessibilityLabel: "Save changes",
165
174
  style: [styles.saveButton, draft.isSaving && styles.saveDisabled],
166
175
  children: /*#__PURE__*/_jsx(Text, {
@@ -196,6 +205,8 @@ function EditorHeader({
196
205
  style: styles.header,
197
206
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
198
207
  onPress: onClose,
208
+ testID: "buoy-storage-editor-close",
209
+ accessibilityRole: "button",
199
210
  accessibilityLabel: "Close editor",
200
211
  style: styles.headerButton,
201
212
  hitSlop: {
@@ -210,7 +221,13 @@ function EditorHeader({
210
221
  })
211
222
  }), /*#__PURE__*/_jsxs(View, {
212
223
  style: styles.headerTitle,
213
- children: [/*#__PURE__*/_jsx(Text, {
224
+ children: [/*#__PURE__*/_jsx(Text
225
+ // Which key this editor is pointed at. Worth a handle of its own:
226
+ // the editor is reached by expanding a card in a list of hundreds,
227
+ // and opening it on the wrong one looks exactly like opening it on
228
+ // the right one until a write lands somewhere unexpected.
229
+ , {
230
+ testID: "buoy-storage-editor-key",
214
231
  style: styles.headerKey,
215
232
  numberOfLines: 1,
216
233
  children: storageKey.key
@@ -220,6 +237,8 @@ function EditorHeader({
220
237
  })]
221
238
  }), /*#__PURE__*/_jsxs(TouchableOpacity, {
222
239
  onPress: onRaw,
240
+ testID: "buoy-storage-editor-raw",
241
+ accessibilityRole: "button",
223
242
  accessibilityLabel: "Edit raw JSON",
224
243
  style: styles.headerButton,
225
244
  hitSlop: {
@@ -265,8 +284,10 @@ function storageTypeColor(storageType) {
265
284
  const styles = StyleSheet.create({
266
285
  screen: {
267
286
  flex: 1,
268
- // Transparent so the night sheet's star field shows through.
269
- backgroundColor: "transparent"
287
+ // Opaque on purpose: transparency here showed the whole key list through
288
+ // the editor, not just the sheet's star field. The screen paints its own
289
+ // ToolBackground instead — see the mount site above.
290
+ backgroundColor: night.color.bg
270
291
  },
271
292
  header: {
272
293
  flexDirection: "row",