@buoy-gg/route-events 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.
- package/lib/commonjs/components/NavigationStack.js +38 -3
- package/lib/commonjs/components/RouteEventDetail.js +4 -0
- package/lib/commonjs/components/RouteEventExpandedContent.js +15 -0
- package/lib/commonjs/components/RouteEventItemCompact.js +16 -2
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +55 -18
- package/lib/commonjs/components/RouteEventsTimeline.js +13 -4
- package/lib/commonjs/components/RoutesSitemap.js +77 -11
- package/lib/commonjs/utils/testIDs.js +63 -0
- package/lib/module/components/NavigationStack.js +38 -3
- package/lib/module/components/RouteEventDetail.js +4 -0
- package/lib/module/components/RouteEventExpandedContent.js +15 -0
- package/lib/module/components/RouteEventItemCompact.js +16 -2
- package/lib/module/components/RouteEventsModalWithTabs.js +55 -18
- package/lib/module/components/RouteEventsTimeline.js +13 -4
- package/lib/module/components/RoutesSitemap.js +77 -11
- package/lib/module/utils/testIDs.js +55 -0
- package/lib/typescript/components/NavigationStack.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventDetail.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventsTimeline.d.ts +3 -1
- package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -1
- package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -1
- package/lib/typescript/utils/testIDs.d.ts +39 -0
- package/lib/typescript/utils/testIDs.d.ts.map +1 -0
- package/package.json +5 -5
|
@@ -10,6 +10,7 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
10
10
|
var _license = require("@buoy-gg/license");
|
|
11
11
|
var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
|
|
12
12
|
var _useNavigationStack = require("../useNavigationStack");
|
|
13
|
+
var _testIDs = require("../utils/testIDs");
|
|
13
14
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
14
15
|
/**
|
|
15
16
|
* NavigationStack - Visual representation of current navigation stack
|
|
@@ -52,6 +53,9 @@ function NavigationStack({
|
|
|
52
53
|
const navigateToIndex = index => onAction ? onAction("navigateToIndex", {
|
|
53
54
|
index
|
|
54
55
|
}) : hookResult.navigateToIndex(index);
|
|
56
|
+
const popToIndex = index => onAction ? onAction("popToIndex", {
|
|
57
|
+
index
|
|
58
|
+
}) : hookResult.popToIndex(index);
|
|
55
59
|
const goBack = () => onAction ? onAction("goBack") : hookResult.goBack();
|
|
56
60
|
const popToTop = () => onAction ? onAction("popToTop") : hookResult.popToTop();
|
|
57
61
|
const [expandedIndex, setExpandedIndex] = (0, _react.useState)(null);
|
|
@@ -106,6 +110,7 @@ function NavigationStack({
|
|
|
106
110
|
style: styles.loadingContainer,
|
|
107
111
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
108
112
|
style: styles.loadingText,
|
|
113
|
+
testID: "buoy-routes-stack-loading",
|
|
109
114
|
children: "Loading navigation stack..."
|
|
110
115
|
})
|
|
111
116
|
})
|
|
@@ -118,11 +123,13 @@ function NavigationStack({
|
|
|
118
123
|
style: [styles.container, style],
|
|
119
124
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
120
125
|
style: styles.errorContainer,
|
|
126
|
+
testID: "buoy-routes-stack-error",
|
|
121
127
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
122
128
|
style: styles.errorText,
|
|
123
129
|
children: "Error loading stack"
|
|
124
130
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
125
131
|
style: styles.errorDetail,
|
|
132
|
+
testID: "buoy-routes-stack-error-detail",
|
|
126
133
|
children: error.message
|
|
127
134
|
})]
|
|
128
135
|
})
|
|
@@ -135,8 +142,10 @@ function NavigationStack({
|
|
|
135
142
|
style: [styles.container, style],
|
|
136
143
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
137
144
|
style: styles.emptyContainer,
|
|
145
|
+
testID: "buoy-routes-stack-empty",
|
|
138
146
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
139
147
|
style: styles.emptyText,
|
|
148
|
+
testID: "buoy-routes-stack-empty-text",
|
|
140
149
|
children: "No navigation stack"
|
|
141
150
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
142
151
|
style: styles.emptySubtext,
|
|
@@ -233,12 +242,19 @@ function NavigationStack({
|
|
|
233
242
|
const screensToRemove = stackDepth - 1 - selectedRoute.index;
|
|
234
243
|
const targetIndex = selectedRoute.index;
|
|
235
244
|
confirmDestructive("Pop to Route", `Remove ${screensToRemove} screen${screensToRemove !== 1 ? "s" : ""} above ${selectedRoute.pathname}?`, "Pop",
|
|
236
|
-
//
|
|
237
|
-
|
|
245
|
+
// POP, not navigate. `navigate` only returns to a mounted screen when
|
|
246
|
+
// the name AND the params match, so on any dynamic route — the common
|
|
247
|
+
// case for a stack you would want to unwind — it pushed a fresh copy
|
|
248
|
+
// instead: the dialog promised to remove two screens and the stack came
|
|
249
|
+
// back one deeper, with the route in it twice. `popToIndex` is the
|
|
250
|
+
// action the hook has always exported for this, and the same one the
|
|
251
|
+
// desktop host already dispatches over the socket.
|
|
252
|
+
() => popToIndex(targetIndex));
|
|
238
253
|
};
|
|
239
254
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
240
255
|
style: [styles.container, style],
|
|
241
256
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
257
|
+
testID: "buoy-routes-stack-scroll",
|
|
242
258
|
style: styles.stackScroll,
|
|
243
259
|
contentContainerStyle: [styles.stackContent, {
|
|
244
260
|
paddingBottom: styles.stackContent.padding + insets.bottom
|
|
@@ -247,14 +263,19 @@ function NavigationStack({
|
|
|
247
263
|
const actualIndex = stack.length - 1 - reverseIndex;
|
|
248
264
|
const isExpanded = expandedIndex === actualIndex;
|
|
249
265
|
const hasParams = Object.keys(item.params).length > 0;
|
|
266
|
+
const itemTestID = (0, _testIDs.stackItemTestID)(actualIndex);
|
|
250
267
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
251
268
|
style: styles.stackItemWrapper,
|
|
269
|
+
testID: itemTestID,
|
|
252
270
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
253
271
|
style: [styles.stackItem, isExpanded && styles.stackItemSelected],
|
|
254
272
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
|
|
255
273
|
style: styles.stackItemHeader,
|
|
256
274
|
onPress: () => toggleExpand(actualIndex),
|
|
257
275
|
activeOpacity: 0.7,
|
|
276
|
+
testID: `${itemTestID}-toggle`,
|
|
277
|
+
accessibilityRole: "button",
|
|
278
|
+
accessibilityLabel: `${item.pathname}, ${item.isFocused ? "focused" : "mounted"}, position ${actualIndex} of ${stackDepth - 1}`,
|
|
258
279
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
259
280
|
style: styles.expandIndicator,
|
|
260
281
|
children: isExpanded ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChevronDown, {
|
|
@@ -267,10 +288,12 @@ function NavigationStack({
|
|
|
267
288
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
268
289
|
style: styles.stackItemPath,
|
|
269
290
|
numberOfLines: 1,
|
|
291
|
+
testID: `${itemTestID}-path`,
|
|
270
292
|
children: item.pathname
|
|
271
293
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton, {
|
|
272
294
|
value: item.pathname,
|
|
273
|
-
buttonStyle: styles.copyRouteButton
|
|
295
|
+
buttonStyle: styles.copyRouteButton,
|
|
296
|
+
testID: `${itemTestID}-copy`
|
|
274
297
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
275
298
|
style: [styles.statusBadge, {
|
|
276
299
|
backgroundColor: item.isFocused ? `${_sharedUi.buoyColors.success}15` : "#3B82F615",
|
|
@@ -280,6 +303,7 @@ function NavigationStack({
|
|
|
280
303
|
style: [styles.statusBadgeText, {
|
|
281
304
|
color: item.isFocused ? _sharedUi.buoyColors.success : "#3B82F6"
|
|
282
305
|
}],
|
|
306
|
+
testID: `${itemTestID}-status`,
|
|
283
307
|
children: item.isFocused ? "FOCUSED" : "MOUNTED"
|
|
284
308
|
})
|
|
285
309
|
})]
|
|
@@ -292,6 +316,7 @@ function NavigationStack({
|
|
|
292
316
|
children: "Route Name:"
|
|
293
317
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
294
318
|
style: styles.detailValue,
|
|
319
|
+
testID: `${itemTestID}-name`,
|
|
295
320
|
children: item.name
|
|
296
321
|
})]
|
|
297
322
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
@@ -301,6 +326,7 @@ function NavigationStack({
|
|
|
301
326
|
children: "Position:"
|
|
302
327
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
303
328
|
style: styles.detailValue,
|
|
329
|
+
testID: `${itemTestID}-position`,
|
|
304
330
|
children: [actualIndex, " / ", stackDepth - 1]
|
|
305
331
|
})]
|
|
306
332
|
}), hasParams && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
@@ -318,6 +344,9 @@ function NavigationStack({
|
|
|
318
344
|
style: [styles.actionButton, isAtRoot && styles.actionButtonDisabled],
|
|
319
345
|
onPress: handleGoBack,
|
|
320
346
|
disabled: isAtRoot,
|
|
347
|
+
testID: `${itemTestID}-back`,
|
|
348
|
+
accessibilityRole: "button",
|
|
349
|
+
accessibilityLabel: "Go back one screen",
|
|
321
350
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
322
351
|
style: [styles.actionButtonText, isAtRoot && styles.actionButtonTextDisabled],
|
|
323
352
|
children: "Back"
|
|
@@ -332,6 +361,9 @@ function NavigationStack({
|
|
|
332
361
|
style: [styles.actionButton, item.isFocused && styles.actionButtonDisabled],
|
|
333
362
|
onPress: handleGo,
|
|
334
363
|
disabled: item.isFocused,
|
|
364
|
+
testID: `${itemTestID}-go`,
|
|
365
|
+
accessibilityRole: "button",
|
|
366
|
+
accessibilityLabel: `Navigate to ${item.pathname}`,
|
|
335
367
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
336
368
|
style: [styles.actionButtonText, item.isFocused && styles.actionButtonTextDisabled],
|
|
337
369
|
children: "Go"
|
|
@@ -346,6 +378,9 @@ function NavigationStack({
|
|
|
346
378
|
style: [styles.actionButton, (item.isFocused || actualIndex === stackDepth - 1) && styles.actionButtonDisabled],
|
|
347
379
|
onPress: handlePopTo,
|
|
348
380
|
disabled: item.isFocused || actualIndex === stackDepth - 1,
|
|
381
|
+
testID: `${itemTestID}-pop-to`,
|
|
382
|
+
accessibilityRole: "button",
|
|
383
|
+
accessibilityLabel: `Remove screens above ${item.pathname}`,
|
|
349
384
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
350
385
|
style: [styles.actionButtonText, (item.isFocused || actualIndex === stackDepth - 1) && styles.actionButtonTextDisabled],
|
|
351
386
|
children: "Pop To"
|
|
@@ -27,6 +27,7 @@ function RouteEventDetail({
|
|
|
27
27
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
28
28
|
style: styles.container,
|
|
29
29
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
30
|
+
testID: "buoy-routes-detail-scroll",
|
|
30
31
|
style: styles.scroll,
|
|
31
32
|
contentContainerStyle: styles.scrollContent,
|
|
32
33
|
showsVerticalScrollIndicator: true,
|
|
@@ -41,6 +42,9 @@ function RouteEventDetail({
|
|
|
41
42
|
style: styles.goButton,
|
|
42
43
|
onPress: () => onNavigate(event.pathname),
|
|
43
44
|
activeOpacity: 0.8,
|
|
45
|
+
testID: "buoy-routes-detail-go",
|
|
46
|
+
accessibilityRole: "button",
|
|
47
|
+
accessibilityLabel: `Go to route ${event.pathname}`,
|
|
44
48
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Navigation, {
|
|
45
49
|
size: 16,
|
|
46
50
|
color: _sharedUi.night.color.onAccent
|
|
@@ -41,11 +41,13 @@ function RouteEventExpandedContent({
|
|
|
41
41
|
style: styles.section,
|
|
42
42
|
children: [routeTemplate && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
43
43
|
style: styles.detailRow,
|
|
44
|
+
testID: "buoy-routes-detail-template",
|
|
44
45
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
45
46
|
style: styles.detailLabel,
|
|
46
47
|
children: "Template:"
|
|
47
48
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
48
49
|
style: styles.detailValue,
|
|
50
|
+
testID: "buoy-routes-detail-template-value",
|
|
49
51
|
children: routeTemplate
|
|
50
52
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton, {
|
|
51
53
|
value: routeTemplate,
|
|
@@ -53,11 +55,13 @@ function RouteEventExpandedContent({
|
|
|
53
55
|
})]
|
|
54
56
|
}), event.previousPathname && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
55
57
|
style: styles.detailRow,
|
|
58
|
+
testID: "buoy-routes-detail-from",
|
|
56
59
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
57
60
|
style: styles.detailLabel,
|
|
58
61
|
children: "From:"
|
|
59
62
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
60
63
|
style: styles.detailValue,
|
|
64
|
+
testID: "buoy-routes-detail-from-value",
|
|
61
65
|
children: event.previousPathname
|
|
62
66
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton, {
|
|
63
67
|
value: event.previousPathname,
|
|
@@ -65,11 +69,13 @@ function RouteEventExpandedContent({
|
|
|
65
69
|
})]
|
|
66
70
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
67
71
|
style: styles.detailRow,
|
|
72
|
+
testID: "buoy-routes-detail-to",
|
|
68
73
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
69
74
|
style: styles.detailLabel,
|
|
70
75
|
children: "To:"
|
|
71
76
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
72
77
|
style: styles.detailValue,
|
|
78
|
+
testID: "buoy-routes-detail-to-value",
|
|
73
79
|
children: event.pathname
|
|
74
80
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton, {
|
|
75
81
|
value: event.pathname,
|
|
@@ -80,20 +86,24 @@ function RouteEventExpandedContent({
|
|
|
80
86
|
style: styles.section,
|
|
81
87
|
children: [event.timeSincePrevious !== undefined && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
82
88
|
style: styles.detailRow,
|
|
89
|
+
testID: "buoy-routes-detail-duration",
|
|
83
90
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
84
91
|
style: styles.detailLabel,
|
|
85
92
|
children: "Duration:"
|
|
86
93
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
87
94
|
style: styles.detailValue,
|
|
95
|
+
testID: "buoy-routes-detail-duration-value",
|
|
88
96
|
children: formatDuration(event.timeSincePrevious)
|
|
89
97
|
})]
|
|
90
98
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
91
99
|
style: styles.detailRow,
|
|
100
|
+
testID: "buoy-routes-detail-time",
|
|
92
101
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
93
102
|
style: styles.detailLabel,
|
|
94
103
|
children: "Time:"
|
|
95
104
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
96
105
|
style: styles.detailValue,
|
|
106
|
+
testID: "buoy-routes-detail-time-value",
|
|
97
107
|
children: new Date(event.timestamp).toLocaleString()
|
|
98
108
|
})]
|
|
99
109
|
})]
|
|
@@ -101,11 +111,13 @@ function RouteEventExpandedContent({
|
|
|
101
111
|
style: styles.section,
|
|
102
112
|
children: [event.segments && event.segments.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
103
113
|
style: styles.detailRow,
|
|
114
|
+
testID: "buoy-routes-detail-segments",
|
|
104
115
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
105
116
|
style: styles.detailLabel,
|
|
106
117
|
children: "Segments:"
|
|
107
118
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
108
119
|
style: styles.detailValue,
|
|
120
|
+
testID: "buoy-routes-detail-segments-value",
|
|
109
121
|
children: event.segments.join(' → ')
|
|
110
122
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton, {
|
|
111
123
|
value: JSON.stringify(event.segments),
|
|
@@ -117,6 +129,7 @@ function RouteEventExpandedContent({
|
|
|
117
129
|
style: styles.dataViewerHeader,
|
|
118
130
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
119
131
|
style: styles.dataViewerTitle,
|
|
132
|
+
testID: "buoy-routes-detail-params-title",
|
|
120
133
|
children: ["Parameters (", Object.keys(event.params).length, ")"]
|
|
121
134
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton, {
|
|
122
135
|
value: JSON.stringify(event.params, null, 2),
|
|
@@ -131,11 +144,13 @@ function RouteEventExpandedContent({
|
|
|
131
144
|
})]
|
|
132
145
|
}), visitNumber > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
133
146
|
style: styles.detailRow,
|
|
147
|
+
testID: "buoy-routes-detail-visited",
|
|
134
148
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
135
149
|
style: styles.detailLabel,
|
|
136
150
|
children: "Visited:"
|
|
137
151
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
138
152
|
style: styles.detailValue,
|
|
153
|
+
testID: "buoy-routes-detail-visited-value",
|
|
139
154
|
children: [visitNumber, " time", visitNumber !== 1 ? 's' : '']
|
|
140
155
|
})]
|
|
141
156
|
})]
|
|
@@ -9,6 +9,7 @@ var _reactNative = require("react-native");
|
|
|
9
9
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
10
10
|
var _RouteEventExpandedContent = require("./RouteEventExpandedContent");
|
|
11
11
|
var _routeTemplate = require("../utils/routeTemplate");
|
|
12
|
+
var _testIDs = require("../utils/testIDs");
|
|
12
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
14
|
/**
|
|
14
15
|
* RouteEventItemCompact - Compact list item for route events
|
|
@@ -116,6 +117,11 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
|
|
|
116
117
|
const statusLabel = (0, _react.useMemo)(() => getStatusLabel(routeType), [routeType]);
|
|
117
118
|
const statusSublabel = (0, _react.useMemo)(() => getStatusSublabel(event), [event]);
|
|
118
119
|
const badgeText = (0, _react.useMemo)(() => getBadgeText(event), [event]);
|
|
120
|
+
const testID = (0, _react.useMemo)(() => (0, _testIDs.eventRowTestID)(event.pathname, visitNumber), [event.pathname, visitNumber]);
|
|
121
|
+
// Spelled out for assistive tech: the row's whole verdict is a coloured dot
|
|
122
|
+
// and two short words that Fabric merges into the row's container, so
|
|
123
|
+
// VoiceOver would otherwise announce a control it cannot describe.
|
|
124
|
+
const accessibilityLabel = (0, _react.useMemo)(() => `${event.pathname}, ${statusLabel}, ${statusSublabel}`, [event.pathname, statusLabel, statusSublabel]);
|
|
119
125
|
const expandedContent = (0, _react.useMemo)(() => {
|
|
120
126
|
if (!isExpanded) return undefined;
|
|
121
127
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventExpandedContent.RouteEventExpandedContent, {
|
|
@@ -134,6 +140,7 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
|
|
|
134
140
|
style: [styles.badgeText, {
|
|
135
141
|
color: statusColor
|
|
136
142
|
}],
|
|
143
|
+
testID: `${testID}-badge`,
|
|
137
144
|
children: badgeText
|
|
138
145
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
139
146
|
style: styles.goButton,
|
|
@@ -141,13 +148,16 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
|
|
|
141
148
|
e.stopPropagation();
|
|
142
149
|
onNavigate(event.pathname);
|
|
143
150
|
},
|
|
151
|
+
testID: `${testID}-go`,
|
|
152
|
+
accessibilityRole: "button",
|
|
153
|
+
accessibilityLabel: `Go to ${event.pathname}`,
|
|
144
154
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
145
155
|
style: styles.goButtonText,
|
|
146
156
|
children: "Go"
|
|
147
157
|
})
|
|
148
158
|
})]
|
|
149
159
|
});
|
|
150
|
-
}, [onNavigate, event.pathname, badgeText, statusColor]);
|
|
160
|
+
}, [onNavigate, event.pathname, badgeText, statusColor, testID]);
|
|
151
161
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CompactRow, {
|
|
152
162
|
statusDotColor: statusColor,
|
|
153
163
|
statusLabel: statusLabel,
|
|
@@ -165,7 +175,11 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
|
|
|
165
175
|
expandedContent: expandedContent,
|
|
166
176
|
isExpanded: isExpanded,
|
|
167
177
|
expandedGlowColor: statusColor,
|
|
168
|
-
onPress: () => onPress(index)
|
|
178
|
+
onPress: () => onPress(index),
|
|
179
|
+
testID: testID,
|
|
180
|
+
statusTestID: `${testID}-status`,
|
|
181
|
+
statusSublabelTestID: `${testID}-sublabel`,
|
|
182
|
+
accessibilityLabel: accessibilityLabel
|
|
169
183
|
});
|
|
170
184
|
});
|
|
171
185
|
const styles = _reactNative.StyleSheet.create({
|
|
@@ -189,7 +189,11 @@ function RouteEventsModalWithTabs({
|
|
|
189
189
|
const loadTabState = async () => {
|
|
190
190
|
try {
|
|
191
191
|
const storedTab = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.routeEvents.activeTab());
|
|
192
|
-
|
|
192
|
+
// Every tab the selector offers, including "stack": the save side has
|
|
193
|
+
// always written it, so leaving it off this list meant the Stack tab
|
|
194
|
+
// was persisted and then silently discarded on the next launch —
|
|
195
|
+
// you came back to Events having chosen otherwise.
|
|
196
|
+
if (storedTab === "routes" || storedTab === "events" || storedTab === "stack") {
|
|
193
197
|
setActiveTab(storedTab);
|
|
194
198
|
}
|
|
195
199
|
hasLoadedTabState.current = true;
|
|
@@ -361,20 +365,28 @@ function RouteEventsModalWithTabs({
|
|
|
361
365
|
const visibleEvents = (0, _react.useMemo)(() => filteredEvents.slice(0, eventLimit), [filteredEvents, eventLimit]);
|
|
362
366
|
const lockedEventCount = (0, _react.useMemo)(() => Math.max(0, filteredEvents.length - eventLimit), [filteredEvents.length, eventLimit]);
|
|
363
367
|
|
|
364
|
-
|
|
368
|
+
/**
|
|
369
|
+
* "This was the Nth time you were here", per event.
|
|
370
|
+
*
|
|
371
|
+
* Keyed by the EVENT, not by its index. Indices belong to the unfiltered
|
|
372
|
+
* list, and every consumer below reads them off the FILTERED one — so with
|
|
373
|
+
* a search typed, or any ignore pattern active (there are three by
|
|
374
|
+
* default), each row was handed the visit count of whatever event sat at
|
|
375
|
+
* that position in the full history. Rows read plausibly and were simply
|
|
376
|
+
* about a different navigation.
|
|
377
|
+
*/
|
|
365
378
|
const visitCounts = (0, _react.useMemo)(() => {
|
|
366
379
|
const counts = new Map();
|
|
367
|
-
const
|
|
380
|
+
const byEvent = new Map();
|
|
368
381
|
|
|
369
|
-
//
|
|
382
|
+
// Oldest to newest, so the first time you visited a route is visit 1.
|
|
370
383
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
371
384
|
const event = events[i];
|
|
372
|
-
const
|
|
373
|
-
const visitNumber = currentCount + 1;
|
|
385
|
+
const visitNumber = (counts.get(event.pathname) || 0) + 1;
|
|
374
386
|
counts.set(event.pathname, visitNumber);
|
|
375
|
-
|
|
387
|
+
byEvent.set(event, visitNumber);
|
|
376
388
|
}
|
|
377
|
-
return
|
|
389
|
+
return byEvent;
|
|
378
390
|
}, [events]);
|
|
379
391
|
|
|
380
392
|
// Prepare copy data - memoized so it only rebuilds when dependencies change
|
|
@@ -389,7 +401,7 @@ function RouteEventsModalWithTabs({
|
|
|
389
401
|
},
|
|
390
402
|
events: filteredEvents.map((event, index) => ({
|
|
391
403
|
index,
|
|
392
|
-
visitNumber: visitCounts.get(
|
|
404
|
+
visitNumber: visitCounts.get(event) || 1,
|
|
393
405
|
pathname: event.pathname,
|
|
394
406
|
timestamp: event.timestamp,
|
|
395
407
|
timestampRelative: (0, _sharedUi.formatRelativeTime)(event.timestamp),
|
|
@@ -451,23 +463,29 @@ function RouteEventsModalWithTabs({
|
|
|
451
463
|
if (filteredEvents.length === 0) {
|
|
452
464
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
453
465
|
style: styles.contentWrapper,
|
|
466
|
+
testID: "buoy-routes-events",
|
|
454
467
|
children: [showSearchBar && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
455
468
|
style: styles.searchContainer,
|
|
469
|
+
testID: "buoy-routes-search-field",
|
|
456
470
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.SearchBar, {
|
|
457
471
|
value: searchQuery,
|
|
458
472
|
onChange: setSearchQuery,
|
|
459
|
-
placeholder: "Search pathname or params..."
|
|
473
|
+
placeholder: "Search pathname or params...",
|
|
474
|
+
testID: "buoy-routes-search"
|
|
460
475
|
})
|
|
461
476
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
462
477
|
style: styles.emptyState,
|
|
478
|
+
testID: "buoy-routes-empty",
|
|
463
479
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Navigation, {
|
|
464
480
|
size: 48,
|
|
465
481
|
color: _sharedUi.night.color.textTertiary
|
|
466
482
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
467
483
|
style: styles.emptyTitle,
|
|
484
|
+
testID: "buoy-routes-empty-title",
|
|
468
485
|
children: searchQuery.trim() ? "No matching events" : isListening ? "No route events yet" : "Event listener is paused"
|
|
469
486
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
470
487
|
style: styles.emptySubtitle,
|
|
488
|
+
testID: "buoy-routes-empty-subtitle",
|
|
471
489
|
children: searchQuery.trim() ? "Try a different search term" : isListening ? "Navigation events will appear here" : "Press play to start monitoring"
|
|
472
490
|
})]
|
|
473
491
|
})]
|
|
@@ -477,20 +495,25 @@ function RouteEventsModalWithTabs({
|
|
|
477
495
|
// Show chronological timeline of events
|
|
478
496
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
479
497
|
style: styles.contentWrapper,
|
|
498
|
+
testID: "buoy-routes-events",
|
|
480
499
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
481
500
|
style: styles.searchContainer,
|
|
501
|
+
testID: "buoy-routes-search-field",
|
|
482
502
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.SearchBar, {
|
|
483
503
|
value: searchQuery,
|
|
484
504
|
onChange: setSearchQuery,
|
|
485
|
-
placeholder: "Search pathname or params..."
|
|
505
|
+
placeholder: "Search pathname or params...",
|
|
506
|
+
testID: "buoy-routes-search"
|
|
486
507
|
})
|
|
487
508
|
}), lockedEventCount > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
488
509
|
style: styles.lockedEventsBanner,
|
|
510
|
+
testID: "buoy-routes-locked-banner",
|
|
489
511
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
|
|
490
512
|
size: 14,
|
|
491
513
|
color: _sharedUi.night.color.warning
|
|
492
514
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
493
515
|
style: styles.lockedEventsBannerText,
|
|
516
|
+
testID: "buoy-routes-locked-banner-text",
|
|
494
517
|
children: [lockedEventCount, " older ", lockedEventCount === 1 ? 'event' : 'events', " locked"]
|
|
495
518
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
496
519
|
style: styles.lockedEventsBannerSubtext,
|
|
@@ -508,9 +531,11 @@ function RouteEventsModalWithTabs({
|
|
|
508
531
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
509
532
|
onBack: handleBackFromDetail
|
|
510
533
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
511
|
-
title: selectedEvent.pathname
|
|
534
|
+
title: selectedEvent.pathname,
|
|
535
|
+
testID: "buoy-routes-detail-title"
|
|
512
536
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
|
|
513
537
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
538
|
+
testID: "buoy-routes-detail-copy",
|
|
514
539
|
value: {
|
|
515
540
|
pathname: selectedEvent.pathname,
|
|
516
541
|
previousPathname: selectedEvent.previousPathname,
|
|
@@ -528,7 +553,8 @@ function RouteEventsModalWithTabs({
|
|
|
528
553
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
529
554
|
onBack: () => setShowFilters(false)
|
|
530
555
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
531
|
-
title: "Filters"
|
|
556
|
+
title: "Filters",
|
|
557
|
+
testID: "buoy-routes-filters-title"
|
|
532
558
|
})]
|
|
533
559
|
}) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
534
560
|
children: [onBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
@@ -548,19 +574,25 @@ function RouteEventsModalWithTabs({
|
|
|
548
574
|
label: "Stack"
|
|
549
575
|
}],
|
|
550
576
|
activeTab: activeTab,
|
|
551
|
-
onTabChange: tab => handleTabChange(tab)
|
|
577
|
+
onTabChange: tab => handleTabChange(tab),
|
|
578
|
+
testIDPrefix: "buoy-routes-tab"
|
|
552
579
|
})
|
|
553
580
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
|
|
554
581
|
children: [activeTab === "stack" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
555
582
|
value: stackCopyValue,
|
|
556
|
-
buttonStyle: styles.iconButton
|
|
583
|
+
buttonStyle: styles.iconButton,
|
|
584
|
+
testID: "buoy-routes-stack-copy"
|
|
557
585
|
}), activeTab === "events" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
558
586
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
559
587
|
value: copyAllEventsData,
|
|
560
|
-
buttonStyle: styles.iconButton
|
|
588
|
+
buttonStyle: styles.iconButton,
|
|
589
|
+
testID: "buoy-routes-copy"
|
|
561
590
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
562
591
|
onPress: handleToggleFilters,
|
|
563
592
|
style: [styles.iconButton, ignoredPatterns.size > 0 && styles.activeFilterButton],
|
|
593
|
+
testID: "buoy-routes-filters",
|
|
594
|
+
accessibilityRole: "button",
|
|
595
|
+
accessibilityLabel: ignoredPatterns.size > 0 ? `Route filters, ${ignoredPatterns.size} active` : "Route filters",
|
|
564
596
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
|
|
565
597
|
size: 14,
|
|
566
598
|
color: ignoredPatterns.size > 0 ? _sharedUi.night.color.accent : _sharedUi.night.color.textSecondary
|
|
@@ -568,10 +600,14 @@ function RouteEventsModalWithTabs({
|
|
|
568
600
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
|
|
569
601
|
isEnabled: isListening,
|
|
570
602
|
onToggle: handleToggleListening,
|
|
571
|
-
accessibilityLabel: "Toggle route event monitoring"
|
|
603
|
+
accessibilityLabel: "Toggle route event monitoring",
|
|
604
|
+
testID: "buoy-routes-power"
|
|
572
605
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
573
606
|
onPress: handleClearEvents,
|
|
574
607
|
style: styles.iconButton,
|
|
608
|
+
testID: "buoy-routes-clear",
|
|
609
|
+
accessibilityRole: "button",
|
|
610
|
+
accessibilityLabel: "Clear route events",
|
|
575
611
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
|
|
576
612
|
size: 14,
|
|
577
613
|
color: _sharedUi.night.color.danger
|
|
@@ -667,7 +703,8 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
667
703
|
paddingVertical: 8,
|
|
668
704
|
borderBottomWidth: 1,
|
|
669
705
|
borderBottomColor: _sharedUi.buoyColors.border,
|
|
670
|
-
|
|
706
|
+
// Clear, like Bench — the host's hoisted sky shows through the header.
|
|
707
|
+
backgroundColor: "transparent"
|
|
671
708
|
},
|
|
672
709
|
iconButton: {
|
|
673
710
|
padding: 6,
|
|
@@ -28,19 +28,22 @@ function RouteEventsTimeline({
|
|
|
28
28
|
const handleItemPress = (0, _react.useCallback)(index => {
|
|
29
29
|
const event = events[index];
|
|
30
30
|
if (!event) return;
|
|
31
|
-
onSelectEvent(event, visitCounts.get(
|
|
31
|
+
onSelectEvent(event, visitCounts.get(event) || 1);
|
|
32
32
|
}, [events, visitCounts, onSelectEvent]);
|
|
33
33
|
if (events.length === 0) {
|
|
34
34
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
35
35
|
style: styles.emptyState,
|
|
36
|
+
testID: "buoy-routes-empty",
|
|
36
37
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Navigation, {
|
|
37
38
|
size: 48,
|
|
38
39
|
color: _sharedUi.night.color.textTertiary
|
|
39
40
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
40
41
|
style: styles.emptyTitle,
|
|
42
|
+
testID: "buoy-routes-empty-title",
|
|
41
43
|
children: "No events yet"
|
|
42
44
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
43
45
|
style: styles.emptySubtitle,
|
|
46
|
+
testID: "buoy-routes-empty-subtitle",
|
|
44
47
|
children: "Navigation events will appear here"
|
|
45
48
|
})]
|
|
46
49
|
});
|
|
@@ -51,6 +54,7 @@ function RouteEventsTimeline({
|
|
|
51
54
|
// the 500-event cap that measured 32.8 JS FPS during an idle dwell,
|
|
52
55
|
// tool-bench 2026-07-06).
|
|
53
56
|
(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
57
|
+
testID: "buoy-routes-timeline",
|
|
54
58
|
contentContainerStyle: styles.listContent,
|
|
55
59
|
data: events
|
|
56
60
|
// STABLE content keys — index-based keys remount every row when a new
|
|
@@ -63,14 +67,19 @@ function RouteEventsTimeline({
|
|
|
63
67
|
index
|
|
64
68
|
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventItemCompact.RouteEventItemCompact, {
|
|
65
69
|
event: event,
|
|
66
|
-
visitNumber: visitCounts.get(
|
|
70
|
+
visitNumber: visitCounts.get(event) || 1,
|
|
67
71
|
isExpanded: false,
|
|
68
72
|
index: index,
|
|
69
73
|
onPress: handleItemPress,
|
|
70
74
|
onNavigate: onNavigate
|
|
71
75
|
}),
|
|
72
|
-
showsVerticalScrollIndicator: true
|
|
73
|
-
|
|
76
|
+
showsVerticalScrollIndicator: true
|
|
77
|
+
// Clipped cells fail Detox's hittability check even when they are on
|
|
78
|
+
// screen, so the e2e bundle keeps every mounted row targetable. The
|
|
79
|
+
// env var is inlined at bundle time; guarded so a non-inlining bundler
|
|
80
|
+
// keeps production behaviour.
|
|
81
|
+
,
|
|
82
|
+
removeClippedSubviews: !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E),
|
|
74
83
|
initialNumToRender: 12,
|
|
75
84
|
maxToRenderPerBatch: 8,
|
|
76
85
|
windowSize: 5,
|