@buoy-gg/storage 7.0.27 → 7.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +51 -2
  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 +52 -3
  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
@@ -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
@@ -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