@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
|
@@ -9,6 +9,7 @@ var _sharedUi = require("@buoy-gg/shared-ui");
|
|
|
9
9
|
var _reactNative = require("react-native");
|
|
10
10
|
var _license = require("@buoy-gg/license");
|
|
11
11
|
var _useRouteSitemap = require("../useRouteSitemap");
|
|
12
|
+
var _testIDs = require("../utils/testIDs");
|
|
12
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
14
|
/**
|
|
14
15
|
* RoutesSitemap - Visual sitemap of all app routes
|
|
@@ -256,6 +257,7 @@ function RoutesSitemap({
|
|
|
256
257
|
style: styles.loadingContainer,
|
|
257
258
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
258
259
|
style: styles.loadingText,
|
|
260
|
+
testID: "buoy-routes-sitemap-loading",
|
|
259
261
|
children: isSupported ? "Loading routes..." : "Route sitemap isn't available on the dashboard — the route tree lives on the device. Use the Events tab to see navigation here."
|
|
260
262
|
})
|
|
261
263
|
})
|
|
@@ -263,8 +265,14 @@ function RoutesSitemap({
|
|
|
263
265
|
}
|
|
264
266
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
265
267
|
style: [styles.container, style],
|
|
266
|
-
children: [isSearching &&
|
|
268
|
+
children: [isSearching &&
|
|
269
|
+
/*#__PURE__*/
|
|
270
|
+
// The wrapper carries the handle as well as the field: a TextInput's
|
|
271
|
+
// own testID never reaches the XCUITest element tree on this RN
|
|
272
|
+
// version, so the a11y lane has nothing else to aim at.
|
|
273
|
+
(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
267
274
|
style: styles.searchContainer,
|
|
275
|
+
testID: "buoy-routes-sitemap-search-field",
|
|
268
276
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
|
|
269
277
|
size: 16,
|
|
270
278
|
color: _sharedUi.night.color.textTertiary
|
|
@@ -276,10 +284,15 @@ function RoutesSitemap({
|
|
|
276
284
|
onChangeText: setSearchQuery,
|
|
277
285
|
autoCapitalize: "none",
|
|
278
286
|
autoCorrect: false,
|
|
279
|
-
autoFocus: true
|
|
287
|
+
autoFocus: true,
|
|
288
|
+
testID: "buoy-routes-sitemap-search",
|
|
289
|
+
accessibilityLabel: "Search routes"
|
|
280
290
|
}), searchQuery.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
281
291
|
onPress: () => setSearchQuery(""),
|
|
282
292
|
style: styles.clearButton,
|
|
293
|
+
testID: "buoy-routes-sitemap-search-clear",
|
|
294
|
+
accessibilityRole: "button",
|
|
295
|
+
accessibilityLabel: "Clear route search",
|
|
283
296
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
284
297
|
style: styles.clearButtonText,
|
|
285
298
|
children: "\u2715"
|
|
@@ -290,12 +303,16 @@ function RoutesSitemap({
|
|
|
290
303
|
setIsSearching(false);
|
|
291
304
|
setSearchQuery("");
|
|
292
305
|
},
|
|
306
|
+
testID: "buoy-routes-sitemap-search-done",
|
|
307
|
+
accessibilityRole: "button",
|
|
308
|
+
accessibilityLabel: "Done searching routes",
|
|
293
309
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
294
310
|
style: styles.closeSearchText,
|
|
295
311
|
children: "Done"
|
|
296
312
|
})
|
|
297
313
|
})]
|
|
298
314
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
|
|
315
|
+
testID: "buoy-routes-sitemap-scroll",
|
|
299
316
|
style: styles.scrollView,
|
|
300
317
|
contentContainerStyle: styles.scrollContent,
|
|
301
318
|
children: [!isSearching && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
@@ -308,6 +325,8 @@ function RoutesSitemap({
|
|
|
308
325
|
style: [styles.iconButton, IS_DESKTOP && styles.iconButtonDesktop],
|
|
309
326
|
onPress: handleGoHome,
|
|
310
327
|
accessibilityLabel: "Go to home route",
|
|
328
|
+
accessibilityRole: "button",
|
|
329
|
+
testID: "buoy-routes-sitemap-home",
|
|
311
330
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Home, {
|
|
312
331
|
size: ACTION_ICON_SIZE,
|
|
313
332
|
color: _sharedUi.night.color.textSecondary
|
|
@@ -320,6 +339,7 @@ function RoutesSitemap({
|
|
|
320
339
|
style: [styles.actionWrapper, IS_DESKTOP && styles.actionWrapperDesktop],
|
|
321
340
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CopyButton, {
|
|
322
341
|
value: copyAllData,
|
|
342
|
+
testID: "buoy-routes-sitemap-copy",
|
|
323
343
|
size: IS_DESKTOP ? ACTION_ICON_SIZE : 14,
|
|
324
344
|
buttonStyle: IS_DESKTOP ? styles.actionButtonHeaderDesktop : styles.actionButtonHeader
|
|
325
345
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -332,6 +352,9 @@ function RoutesSitemap({
|
|
|
332
352
|
style: [styles.iconButton, IS_DESKTOP && styles.iconButtonDesktop, isRefreshing && styles.refreshButtonDisabled],
|
|
333
353
|
onPress: handleManualRefresh,
|
|
334
354
|
disabled: isRefreshing,
|
|
355
|
+
testID: "buoy-routes-sitemap-refresh",
|
|
356
|
+
accessibilityRole: "button",
|
|
357
|
+
accessibilityLabel: "Refresh route tree",
|
|
335
358
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RefreshCw, {
|
|
336
359
|
size: ACTION_ICON_SIZE,
|
|
337
360
|
color: isRefreshing ? _sharedUi.night.color.textTertiary : _sharedUi.night.color.textSecondary
|
|
@@ -345,6 +368,9 @@ function RoutesSitemap({
|
|
|
345
368
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
346
369
|
style: [styles.iconButton, IS_DESKTOP && styles.iconButtonDesktop],
|
|
347
370
|
onPress: () => setIsSearching(true),
|
|
371
|
+
testID: "buoy-routes-sitemap-search-open",
|
|
372
|
+
accessibilityRole: "button",
|
|
373
|
+
accessibilityLabel: "Search routes",
|
|
348
374
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
|
|
349
375
|
size: ACTION_ICON_SIZE,
|
|
350
376
|
color: _sharedUi.night.color.textSecondary
|
|
@@ -358,28 +384,36 @@ function RoutesSitemap({
|
|
|
358
384
|
style: styles.statsGrid,
|
|
359
385
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(StatItem, {
|
|
360
386
|
value: stats.total,
|
|
361
|
-
label: "Total"
|
|
387
|
+
label: "Total",
|
|
388
|
+
id: "total"
|
|
362
389
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(StatItem, {
|
|
363
390
|
value: stats.static,
|
|
364
|
-
label: "Static"
|
|
391
|
+
label: "Static",
|
|
392
|
+
id: "static"
|
|
365
393
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(StatItem, {
|
|
366
394
|
value: stats.dynamic,
|
|
367
|
-
label: "Dynamic"
|
|
395
|
+
label: "Dynamic",
|
|
396
|
+
id: "dynamic"
|
|
368
397
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(StatItem, {
|
|
369
398
|
value: stats.catchAll,
|
|
370
|
-
label: "Catch-All"
|
|
399
|
+
label: "Catch-All",
|
|
400
|
+
id: "catch-all"
|
|
371
401
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(StatItem, {
|
|
372
402
|
value: stats.layouts,
|
|
373
|
-
label: "Layouts"
|
|
403
|
+
label: "Layouts",
|
|
404
|
+
id: "layouts"
|
|
374
405
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(StatItem, {
|
|
375
406
|
value: stats.groups,
|
|
376
|
-
label: "Groups"
|
|
407
|
+
label: "Groups",
|
|
408
|
+
id: "groups"
|
|
377
409
|
})]
|
|
378
410
|
})]
|
|
379
411
|
}), displayGroups.length === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
380
412
|
style: styles.emptyState,
|
|
413
|
+
testID: "buoy-routes-sitemap-empty",
|
|
381
414
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
382
415
|
style: styles.emptyText,
|
|
416
|
+
testID: "buoy-routes-sitemap-empty-text",
|
|
383
417
|
children: "No routes found"
|
|
384
418
|
})
|
|
385
419
|
}) : displayGroups.map(group => /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteGroupView, {
|
|
@@ -391,11 +425,13 @@ function RoutesSitemap({
|
|
|
391
425
|
style: styles.metaFooter,
|
|
392
426
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
393
427
|
style: styles.metaText,
|
|
428
|
+
testID: "buoy-routes-sitemap-updated",
|
|
394
429
|
children: lastRefreshLabel
|
|
395
430
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
396
431
|
style: styles.sourceBadge,
|
|
397
432
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
398
433
|
style: styles.sourceText,
|
|
434
|
+
testID: "buoy-routes-sitemap-source",
|
|
399
435
|
children: sourceLabel
|
|
400
436
|
})
|
|
401
437
|
})]
|
|
@@ -414,12 +450,19 @@ function RoutesSitemap({
|
|
|
414
450
|
|
|
415
451
|
function StatItem({
|
|
416
452
|
value,
|
|
417
|
-
label
|
|
453
|
+
label,
|
|
454
|
+
id
|
|
418
455
|
}) {
|
|
419
456
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
420
457
|
style: styles.statItem,
|
|
458
|
+
testID: `buoy-routes-stat-${id}`
|
|
459
|
+
// Fabric merges the number and its caption into this container, so a
|
|
460
|
+
// stat that isn't spelled out announces as a bare digit.
|
|
461
|
+
,
|
|
462
|
+
accessibilityLabel: `${label}, ${value}`,
|
|
421
463
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
422
464
|
style: styles.statValue,
|
|
465
|
+
testID: `buoy-routes-stat-${id}-value`,
|
|
423
466
|
children: value
|
|
424
467
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
425
468
|
style: styles.statLabel,
|
|
@@ -439,6 +482,9 @@ function RouteGroupView({
|
|
|
439
482
|
style: styles.groupHeader,
|
|
440
483
|
onPress: onToggleExpand,
|
|
441
484
|
activeOpacity: 0.7,
|
|
485
|
+
testID: (0, _testIDs.sitemapGroupTestID)(group.title),
|
|
486
|
+
accessibilityRole: "button",
|
|
487
|
+
accessibilityLabel: `${group.title}, ${group.routes.length} routes`,
|
|
442
488
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
443
489
|
style: styles.groupHeaderLeft,
|
|
444
490
|
children: [isExpanded ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChevronDown, {
|
|
@@ -455,6 +501,7 @@ function RouteGroupView({
|
|
|
455
501
|
style: styles.groupBadge,
|
|
456
502
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
457
503
|
style: styles.groupCount,
|
|
504
|
+
testID: `${(0, _testIDs.sitemapGroupTestID)(group.title)}-count`,
|
|
458
505
|
children: group.routes.length
|
|
459
506
|
})
|
|
460
507
|
})]
|
|
@@ -482,6 +529,7 @@ function RouteItemView({
|
|
|
482
529
|
const hasChildren = route.children.length > 0;
|
|
483
530
|
const typeColor = getRouteTypeColor(route.type);
|
|
484
531
|
const canNavigate = route.type !== "layout" && route.type !== "group";
|
|
532
|
+
const testID = (0, _testIDs.sitemapRouteTestID)(route.path);
|
|
485
533
|
|
|
486
534
|
// Leaf routes (no children) have nothing to drill into — expanding would only
|
|
487
535
|
// reveal the Copy/Go actions, so show those inline instead of behind a toggle.
|
|
@@ -491,6 +539,7 @@ function RouteItemView({
|
|
|
491
539
|
style: [styles.routeItem, {
|
|
492
540
|
marginLeft: depth * 12
|
|
493
541
|
}],
|
|
542
|
+
testID: testID,
|
|
494
543
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
495
544
|
style: styles.routeCard,
|
|
496
545
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
@@ -499,6 +548,9 @@ function RouteItemView({
|
|
|
499
548
|
style: styles.routeHeaderLeft,
|
|
500
549
|
onPress: () => setIsExpanded(!isExpanded),
|
|
501
550
|
activeOpacity: 0.7,
|
|
551
|
+
testID: `${testID}-toggle`,
|
|
552
|
+
accessibilityRole: "button",
|
|
553
|
+
accessibilityLabel: `${route.path}, ${route.type}, ${route.children.length} nested`,
|
|
502
554
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
503
555
|
style: styles.expandIndicator,
|
|
504
556
|
children: isExpanded ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChevronDown, {
|
|
@@ -511,6 +563,7 @@ function RouteItemView({
|
|
|
511
563
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
512
564
|
style: styles.routePath,
|
|
513
565
|
numberOfLines: 3,
|
|
566
|
+
testID: `${testID}-path`,
|
|
514
567
|
children: route.path
|
|
515
568
|
})]
|
|
516
569
|
}) :
|
|
@@ -518,12 +571,19 @@ function RouteItemView({
|
|
|
518
571
|
// Non-toggling header for leaf routes (keeps the chevron column as a
|
|
519
572
|
// spacer so paths stay aligned with expandable siblings).
|
|
520
573
|
(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
521
|
-
style: styles.routeHeaderLeft
|
|
574
|
+
style: styles.routeHeaderLeft
|
|
575
|
+
// `accessible` as well as the label: a plain View is not an
|
|
576
|
+
// element, so without it the label is never announced and the
|
|
577
|
+
// path is read as a bare text node with no type beside it.
|
|
578
|
+
,
|
|
579
|
+
accessible: true,
|
|
580
|
+
accessibilityLabel: `${route.path}, ${route.type}`,
|
|
522
581
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
523
582
|
style: styles.expandIndicator
|
|
524
583
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
525
584
|
style: styles.routePath,
|
|
526
585
|
numberOfLines: 3,
|
|
586
|
+
testID: `${testID}-path`,
|
|
527
587
|
children: route.path
|
|
528
588
|
})]
|
|
529
589
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
@@ -532,6 +592,7 @@ function RouteItemView({
|
|
|
532
592
|
style: styles.childCountBadge,
|
|
533
593
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
534
594
|
style: styles.childCountText,
|
|
595
|
+
testID: `${testID}-children-count`,
|
|
535
596
|
children: route.children.length
|
|
536
597
|
})
|
|
537
598
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
@@ -543,15 +604,20 @@ function RouteItemView({
|
|
|
543
604
|
style: [styles.typeText, {
|
|
544
605
|
color: typeColor
|
|
545
606
|
}],
|
|
607
|
+
testID: `${testID}-type`,
|
|
546
608
|
children: route.type.toUpperCase()
|
|
547
609
|
})
|
|
548
610
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InlineCopyButton, {
|
|
549
611
|
value: route.path,
|
|
550
|
-
buttonStyle: styles.iconAction
|
|
612
|
+
buttonStyle: styles.iconAction,
|
|
613
|
+
testID: `${testID}-copy`
|
|
551
614
|
}), canNavigate && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
552
615
|
style: styles.goButton,
|
|
553
616
|
onPress: () => onNavigate(route),
|
|
554
617
|
activeOpacity: 0.7,
|
|
618
|
+
testID: `${testID}-go`,
|
|
619
|
+
accessibilityRole: "button",
|
|
620
|
+
accessibilityLabel: `Go to ${route.path}`,
|
|
555
621
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
556
622
|
style: styles.goButtonText,
|
|
557
623
|
children: "Go"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.eventRowTestID = eventRowTestID;
|
|
7
|
+
exports.pathSlug = pathSlug;
|
|
8
|
+
exports.sitemapGroupTestID = sitemapGroupTestID;
|
|
9
|
+
exports.sitemapRouteTestID = sitemapRouteTestID;
|
|
10
|
+
exports.stackItemTestID = stackItemTestID;
|
|
11
|
+
/**
|
|
12
|
+
* Stable e2e handles for the routes tool.
|
|
13
|
+
*
|
|
14
|
+
* Everything in this tool is addressed by its PATH, never by a list index or a
|
|
15
|
+
* per-session id. Route events are a prepend-list whose indices shift on every
|
|
16
|
+
* navigation, the sitemap re-sorts and regroups as you search, and the stack
|
|
17
|
+
* renumbers itself on every push — the pathname is the one thing that means the
|
|
18
|
+
* same screen on the next launch, and the only thing a test can name up front.
|
|
19
|
+
*
|
|
20
|
+
* A pathname is not testID-safe as it stands (`/blog/2024/react-native-tips`,
|
|
21
|
+
* `/pokemon/[id]`), so it is slugged the way the rest of Buoy's handles read:
|
|
22
|
+
* `buoy-routes-route-blog-year-slug`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Any path-ish string → a testID-safe slug (`/` → `-`, brackets dropped). */
|
|
26
|
+
function pathSlug(path) {
|
|
27
|
+
return String(path).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "root";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `buoy-routes-event-row-<slug>-<visit>` for one row of the events timeline.
|
|
32
|
+
*
|
|
33
|
+
* The visit number is part of the handle because revisiting a screen is the
|
|
34
|
+
* normal case here: three trips to `/about` would otherwise be three views
|
|
35
|
+
* sharing one testID, which Detox reports as "multiple elements found" — an
|
|
36
|
+
* ambiguity error where the test meant "the second visit". It also makes the
|
|
37
|
+
* tool's own visit counting addressable rather than merely visible.
|
|
38
|
+
*/
|
|
39
|
+
function eventRowTestID(pathname, visitNumber) {
|
|
40
|
+
return `buoy-routes-event-row-${pathSlug(pathname)}-${visitNumber}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `buoy-routes-route-<slug>` for one row of the sitemap. */
|
|
44
|
+
function sitemapRouteTestID(path) {
|
|
45
|
+
return `buoy-routes-route-${pathSlug(path)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `buoy-routes-group-<slug>` for a sitemap group header. */
|
|
49
|
+
function sitemapGroupTestID(title) {
|
|
50
|
+
return `buoy-routes-group-${pathSlug(title)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `buoy-routes-stack-item-<index>` for one frame of the navigation stack.
|
|
55
|
+
*
|
|
56
|
+
* The exception to addressing by path: a stack legitimately holds the same
|
|
57
|
+
* route twice (push `/route-bench/1` from `/route-bench/1`), and its whole
|
|
58
|
+
* subject is POSITION — "index 2 of 4" is what the row means, so the position
|
|
59
|
+
* is what the handle should say.
|
|
60
|
+
*/
|
|
61
|
+
function stackItemTestID(index) {
|
|
62
|
+
return `buoy-routes-stack-item-${index}`;
|
|
63
|
+
}
|
|
@@ -13,6 +13,7 @@ import { ChevronDown, ChevronRight, InlineCopyButton, ProUpgradeModal, buoyColor
|
|
|
13
13
|
import { useIsPro } from "@buoy-gg/license";
|
|
14
14
|
import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
|
|
15
15
|
import { useNavigationStack } from "../useNavigationStack";
|
|
16
|
+
import { stackItemTestID } from "../utils/testIDs";
|
|
16
17
|
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// Types
|
|
@@ -48,6 +49,9 @@ export function NavigationStack({
|
|
|
48
49
|
const navigateToIndex = index => onAction ? onAction("navigateToIndex", {
|
|
49
50
|
index
|
|
50
51
|
}) : hookResult.navigateToIndex(index);
|
|
52
|
+
const popToIndex = index => onAction ? onAction("popToIndex", {
|
|
53
|
+
index
|
|
54
|
+
}) : hookResult.popToIndex(index);
|
|
51
55
|
const goBack = () => onAction ? onAction("goBack") : hookResult.goBack();
|
|
52
56
|
const popToTop = () => onAction ? onAction("popToTop") : hookResult.popToTop();
|
|
53
57
|
const [expandedIndex, setExpandedIndex] = useState(null);
|
|
@@ -102,6 +106,7 @@ export function NavigationStack({
|
|
|
102
106
|
style: styles.loadingContainer,
|
|
103
107
|
children: /*#__PURE__*/_jsx(Text, {
|
|
104
108
|
style: styles.loadingText,
|
|
109
|
+
testID: "buoy-routes-stack-loading",
|
|
105
110
|
children: "Loading navigation stack..."
|
|
106
111
|
})
|
|
107
112
|
})
|
|
@@ -114,11 +119,13 @@ export function NavigationStack({
|
|
|
114
119
|
style: [styles.container, style],
|
|
115
120
|
children: /*#__PURE__*/_jsxs(View, {
|
|
116
121
|
style: styles.errorContainer,
|
|
122
|
+
testID: "buoy-routes-stack-error",
|
|
117
123
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
118
124
|
style: styles.errorText,
|
|
119
125
|
children: "Error loading stack"
|
|
120
126
|
}), /*#__PURE__*/_jsx(Text, {
|
|
121
127
|
style: styles.errorDetail,
|
|
128
|
+
testID: "buoy-routes-stack-error-detail",
|
|
122
129
|
children: error.message
|
|
123
130
|
})]
|
|
124
131
|
})
|
|
@@ -131,8 +138,10 @@ export function NavigationStack({
|
|
|
131
138
|
style: [styles.container, style],
|
|
132
139
|
children: /*#__PURE__*/_jsxs(View, {
|
|
133
140
|
style: styles.emptyContainer,
|
|
141
|
+
testID: "buoy-routes-stack-empty",
|
|
134
142
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
135
143
|
style: styles.emptyText,
|
|
144
|
+
testID: "buoy-routes-stack-empty-text",
|
|
136
145
|
children: "No navigation stack"
|
|
137
146
|
}), /*#__PURE__*/_jsx(Text, {
|
|
138
147
|
style: styles.emptySubtext,
|
|
@@ -229,12 +238,19 @@ export function NavigationStack({
|
|
|
229
238
|
const screensToRemove = stackDepth - 1 - selectedRoute.index;
|
|
230
239
|
const targetIndex = selectedRoute.index;
|
|
231
240
|
confirmDestructive("Pop to Route", `Remove ${screensToRemove} screen${screensToRemove !== 1 ? "s" : ""} above ${selectedRoute.pathname}?`, "Pop",
|
|
232
|
-
//
|
|
233
|
-
|
|
241
|
+
// POP, not navigate. `navigate` only returns to a mounted screen when
|
|
242
|
+
// the name AND the params match, so on any dynamic route — the common
|
|
243
|
+
// case for a stack you would want to unwind — it pushed a fresh copy
|
|
244
|
+
// instead: the dialog promised to remove two screens and the stack came
|
|
245
|
+
// back one deeper, with the route in it twice. `popToIndex` is the
|
|
246
|
+
// action the hook has always exported for this, and the same one the
|
|
247
|
+
// desktop host already dispatches over the socket.
|
|
248
|
+
() => popToIndex(targetIndex));
|
|
234
249
|
};
|
|
235
250
|
return /*#__PURE__*/_jsxs(View, {
|
|
236
251
|
style: [styles.container, style],
|
|
237
252
|
children: [/*#__PURE__*/_jsx(ScrollView, {
|
|
253
|
+
testID: "buoy-routes-stack-scroll",
|
|
238
254
|
style: styles.stackScroll,
|
|
239
255
|
contentContainerStyle: [styles.stackContent, {
|
|
240
256
|
paddingBottom: styles.stackContent.padding + insets.bottom
|
|
@@ -243,14 +259,19 @@ export function NavigationStack({
|
|
|
243
259
|
const actualIndex = stack.length - 1 - reverseIndex;
|
|
244
260
|
const isExpanded = expandedIndex === actualIndex;
|
|
245
261
|
const hasParams = Object.keys(item.params).length > 0;
|
|
262
|
+
const itemTestID = stackItemTestID(actualIndex);
|
|
246
263
|
return /*#__PURE__*/_jsx(View, {
|
|
247
264
|
style: styles.stackItemWrapper,
|
|
265
|
+
testID: itemTestID,
|
|
248
266
|
children: /*#__PURE__*/_jsxs(View, {
|
|
249
267
|
style: [styles.stackItem, isExpanded && styles.stackItemSelected],
|
|
250
268
|
children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
251
269
|
style: styles.stackItemHeader,
|
|
252
270
|
onPress: () => toggleExpand(actualIndex),
|
|
253
271
|
activeOpacity: 0.7,
|
|
272
|
+
testID: `${itemTestID}-toggle`,
|
|
273
|
+
accessibilityRole: "button",
|
|
274
|
+
accessibilityLabel: `${item.pathname}, ${item.isFocused ? "focused" : "mounted"}, position ${actualIndex} of ${stackDepth - 1}`,
|
|
254
275
|
children: [/*#__PURE__*/_jsx(View, {
|
|
255
276
|
style: styles.expandIndicator,
|
|
256
277
|
children: isExpanded ? /*#__PURE__*/_jsx(ChevronDown, {
|
|
@@ -263,10 +284,12 @@ export function NavigationStack({
|
|
|
263
284
|
}), /*#__PURE__*/_jsx(Text, {
|
|
264
285
|
style: styles.stackItemPath,
|
|
265
286
|
numberOfLines: 1,
|
|
287
|
+
testID: `${itemTestID}-path`,
|
|
266
288
|
children: item.pathname
|
|
267
289
|
}), /*#__PURE__*/_jsx(InlineCopyButton, {
|
|
268
290
|
value: item.pathname,
|
|
269
|
-
buttonStyle: styles.copyRouteButton
|
|
291
|
+
buttonStyle: styles.copyRouteButton,
|
|
292
|
+
testID: `${itemTestID}-copy`
|
|
270
293
|
}), /*#__PURE__*/_jsx(View, {
|
|
271
294
|
style: [styles.statusBadge, {
|
|
272
295
|
backgroundColor: item.isFocused ? `${buoyColors.success}15` : "#3B82F615",
|
|
@@ -276,6 +299,7 @@ export function NavigationStack({
|
|
|
276
299
|
style: [styles.statusBadgeText, {
|
|
277
300
|
color: item.isFocused ? buoyColors.success : "#3B82F6"
|
|
278
301
|
}],
|
|
302
|
+
testID: `${itemTestID}-status`,
|
|
279
303
|
children: item.isFocused ? "FOCUSED" : "MOUNTED"
|
|
280
304
|
})
|
|
281
305
|
})]
|
|
@@ -288,6 +312,7 @@ export function NavigationStack({
|
|
|
288
312
|
children: "Route Name:"
|
|
289
313
|
}), /*#__PURE__*/_jsx(Text, {
|
|
290
314
|
style: styles.detailValue,
|
|
315
|
+
testID: `${itemTestID}-name`,
|
|
291
316
|
children: item.name
|
|
292
317
|
})]
|
|
293
318
|
}), /*#__PURE__*/_jsxs(View, {
|
|
@@ -297,6 +322,7 @@ export function NavigationStack({
|
|
|
297
322
|
children: "Position:"
|
|
298
323
|
}), /*#__PURE__*/_jsxs(Text, {
|
|
299
324
|
style: styles.detailValue,
|
|
325
|
+
testID: `${itemTestID}-position`,
|
|
300
326
|
children: [actualIndex, " / ", stackDepth - 1]
|
|
301
327
|
})]
|
|
302
328
|
}), hasParams && /*#__PURE__*/_jsx(View, {
|
|
@@ -314,6 +340,9 @@ export function NavigationStack({
|
|
|
314
340
|
style: [styles.actionButton, isAtRoot && styles.actionButtonDisabled],
|
|
315
341
|
onPress: handleGoBack,
|
|
316
342
|
disabled: isAtRoot,
|
|
343
|
+
testID: `${itemTestID}-back`,
|
|
344
|
+
accessibilityRole: "button",
|
|
345
|
+
accessibilityLabel: "Go back one screen",
|
|
317
346
|
children: /*#__PURE__*/_jsx(Text, {
|
|
318
347
|
style: [styles.actionButtonText, isAtRoot && styles.actionButtonTextDisabled],
|
|
319
348
|
children: "Back"
|
|
@@ -328,6 +357,9 @@ export function NavigationStack({
|
|
|
328
357
|
style: [styles.actionButton, item.isFocused && styles.actionButtonDisabled],
|
|
329
358
|
onPress: handleGo,
|
|
330
359
|
disabled: item.isFocused,
|
|
360
|
+
testID: `${itemTestID}-go`,
|
|
361
|
+
accessibilityRole: "button",
|
|
362
|
+
accessibilityLabel: `Navigate to ${item.pathname}`,
|
|
331
363
|
children: /*#__PURE__*/_jsx(Text, {
|
|
332
364
|
style: [styles.actionButtonText, item.isFocused && styles.actionButtonTextDisabled],
|
|
333
365
|
children: "Go"
|
|
@@ -342,6 +374,9 @@ export function NavigationStack({
|
|
|
342
374
|
style: [styles.actionButton, (item.isFocused || actualIndex === stackDepth - 1) && styles.actionButtonDisabled],
|
|
343
375
|
onPress: handlePopTo,
|
|
344
376
|
disabled: item.isFocused || actualIndex === stackDepth - 1,
|
|
377
|
+
testID: `${itemTestID}-pop-to`,
|
|
378
|
+
accessibilityRole: "button",
|
|
379
|
+
accessibilityLabel: `Remove screens above ${item.pathname}`,
|
|
345
380
|
children: /*#__PURE__*/_jsx(Text, {
|
|
346
381
|
style: [styles.actionButtonText, (item.isFocused || actualIndex === stackDepth - 1) && styles.actionButtonTextDisabled],
|
|
347
382
|
children: "Pop To"
|
|
@@ -23,6 +23,7 @@ export function RouteEventDetail({
|
|
|
23
23
|
return /*#__PURE__*/_jsxs(View, {
|
|
24
24
|
style: styles.container,
|
|
25
25
|
children: [/*#__PURE__*/_jsx(ScrollView, {
|
|
26
|
+
testID: "buoy-routes-detail-scroll",
|
|
26
27
|
style: styles.scroll,
|
|
27
28
|
contentContainerStyle: styles.scrollContent,
|
|
28
29
|
showsVerticalScrollIndicator: true,
|
|
@@ -37,6 +38,9 @@ export function RouteEventDetail({
|
|
|
37
38
|
style: styles.goButton,
|
|
38
39
|
onPress: () => onNavigate(event.pathname),
|
|
39
40
|
activeOpacity: 0.8,
|
|
41
|
+
testID: "buoy-routes-detail-go",
|
|
42
|
+
accessibilityRole: "button",
|
|
43
|
+
accessibilityLabel: `Go to route ${event.pathname}`,
|
|
40
44
|
children: [/*#__PURE__*/_jsx(Navigation, {
|
|
41
45
|
size: 16,
|
|
42
46
|
color: night.color.onAccent
|
|
@@ -37,11 +37,13 @@ export function RouteEventExpandedContent({
|
|
|
37
37
|
style: styles.section,
|
|
38
38
|
children: [routeTemplate && /*#__PURE__*/_jsxs(View, {
|
|
39
39
|
style: styles.detailRow,
|
|
40
|
+
testID: "buoy-routes-detail-template",
|
|
40
41
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
41
42
|
style: styles.detailLabel,
|
|
42
43
|
children: "Template:"
|
|
43
44
|
}), /*#__PURE__*/_jsx(Text, {
|
|
44
45
|
style: styles.detailValue,
|
|
46
|
+
testID: "buoy-routes-detail-template-value",
|
|
45
47
|
children: routeTemplate
|
|
46
48
|
}), /*#__PURE__*/_jsx(InlineCopyButton, {
|
|
47
49
|
value: routeTemplate,
|
|
@@ -49,11 +51,13 @@ export function RouteEventExpandedContent({
|
|
|
49
51
|
})]
|
|
50
52
|
}), event.previousPathname && /*#__PURE__*/_jsxs(View, {
|
|
51
53
|
style: styles.detailRow,
|
|
54
|
+
testID: "buoy-routes-detail-from",
|
|
52
55
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
53
56
|
style: styles.detailLabel,
|
|
54
57
|
children: "From:"
|
|
55
58
|
}), /*#__PURE__*/_jsx(Text, {
|
|
56
59
|
style: styles.detailValue,
|
|
60
|
+
testID: "buoy-routes-detail-from-value",
|
|
57
61
|
children: event.previousPathname
|
|
58
62
|
}), /*#__PURE__*/_jsx(InlineCopyButton, {
|
|
59
63
|
value: event.previousPathname,
|
|
@@ -61,11 +65,13 @@ export function RouteEventExpandedContent({
|
|
|
61
65
|
})]
|
|
62
66
|
}), /*#__PURE__*/_jsxs(View, {
|
|
63
67
|
style: styles.detailRow,
|
|
68
|
+
testID: "buoy-routes-detail-to",
|
|
64
69
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
65
70
|
style: styles.detailLabel,
|
|
66
71
|
children: "To:"
|
|
67
72
|
}), /*#__PURE__*/_jsx(Text, {
|
|
68
73
|
style: styles.detailValue,
|
|
74
|
+
testID: "buoy-routes-detail-to-value",
|
|
69
75
|
children: event.pathname
|
|
70
76
|
}), /*#__PURE__*/_jsx(InlineCopyButton, {
|
|
71
77
|
value: event.pathname,
|
|
@@ -76,20 +82,24 @@ export function RouteEventExpandedContent({
|
|
|
76
82
|
style: styles.section,
|
|
77
83
|
children: [event.timeSincePrevious !== undefined && /*#__PURE__*/_jsxs(View, {
|
|
78
84
|
style: styles.detailRow,
|
|
85
|
+
testID: "buoy-routes-detail-duration",
|
|
79
86
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
80
87
|
style: styles.detailLabel,
|
|
81
88
|
children: "Duration:"
|
|
82
89
|
}), /*#__PURE__*/_jsx(Text, {
|
|
83
90
|
style: styles.detailValue,
|
|
91
|
+
testID: "buoy-routes-detail-duration-value",
|
|
84
92
|
children: formatDuration(event.timeSincePrevious)
|
|
85
93
|
})]
|
|
86
94
|
}), /*#__PURE__*/_jsxs(View, {
|
|
87
95
|
style: styles.detailRow,
|
|
96
|
+
testID: "buoy-routes-detail-time",
|
|
88
97
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
89
98
|
style: styles.detailLabel,
|
|
90
99
|
children: "Time:"
|
|
91
100
|
}), /*#__PURE__*/_jsx(Text, {
|
|
92
101
|
style: styles.detailValue,
|
|
102
|
+
testID: "buoy-routes-detail-time-value",
|
|
93
103
|
children: new Date(event.timestamp).toLocaleString()
|
|
94
104
|
})]
|
|
95
105
|
})]
|
|
@@ -97,11 +107,13 @@ export function RouteEventExpandedContent({
|
|
|
97
107
|
style: styles.section,
|
|
98
108
|
children: [event.segments && event.segments.length > 0 && /*#__PURE__*/_jsxs(View, {
|
|
99
109
|
style: styles.detailRow,
|
|
110
|
+
testID: "buoy-routes-detail-segments",
|
|
100
111
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
101
112
|
style: styles.detailLabel,
|
|
102
113
|
children: "Segments:"
|
|
103
114
|
}), /*#__PURE__*/_jsx(Text, {
|
|
104
115
|
style: styles.detailValue,
|
|
116
|
+
testID: "buoy-routes-detail-segments-value",
|
|
105
117
|
children: event.segments.join(' → ')
|
|
106
118
|
}), /*#__PURE__*/_jsx(InlineCopyButton, {
|
|
107
119
|
value: JSON.stringify(event.segments),
|
|
@@ -113,6 +125,7 @@ export function RouteEventExpandedContent({
|
|
|
113
125
|
style: styles.dataViewerHeader,
|
|
114
126
|
children: [/*#__PURE__*/_jsxs(Text, {
|
|
115
127
|
style: styles.dataViewerTitle,
|
|
128
|
+
testID: "buoy-routes-detail-params-title",
|
|
116
129
|
children: ["Parameters (", Object.keys(event.params).length, ")"]
|
|
117
130
|
}), /*#__PURE__*/_jsx(InlineCopyButton, {
|
|
118
131
|
value: JSON.stringify(event.params, null, 2),
|
|
@@ -127,11 +140,13 @@ export function RouteEventExpandedContent({
|
|
|
127
140
|
})]
|
|
128
141
|
}), visitNumber > 1 && /*#__PURE__*/_jsxs(View, {
|
|
129
142
|
style: styles.detailRow,
|
|
143
|
+
testID: "buoy-routes-detail-visited",
|
|
130
144
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
131
145
|
style: styles.detailLabel,
|
|
132
146
|
children: "Visited:"
|
|
133
147
|
}), /*#__PURE__*/_jsxs(Text, {
|
|
134
148
|
style: styles.detailValue,
|
|
149
|
+
testID: "buoy-routes-detail-visited-value",
|
|
135
150
|
children: [visitNumber, " time", visitNumber !== 1 ? 's' : '']
|
|
136
151
|
})]
|
|
137
152
|
})]
|