@buoy-gg/react-query 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 (73) hide show
  1. package/lib/commonjs/react-query/components/DataEditorMode.js +7 -0
  2. package/lib/commonjs/react-query/components/FilterStatusBadge.js +4 -0
  3. package/lib/commonjs/react-query/components/MutationEditorMode.js +4 -0
  4. package/lib/commonjs/react-query/components/WifiToggle.js +3 -1
  5. package/lib/commonjs/react-query/components/modals/DataEditorModal.js +24 -0
  6. package/lib/commonjs/react-query/components/modals/MutationEditorModal.js +32 -0
  7. package/lib/commonjs/react-query/components/modals/QueryBrowserModal.js +1 -0
  8. package/lib/commonjs/react-query/components/modals/ReactQueryModalHeader.js +19 -2
  9. package/lib/commonjs/react-query/components/query-browser/ActionButton.js +3 -1
  10. package/lib/commonjs/react-query/components/query-browser/Explorer.js +20 -1
  11. package/lib/commonjs/react-query/components/query-browser/MutationButton.js +12 -1
  12. package/lib/commonjs/react-query/components/query-browser/MutationDetails.js +3 -1
  13. package/lib/commonjs/react-query/components/query-browser/MutationDetailsChips.js +3 -1
  14. package/lib/commonjs/react-query/components/query-browser/MutationStatusCount.js +4 -0
  15. package/lib/commonjs/react-query/components/query-browser/MutationsList.js +7 -2
  16. package/lib/commonjs/react-query/components/query-browser/QueryBrowser.js +9 -2
  17. package/lib/commonjs/react-query/components/query-browser/QueryDetails.js +5 -1
  18. package/lib/commonjs/react-query/components/query-browser/QueryDetailsChip.js +3 -1
  19. package/lib/commonjs/react-query/components/query-browser/QueryRow.js +12 -1
  20. package/lib/commonjs/react-query/components/query-browser/QueryStatus.js +12 -1
  21. package/lib/commonjs/react-query/components/query-browser/QueryStatusCount.js +5 -0
  22. package/lib/commonjs/react-query/utils/testIDs.js +61 -0
  23. package/lib/module/react-query/components/DataEditorMode.js +7 -0
  24. package/lib/module/react-query/components/FilterStatusBadge.js +4 -0
  25. package/lib/module/react-query/components/MutationEditorMode.js +4 -0
  26. package/lib/module/react-query/components/WifiToggle.js +3 -1
  27. package/lib/module/react-query/components/modals/DataEditorModal.js +25 -1
  28. package/lib/module/react-query/components/modals/MutationEditorModal.js +33 -1
  29. package/lib/module/react-query/components/modals/QueryBrowserModal.js +1 -0
  30. package/lib/module/react-query/components/modals/ReactQueryModalHeader.js +19 -2
  31. package/lib/module/react-query/components/query-browser/ActionButton.js +3 -1
  32. package/lib/module/react-query/components/query-browser/Explorer.js +20 -1
  33. package/lib/module/react-query/components/query-browser/MutationButton.js +12 -1
  34. package/lib/module/react-query/components/query-browser/MutationDetails.js +3 -1
  35. package/lib/module/react-query/components/query-browser/MutationDetailsChips.js +3 -1
  36. package/lib/module/react-query/components/query-browser/MutationStatusCount.js +4 -0
  37. package/lib/module/react-query/components/query-browser/MutationsList.js +7 -2
  38. package/lib/module/react-query/components/query-browser/QueryBrowser.js +9 -2
  39. package/lib/module/react-query/components/query-browser/QueryDetails.js +5 -1
  40. package/lib/module/react-query/components/query-browser/QueryDetailsChip.js +3 -1
  41. package/lib/module/react-query/components/query-browser/QueryRow.js +12 -1
  42. package/lib/module/react-query/components/query-browser/QueryStatus.js +12 -1
  43. package/lib/module/react-query/components/query-browser/QueryStatusCount.js +5 -0
  44. package/lib/module/react-query/utils/testIDs.js +54 -0
  45. package/lib/typescript/react-query/components/DataEditorMode.d.ts.map +1 -1
  46. package/lib/typescript/react-query/components/FilterStatusBadge.d.ts.map +1 -1
  47. package/lib/typescript/react-query/components/MutationEditorMode.d.ts.map +1 -1
  48. package/lib/typescript/react-query/components/WifiToggle.d.ts +2 -1
  49. package/lib/typescript/react-query/components/WifiToggle.d.ts.map +1 -1
  50. package/lib/typescript/react-query/components/modals/DataEditorModal.d.ts.map +1 -1
  51. package/lib/typescript/react-query/components/modals/MutationEditorModal.d.ts.map +1 -1
  52. package/lib/typescript/react-query/components/modals/QueryBrowserModal.d.ts.map +1 -1
  53. package/lib/typescript/react-query/components/modals/ReactQueryModalHeader.d.ts.map +1 -1
  54. package/lib/typescript/react-query/components/query-browser/ActionButton.d.ts +8 -1
  55. package/lib/typescript/react-query/components/query-browser/ActionButton.d.ts.map +1 -1
  56. package/lib/typescript/react-query/components/query-browser/Explorer.d.ts.map +1 -1
  57. package/lib/typescript/react-query/components/query-browser/MutationButton.d.ts.map +1 -1
  58. package/lib/typescript/react-query/components/query-browser/MutationDetails.d.ts.map +1 -1
  59. package/lib/typescript/react-query/components/query-browser/MutationDetailsChips.d.ts +2 -1
  60. package/lib/typescript/react-query/components/query-browser/MutationDetailsChips.d.ts.map +1 -1
  61. package/lib/typescript/react-query/components/query-browser/MutationStatusCount.d.ts.map +1 -1
  62. package/lib/typescript/react-query/components/query-browser/MutationsList.d.ts.map +1 -1
  63. package/lib/typescript/react-query/components/query-browser/QueryBrowser.d.ts.map +1 -1
  64. package/lib/typescript/react-query/components/query-browser/QueryDetails.d.ts.map +1 -1
  65. package/lib/typescript/react-query/components/query-browser/QueryDetailsChip.d.ts +2 -1
  66. package/lib/typescript/react-query/components/query-browser/QueryDetailsChip.d.ts.map +1 -1
  67. package/lib/typescript/react-query/components/query-browser/QueryRow.d.ts.map +1 -1
  68. package/lib/typescript/react-query/components/query-browser/QueryStatus.d.ts +2 -0
  69. package/lib/typescript/react-query/components/query-browser/QueryStatus.d.ts.map +1 -1
  70. package/lib/typescript/react-query/components/query-browser/QueryStatusCount.d.ts.map +1 -1
  71. package/lib/typescript/react-query/utils/testIDs.d.ts +35 -0
  72. package/lib/typescript/react-query/utils/testIDs.d.ts.map +1 -0
  73. package/package.json +3 -3
@@ -69,6 +69,7 @@ const QueryStatusCount = ({
69
69
  isActive: activeFilter === "fresh",
70
70
  onPress: event => handleFilterClick("fresh", event),
71
71
  onTouchStart: handleTouchStart,
72
+ testID: "buoy-rq-status-fresh",
72
73
  showLabel: true // Always show labels now
73
74
  ,
74
75
  size: size
@@ -79,6 +80,7 @@ const QueryStatusCount = ({
79
80
  isActive: activeFilter === "fetching",
80
81
  onPress: event => handleFilterClick("fetching", event),
81
82
  onTouchStart: handleTouchStart,
83
+ testID: "buoy-rq-status-fetching",
82
84
  showLabel: true // Always show labels now
83
85
  ,
84
86
  size: size
@@ -89,6 +91,7 @@ const QueryStatusCount = ({
89
91
  isActive: activeFilter === "paused",
90
92
  onPress: event => handleFilterClick("paused", event),
91
93
  onTouchStart: handleTouchStart,
94
+ testID: "buoy-rq-status-paused",
92
95
  showLabel: true // Always show labels now
93
96
  ,
94
97
  size: size
@@ -99,6 +102,7 @@ const QueryStatusCount = ({
99
102
  isActive: activeFilter === "stale",
100
103
  onPress: event => handleFilterClick("stale", event),
101
104
  onTouchStart: handleTouchStart,
105
+ testID: "buoy-rq-status-stale",
102
106
  showLabel: true // Always show labels now
103
107
  ,
104
108
  size: size
@@ -109,6 +113,7 @@ const QueryStatusCount = ({
109
113
  isActive: activeFilter === "inactive",
110
114
  onPress: event => handleFilterClick("inactive", event),
111
115
  onTouchStart: handleTouchStart,
116
+ testID: "buoy-rq-status-inactive",
112
117
  showLabel: true // Always show labels now
113
118
  ,
114
119
  size: size
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.explorerTestID = explorerTestID;
7
+ exports.keySlug = keySlug;
8
+ exports.mutationRowTestID = mutationRowTestID;
9
+ exports.queryRowTestID = queryRowTestID;
10
+ /**
11
+ * Stable e2e handles for the React Query tool.
12
+ *
13
+ * Rows are addressed by their KEY, never by an index or a per-session id: a
14
+ * query's position moves every time the list re-sorts by `dataUpdatedAt`, and
15
+ * a mutation's `mutationId` is a counter that restarts with the process. The
16
+ * key is the one thing that means the same query on the next launch, which is
17
+ * also the only thing a test can name up front.
18
+ *
19
+ * `queryHash` would be stable too, but it is raw JSON (`["qbench","fresh"]`) —
20
+ * quotes and brackets that neither Detox's `by.id` nor a Maestro selector
21
+ * handles cleanly. So the parts are slugged and joined the way the rest of
22
+ * Buoy's handles read: `buoy-rq-query-row-qbench-fresh`.
23
+ */
24
+
25
+ /** One key part → a testID-safe slug. */
26
+ function slugPart(part) {
27
+ return String(part).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
28
+ }
29
+
30
+ /** A query/mutation key (array or scalar) → its slug, e.g. `qbench-fresh`. */
31
+ function keySlug(key) {
32
+ const parts = Array.isArray(key) ? key : [key];
33
+ return parts.filter(part => part != null).map(slugPart).filter(Boolean).join("-") || "unkeyed";
34
+ }
35
+
36
+ /** `buoy-rq-query-row-<slug>` for a query key. */
37
+ function queryRowTestID(queryKey) {
38
+ return `buoy-rq-query-row-${keySlug(queryKey)}`;
39
+ }
40
+
41
+ /**
42
+ * `buoy-rq-mutation-row-<slug>` for a mutation.
43
+ *
44
+ * Anonymous mutations (no `mutationKey`) fall back to the id — unstable across
45
+ * launches, but so is the mutation itself, and an unkeyed mutation has nothing
46
+ * else to be named by.
47
+ */
48
+ function mutationRowTestID(mutationKey, mutationId) {
49
+ return mutationKey == null ? `buoy-rq-mutation-row-id-${mutationId}` : `buoy-rq-mutation-row-${keySlug(mutationKey)}`;
50
+ }
51
+
52
+ /**
53
+ * `buoy-rq-explorer-<path>` for one node of the data explorer.
54
+ *
55
+ * The path is the same array the editor writes through
56
+ * (`updateNestedDataByPath`), so the handle and the write target can't drift:
57
+ * `buoy-rq-explorer-nested-count` edits `data.nested.count`.
58
+ */
59
+ function explorerTestID(dataPath) {
60
+ return dataPath.length === 0 ? "buoy-rq-explorer-root" : `buoy-rq-explorer-${dataPath.map(slugPart).filter(Boolean).join("-")}`;
61
+ }
@@ -27,6 +27,7 @@ export function DataEditorMode({
27
27
  const actionButtons = useActionButtons(selectedQuery, queryClient, queryVersion);
28
28
  return /*#__PURE__*/_jsxs(_Fragment, {
29
29
  children: [/*#__PURE__*/_jsxs(ScrollView, {
30
+ testID: "buoy-rq-detail-scroll",
30
31
  accessibilityLabel: "Data editor mode",
31
32
  accessibilityHint: "View data editor mode",
32
33
  "sentry-label": "ignore data editor mode",
@@ -54,6 +55,7 @@ export function DataEditorMode({
54
55
  children: /*#__PURE__*/_jsxs(View, {
55
56
  style: styles.queryExplorerContainer,
56
57
  children: [/*#__PURE__*/_jsx(Text, {
58
+ testID: "buoy-rq-query-explorer-header",
57
59
  style: styles.queryExplorerHeader,
58
60
  children: "Query Explorer"
59
61
  }), /*#__PURE__*/_jsx(View, {
@@ -76,6 +78,7 @@ export function DataEditorMode({
76
78
  children: /*#__PURE__*/_jsx(View, {
77
79
  style: styles.actionsRow,
78
80
  children: actionButtons.map((action, index) => /*#__PURE__*/_jsx(ActionButton, {
81
+ testID: `buoy-rq-action-${action.variant}`,
79
82
  onClick: action.onPress,
80
83
  text: action.label,
81
84
  variant: action.variant,
@@ -108,6 +111,7 @@ export function DataEditorActionsFooter({
108
111
  children: /*#__PURE__*/_jsx(View, {
109
112
  style: styles.actionsRow,
110
113
  children: actionButtons.map((action, index) => /*#__PURE__*/_jsx(ActionButton, {
114
+ testID: `buoy-rq-action-${action.variant}`,
111
115
  onClick: action.onPress,
112
116
  text: action.label,
113
117
  variant: action.variant,
@@ -125,6 +129,7 @@ function DataExplorer({
125
129
  return /*#__PURE__*/_jsxs(View, {
126
130
  style: styles.dataContainer,
127
131
  children: [/*#__PURE__*/_jsx(Text, {
132
+ testID: "buoy-rq-data-header",
128
133
  style: styles.dataHeader,
129
134
  children: "Data Editor"
130
135
  }), /*#__PURE__*/_jsx(View, {
@@ -177,9 +182,11 @@ function DataEmptyState({
177
182
  return /*#__PURE__*/_jsxs(View, {
178
183
  style: styles.emptyState,
179
184
  children: [/*#__PURE__*/_jsx(Text, {
185
+ testID: "buoy-rq-detail-empty-title",
180
186
  style: styles.emptyTitle,
181
187
  children: title
182
188
  }), /*#__PURE__*/_jsx(Text, {
189
+ testID: "buoy-rq-detail-empty-description",
183
190
  style: styles.emptyDescription,
184
191
  children: description
185
192
  })]
@@ -19,12 +19,16 @@ export function FilterStatusBadge({
19
19
  const content = /*#__PURE__*/_jsx(View, {
20
20
  style: styles.container,
21
21
  children: /*#__PURE__*/_jsxs(Text, {
22
+ testID: "buoy-rq-filter-badge",
22
23
  style: styles.text,
23
24
  children: ["Showing ", filteredCount, " of ", totalCount]
24
25
  })
25
26
  });
26
27
  if (onPress) {
27
28
  return /*#__PURE__*/_jsx(TouchableOpacity, {
29
+ testID: "buoy-rq-filter-badge-button",
30
+ accessibilityRole: "button",
31
+ accessibilityLabel: `Showing ${filteredCount} of ${totalCount}, open filters`,
28
32
  onPress: onPress,
29
33
  activeOpacity: 0.7,
30
34
  children: content
@@ -19,6 +19,7 @@ export function MutationEditorMode({
19
19
  }) {
20
20
  return /*#__PURE__*/_jsxs(_Fragment, {
21
21
  children: [/*#__PURE__*/_jsxs(ScrollView, {
22
+ testID: "buoy-rq-mutation-detail-scroll",
22
23
  accessibilityLabel: "Mutation editor mode",
23
24
  accessibilityHint: "View mutation editor mode",
24
25
  "sentry-label": "ignore mutation editor mode",
@@ -84,6 +85,7 @@ export function MutationEditorActionsFooter({
84
85
  children: /*#__PURE__*/_jsx(View, {
85
86
  style: styles.actionsGrid,
86
87
  children: actionButtons.map((action, index) => /*#__PURE__*/_jsx(ActionButton, {
88
+ testID: `buoy-rq-action-${action.variant}`,
87
89
  onClick: action.onPress,
88
90
  text: action.label,
89
91
  variant: action.variant,
@@ -143,9 +145,11 @@ function DataEmptyState({
143
145
  return /*#__PURE__*/_jsxs(View, {
144
146
  style: styles.emptyState,
145
147
  children: [/*#__PURE__*/_jsx(Text, {
148
+ testID: "buoy-rq-mutation-detail-empty-title",
146
149
  style: styles.emptyTitle,
147
150
  children: title
148
151
  }), /*#__PURE__*/_jsx(Text, {
152
+ testID: "buoy-rq-mutation-detail-empty-description",
149
153
  style: styles.emptyDescription,
150
154
  children: description
151
155
  })]
@@ -10,7 +10,8 @@ import { useWifiState } from "../hooks/useWifiState";
10
10
  */
11
11
  import { jsx as _jsx } from "react/jsx-runtime";
12
12
  export function WifiToggle({
13
- size = 16
13
+ size = 16,
14
+ testID
14
15
  }) {
15
16
  const {
16
17
  isOnline,
@@ -18,6 +19,7 @@ export function WifiToggle({
18
19
  } = useWifiState();
19
20
  return /*#__PURE__*/_jsx(TouchableOpacity, {
20
21
  "sentry-label": `ignore toggle WiFi ${isOnline ? "On" : "Off"}`,
22
+ testID: testID,
21
23
  accessibilityRole: "button",
22
24
  accessibilityLabel: `WiFi ${isOnline ? "On" : "Off"}`,
23
25
  accessibilityHint: `Tap to turn WiFi ${isOnline ? "off" : "on"} for React Query`,
@@ -4,7 +4,7 @@ import { JsModal, devToolsStorageKeys, useSafeAreaInsets } from "@buoy-gg/shared
4
4
  import { useGetQueryByQueryKeyWithVersion } from "../../hooks/useSelectedQuery";
5
5
  import { ReactQueryModalHeader } from "./ReactQueryModalHeader";
6
6
  import { DataEditorMode, DataEditorActionsFooter } from "../DataEditorMode";
7
- import { useState, useCallback } from "react";
7
+ import { useState, useCallback, useEffect, useRef } from "react";
8
8
  import { jsx as _jsx } from "react/jsx-runtime";
9
9
  /**
10
10
  * Specialized modal for data editing following "Decompose by Responsibility"
@@ -24,6 +24,30 @@ export function DataEditorModal({
24
24
  version: queryVersion
25
25
  } = useGetQueryByQueryKeyWithVersion(selectedQueryKey);
26
26
  const [modalMode, setModalMode] = useState("bottomSheet");
27
+
28
+ /**
29
+ * Fall back to the list when the selected query leaves the cache — a
30
+ * removal, a cache clear, or garbage collection. Same failure as the
31
+ * mutation editor: the key that selected it is still set, so this modal
32
+ * bails out and the browser stays hidden behind it, leaving an "open" tool
33
+ * with nothing on screen. The ref covers the tick before the subscription
34
+ * has resolved the query, when undefined does not yet mean gone.
35
+ */
36
+ const currentQueryKey = selectedQueryKey ? JSON.stringify(selectedQueryKey) : undefined;
37
+ const seenQueryKey = useRef(undefined);
38
+ useEffect(() => {
39
+ if (selectedQuery) {
40
+ seenQueryKey.current = currentQueryKey;
41
+ return;
42
+ }
43
+ // Keyed to THIS query for the same reason as the mutation editor: the
44
+ // last-seen key outlives the detail view, so an un-keyed check would fire
45
+ // on the next selection before its lookup has resolved.
46
+ if (visible && seenQueryKey.current !== undefined && seenQueryKey.current === currentQueryKey) {
47
+ seenQueryKey.current = undefined;
48
+ onQuerySelect(undefined);
49
+ }
50
+ }, [visible, selectedQuery, currentQueryKey, onQuerySelect]);
27
51
  const handleModeChange = useCallback(mode => {
28
52
  setModalMode(mode);
29
53
  }, []);
@@ -4,7 +4,7 @@ import { JsModal, useSafeAreaInsets, devToolsStorageKeys } from "@buoy-gg/shared
4
4
  import { useGetMutationById } from "../../hooks/useSelectedMutation";
5
5
  import { ReactQueryModalHeader } from "./ReactQueryModalHeader";
6
6
  import { MutationEditorMode, MutationEditorActionsFooter } from "../MutationEditorMode";
7
- import { useState, useCallback } from "react";
7
+ import { useState, useCallback, useEffect, useRef } from "react";
8
8
  import { jsx as _jsx } from "react/jsx-runtime";
9
9
  /**
10
10
  * Modal variant dedicated to editing a single mutation entry. Restores persisted positioning and
@@ -21,6 +21,38 @@ export function MutationEditorModal({
21
21
  }) {
22
22
  const selectedMutation = useGetMutationById(selectedMutationId);
23
23
  const [modalMode, setModalMode] = useState("bottomSheet");
24
+
25
+ /**
26
+ * Fall back to the list when the selected mutation leaves the cache.
27
+ *
28
+ * Without this the whole tool disappears. `Remove` (and a mutation-cache
29
+ * clear, and ordinary garbage collection) drops the entry while the id that
30
+ * selected it is still set, so this modal bails out on the missing mutation
31
+ * and the browser stays hidden behind an `inDetail` that is still true —
32
+ * leaving an "open" tool with nothing on screen and no way back except
33
+ * closing and reopening it from the dial.
34
+ *
35
+ * The ref is what makes it safe: `useGetMutationById` resolves on a
36
+ * `setTimeout(0)`, so the mutation is legitimately undefined for the first
37
+ * tick after a selection. Only a mutation that was HERE and then went away
38
+ * counts as gone.
39
+ */
40
+ const seenMutationId = useRef(undefined);
41
+ useEffect(() => {
42
+ if (selectedMutation) {
43
+ seenMutationId.current = selectedMutation.mutationId;
44
+ return;
45
+ }
46
+ // Only THIS mutation going missing counts. Keying the check to the id
47
+ // matters: leaving a detail view leaves the last-seen id behind, and a
48
+ // plain "we had one and now we don't" flag would fire on the very next
49
+ // selection — during the tick before its own lookup resolves — and bounce
50
+ // the user straight back out of the mutation they just opened.
51
+ if (visible && seenMutationId.current !== undefined && seenMutationId.current === selectedMutationId) {
52
+ seenMutationId.current = undefined;
53
+ onMutationSelect(undefined);
54
+ }
55
+ }, [visible, selectedMutation, selectedMutationId, onMutationSelect]);
24
56
  const handleModeChange = useCallback(mode => {
25
57
  setModalMode(mode);
26
58
  }, []);
@@ -168,6 +168,7 @@ export function QueryBrowserModal({
168
168
  children: /*#__PURE__*/_jsx(TabSelector, {
169
169
  tabs: tabs,
170
170
  activeTab: "filters",
171
+ testIDPrefix: "buoy-rq-filterview-tab",
171
172
  onTabChange: () => {}
172
173
  })
173
174
  })]
@@ -69,7 +69,8 @@ export function ReactQueryModalHeader({
69
69
  children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
70
70
  onBack: onBack
71
71
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
72
- title: getItemText(selectedQuery ?? selectedMutation)
72
+ title: getItemText(selectedQuery ?? selectedMutation),
73
+ testID: "buoy-rq-detail-title"
73
74
  })]
74
75
  });
75
76
  }
@@ -81,11 +82,13 @@ export function ReactQueryModalHeader({
81
82
  noMargin: true,
82
83
  children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
83
84
  style: styles.headerSearchContainer,
85
+ testID: "buoy-rq-search-field",
84
86
  children: [/*#__PURE__*/_jsx(Search, {
85
87
  size: 14,
86
88
  color: night.color.textSecondary
87
89
  }), /*#__PURE__*/_jsx(TextInput, {
88
90
  ref: searchInputRef,
91
+ testID: "buoy-rq-search",
89
92
  style: styles.headerSearchInput,
90
93
  placeholder: "Search query keys...",
91
94
  placeholderTextColor: night.color.textTertiary,
@@ -104,6 +107,9 @@ export function ReactQueryModalHeader({
104
107
  setIsSearchActive(false);
105
108
  },
106
109
  "sentry-label": "ignore clear search header",
110
+ testID: "buoy-rq-search-clear",
111
+ accessibilityRole: "button",
112
+ accessibilityLabel: "Clear search",
107
113
  style: styles.headerSearchClear,
108
114
  children: /*#__PURE__*/_jsx(X, {
109
115
  size: 14,
@@ -117,19 +123,25 @@ export function ReactQueryModalHeader({
117
123
  children: /*#__PURE__*/_jsx(TabSelector, {
118
124
  tabs: tabs,
119
125
  activeTab: activeTab,
126
+ testIDPrefix: "buoy-rq-tab",
120
127
  onTabChange: tab => onTabChange(tab)
121
128
  })
122
129
  }), /*#__PURE__*/_jsx(WifiToggle, {
123
- size: 16
130
+ size: 16,
131
+ testID: "buoy-rq-wifi"
124
132
  })]
125
133
  }) : /*#__PURE__*/_jsx(TabSelector, {
126
134
  tabs: tabs,
127
135
  activeTab: activeTab,
136
+ testIDPrefix: "buoy-rq-tab",
128
137
  onTabChange: tab => onTabChange(tab)
129
138
  })
130
139
  }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
131
140
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
132
141
  "sentry-label": "ignore open search",
142
+ testID: "buoy-rq-search-open",
143
+ accessibilityRole: "button",
144
+ accessibilityLabel: "Search queries and mutations",
133
145
  onPress: handleSearchToggle,
134
146
  style: styles.headerActionButton,
135
147
  children: /*#__PURE__*/_jsx(Search, {
@@ -138,6 +150,9 @@ export function ReactQueryModalHeader({
138
150
  })
139
151
  }), onFilterPress && /*#__PURE__*/_jsx(TouchableOpacity, {
140
152
  "sentry-label": "ignore open filter",
153
+ testID: "buoy-rq-filters",
154
+ accessibilityRole: "button",
155
+ accessibilityLabel: hasActiveFilters ? "Filters, active" : "Filters",
141
156
  onPress: onFilterPress,
142
157
  style: [styles.headerActionButton, hasActiveFilters && styles.activeFilterButton],
143
158
  children: /*#__PURE__*/_jsx(Filter, {
@@ -146,6 +161,8 @@ export function ReactQueryModalHeader({
146
161
  })
147
162
  }), onClearCache && /*#__PURE__*/_jsx(TouchableOpacity, {
148
163
  "sentry-label": "ignore clear cache",
164
+ testID: "buoy-rq-clear-cache",
165
+ accessibilityRole: "button",
149
166
  onPress: onClearCache,
150
167
  style: [styles.headerActionButton, styles.clearCacheButton],
151
168
  accessibilityLabel: `Clear ${activeTab} cache`,
@@ -41,11 +41,13 @@ export default function ActionButton({
41
41
  onClick,
42
42
  text,
43
43
  variant,
44
- disabled
44
+ disabled,
45
+ testID
45
46
  }) {
46
47
  const config = buttonConfigs[variant];
47
48
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
48
49
  "sentry-label": "ignore devtools action button",
50
+ testID: testID,
49
51
  disabled: disabled,
50
52
  onPress: onClick,
51
53
  style: [styles.button, {
@@ -5,6 +5,7 @@ import { useQueryClient } from "@tanstack/react-query";
5
5
  import { updateNestedDataByPath } from "../../utils/updateNestedDataByPath";
6
6
  import { displayValue, night } from "@buoy-gg/shared-ui";
7
7
  import deleteItem from "../../utils/actions/deleteItem";
8
+ import { explorerTestID } from "../../utils/testIDs";
8
9
  import { Text, TouchableOpacity, View, StyleSheet } from "react-native";
9
10
  import { CopyButton as SharedCopyButton } from "@buoy-gg/shared-ui";
10
11
  import { ChevronRight, ChevronDown, Trash } from "@buoy-gg/shared-ui";
@@ -84,6 +85,8 @@ const DeleteItemButton = /*#__PURE__*/memo(({
84
85
  if (!activeQuery) return null;
85
86
  return /*#__PURE__*/_jsx(TouchableOpacity, {
86
87
  "sentry-label": "ignore devtools explorer delete button",
88
+ testID: `${explorerTestID(dataPath)}-delete`,
89
+ accessibilityRole: "button",
87
90
  onPress: handleDelete,
88
91
  style: [styles.deleteButton, isFocused && styles.deleteButtonFocused],
89
92
  accessibilityLabel: "Delete item",
@@ -113,7 +116,10 @@ const ClearArrayButton = /*#__PURE__*/memo(({
113
116
  if (!activeQuery) return null;
114
117
  return /*#__PURE__*/_jsx(TouchableOpacity, {
115
118
  "sentry-label": "ignore devtools explorer clear button",
119
+ testID: `${explorerTestID(dataPath)}-clear`,
116
120
  style: [styles.clearButton, isFocused && styles.clearButtonFocused],
121
+ accessibilityRole: "button",
122
+ accessibilityLabel: "Remove all items",
117
123
  "aria-label": "Remove all items",
118
124
  onPress: handleClear,
119
125
  hitSlop: HIT_SLOP_OPTIMIZED,
@@ -158,6 +164,9 @@ function ToggleValueButton({
158
164
  const textStyle = value ? styles.toggleTextTrue : styles.toggleTextFalse;
159
165
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
160
166
  "sentry-label": "ignore devtools explorer toggle button",
167
+ testID: `${explorerTestID(dataPath)}-toggle`,
168
+ accessibilityRole: "button",
169
+ accessibilityLabel: `${dataPath[dataPath.length - 1] ?? "value"}, ${value ? "true" : "false"}`,
161
170
  style: styles.modernToggleButton,
162
171
  onPress: handleClick,
163
172
  hitSlop: HIT_SLOP_OPTIMIZED,
@@ -170,6 +179,8 @@ function ToggleValueButton({
170
179
  })
171
180
  }), itemsDeletable && /*#__PURE__*/_jsx(TouchableOpacity, {
172
181
  "sentry-label": "ignore devtools explorer delete button in toggle",
182
+ testID: `${explorerTestID(dataPath)}-delete`,
183
+ accessibilityRole: "button",
173
184
  onPress: handleDelete,
174
185
  style: styles.deleteButtonInToggle,
175
186
  accessibilityLabel: "Delete item",
@@ -312,6 +323,12 @@ export default function Explorer({
312
323
  children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
313
324
  "sentry-label": "ignore devtools explorer expander button",
314
325
  style: styles.expanderButton,
326
+ testID: explorerTestID(currentDataPath),
327
+ accessibilityRole: "button",
328
+ accessibilityLabel: `${label}, ${subEntries.length} ${subEntries.length === 1 ? "item" : "items"}`,
329
+ accessibilityState: {
330
+ expanded: isExpanded
331
+ },
315
332
  onPress: toggleExpanded,
316
333
  hitSlop: HIT_SLOP_OPTIMIZED,
317
334
  activeOpacity: 0.6,
@@ -393,7 +410,8 @@ export default function Explorer({
393
410
  style: styles.nebulaInputWrapper,
394
411
  children: /*#__PURE__*/_jsx(CyberpunkInput, {
395
412
  label: label,
396
- accessibilityLabel: "Data input field for editing values",
413
+ testID: `${explorerTestID(currentDataPath)}-input`,
414
+ accessibilityLabel: `${label}, editable value`,
397
415
  style: [valueType === "number" ? styles.textNumber : styles.textString],
398
416
  keyboardType: valueType === "number" ? "numeric" : "default",
399
417
  value: localInputValue,
@@ -438,6 +456,7 @@ export default function Explorer({
438
456
  style: styles.text344054,
439
457
  children: label
440
458
  }), /*#__PURE__*/_jsx(Text, {
459
+ testID: `${explorerTestID(currentDataPath)}-value`,
441
460
  style: styles.displayValueText,
442
461
  children: displayValue(value)
443
462
  })]
@@ -3,6 +3,7 @@
3
3
  import { TouchableOpacity, Text, View, StyleSheet } from "react-native";
4
4
  import { CheckCircle, LoadingCircle, PauseCircle, XCircle } from "./svgs";
5
5
  import { buoyColors, night, useRelativeTime } from "@buoy-gg/shared-ui";
6
+ import { mutationRowTestID } from "../../utils/testIDs";
6
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
8
  const getMutationText = mutation => {
8
9
  if (!mutation.options.mutationKey) return "Anonymous Mutation";
@@ -69,8 +70,16 @@ export default function MutationButton({
69
70
  }
70
71
  };
71
72
  const statusInfo = getStatusInfo();
73
+ const mutationText = getMutationText(mutation);
74
+ const testID = mutationRowTestID(mutation.options.mutationKey, mutation.mutationId);
72
75
  return /*#__PURE__*/_jsxs(TouchableOpacity, {
73
76
  "sentry-label": "ignore devtools mutation button",
77
+ testID: testID,
78
+ accessibilityRole: "button"
79
+ // Same Fabric merge as the query rows: without a spelled-out label the
80
+ // row announces as an unnamed button.
81
+ ,
82
+ accessibilityLabel: `${mutationText}, ${statusInfo.status}`,
74
83
  onPress: () => setSelectedMutation(mutation === selected ? undefined : mutation),
75
84
  style: [styles.button, selected?.mutationId === mutation.mutationId && styles.selected],
76
85
  children: [/*#__PURE__*/_jsxs(View, {
@@ -84,6 +93,7 @@ export default function MutationButton({
84
93
  }), /*#__PURE__*/_jsxs(View, {
85
94
  style: styles.statusInfo,
86
95
  children: [/*#__PURE__*/_jsx(Text, {
96
+ testID: `${testID}-status`,
87
97
  style: [styles.statusLabel, {
88
98
  color: statusInfo.color
89
99
  }],
@@ -96,8 +106,9 @@ export default function MutationButton({
96
106
  }), /*#__PURE__*/_jsx(View, {
97
107
  style: styles.mutationSection,
98
108
  children: /*#__PURE__*/_jsx(Text, {
109
+ testID: `${testID}-key`,
99
110
  style: styles.mutationKey,
100
- children: getMutationText(mutation)
111
+ children: mutationText
101
112
  })
102
113
  })]
103
114
  }), /*#__PURE__*/_jsx(View, {
@@ -26,11 +26,13 @@ export default function MutationDetails({
26
26
  horizontal: true,
27
27
  style: styles.flex1,
28
28
  children: /*#__PURE__*/_jsx(Text, {
29
+ testID: "buoy-rq-mutation-detail-key",
29
30
  style: styles.flexWrap,
30
31
  children: `${selectedMutation.options.mutationKey ? displayValue(selectedMutation.options.mutationKey, true) : "No mutationKey found"}`
31
32
  })
32
33
  }), /*#__PURE__*/_jsx(MutationDetailsChips, {
33
- status: selectedMutation.state.status
34
+ status: selectedMutation.state.status,
35
+ testID: "buoy-rq-mutation-detail-status"
34
36
  })]
35
37
  }), /*#__PURE__*/_jsxs(View, {
36
38
  style: styles.row,
@@ -25,7 +25,8 @@ const textColors = {
25
25
  * Small status chip used in mutation detail views that adapts styling to the mutation status.
26
26
  */
27
27
  export default function QueryDetailsChip({
28
- status
28
+ status,
29
+ testID
29
30
  }) {
30
31
  const statusToColor = status;
31
32
  const backgroundColor = backgroundColors[statusToColor];
@@ -37,6 +38,7 @@ export default function QueryDetailsChip({
37
38
  borderColor
38
39
  }],
39
40
  children: /*#__PURE__*/_jsx(Text, {
41
+ testID: testID,
40
42
  style: [styles.text, {
41
43
  color: textColor
42
44
  }],
@@ -58,6 +58,7 @@ const MutationStatusCount = ({
58
58
  isActive: activeFilter === "pending",
59
59
  onPress: event => handleFilterClick("pending", event),
60
60
  onTouchStart: handleTouchStart,
61
+ testID: "buoy-rq-mstatus-pending",
61
62
  showLabel: true,
62
63
  size: size
63
64
  }), /*#__PURE__*/_jsx(QueryStatus, {
@@ -67,6 +68,7 @@ const MutationStatusCount = ({
67
68
  isActive: activeFilter === "success",
68
69
  onPress: event => handleFilterClick("success", event),
69
70
  onTouchStart: handleTouchStart,
71
+ testID: "buoy-rq-mstatus-success",
70
72
  showLabel: true,
71
73
  size: size
72
74
  }), /*#__PURE__*/_jsx(QueryStatus, {
@@ -76,6 +78,7 @@ const MutationStatusCount = ({
76
78
  isActive: activeFilter === "error",
77
79
  onPress: event => handleFilterClick("error", event),
78
80
  onTouchStart: handleTouchStart,
81
+ testID: "buoy-rq-mstatus-error",
79
82
  showLabel: true,
80
83
  size: size
81
84
  }), /*#__PURE__*/_jsx(QueryStatus, {
@@ -85,6 +88,7 @@ const MutationStatusCount = ({
85
88
  isActive: activeFilter === "paused",
86
89
  onPress: event => handleFilterClick("paused", event),
87
90
  onTouchStart: handleTouchStart,
91
+ testID: "buoy-rq-mstatus-paused",
88
92
  showLabel: true,
89
93
  size: size
90
94
  })]
@@ -114,12 +114,16 @@ export default function MutationsList({
114
114
  style: styles.listWrapper,
115
115
  children: /*#__PURE__*/_jsx(FlatList, {
116
116
  "sentry-label": "ignore devtools mutations list",
117
+ testID: "buoy-rq-mutation-list",
117
118
  data: filteredMutations,
118
119
  renderItem: renderMutation,
119
120
  keyExtractor: (item, index) => `${item.mutationId}-${item.state.submittedAt}-${item.state.status}-${index}`,
120
121
  inverted: true,
121
- showsVerticalScrollIndicator: true,
122
- removeClippedSubviews: true,
122
+ showsVerticalScrollIndicator: true
123
+ // See QueryBrowser: e2e bundles keep every cell mounted so Detox's
124
+ // hittability check doesn't reject an on-screen row as clipped.
125
+ ,
126
+ removeClippedSubviews: !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E),
123
127
  contentContainerStyle: contentContainerStyle || styles.listContent,
124
128
  initialNumToRender: 10,
125
129
  maxToRenderPerBatch: 10,
@@ -130,6 +134,7 @@ export default function MutationsList({
130
134
  style: styles.emptyContainer,
131
135
  children: /*#__PURE__*/_jsx(Text, {
132
136
  style: styles.emptyText,
137
+ testID: "buoy-rq-mutations-empty",
133
138
  children: activeFilter ? `No ${activeFilter} mutations found` : "No mutations found"
134
139
  })
135
140
  }), selectedMutation && !hideInfoPanel && /*#__PURE__*/_jsxs(Animated.View, {
@@ -97,6 +97,7 @@ export default function QueryBrowser({
97
97
  style: styles.emptyContainer,
98
98
  children: /*#__PURE__*/_jsx(Text, {
99
99
  style: styles.emptyText,
100
+ testID: "buoy-rq-empty",
100
101
  children: emptyStateMessage || (activeFilter ? `No ${activeFilter} queries found` : "No queries found")
101
102
  })
102
103
  });
@@ -111,6 +112,7 @@ export default function QueryBrowser({
111
112
  // mutable with stable identity, so `stateSig` carries the changing
112
113
  // fields for the row's memo comparison.
113
114
  _jsx(FlatList, {
115
+ testID: "buoy-rq-query-list",
114
116
  style: styles.listWrapper,
115
117
  contentContainerStyle: contentContainerStyle || styles.listContent,
116
118
  data: filteredQueries,
@@ -123,8 +125,13 @@ export default function QueryBrowser({
123
125
  isSelected: selectedQuery?.queryHash === query.queryHash,
124
126
  onSelect: handleQuerySelect
125
127
  }),
126
- showsVerticalScrollIndicator: true,
127
- removeClippedSubviews: true,
128
+ showsVerticalScrollIndicator: true
129
+ // Clipped cells fail Detox's hittability check even when they are on
130
+ // screen, so e2e bundles (Metro with EXPO_PUBLIC_E2E=1) turn clipping
131
+ // off. Inlined at bundle time; a non-inlining bundler keeps prod
132
+ // behavior.
133
+ ,
134
+ removeClippedSubviews: !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E),
128
135
  initialNumToRender: 12,
129
136
  maxToRenderPerBatch: 8,
130
137
  windowSize: 5,