@buoy-gg/network 7.0.35 → 7.0.38
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/README.md +45 -12
- package/lib/commonjs/index.js +26 -1
- package/lib/commonjs/index.web.js +26 -1
- package/lib/commonjs/network/components/NetworkEventDetailView.js +6 -1
- package/lib/commonjs/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/commonjs/network/components/NetworkModal.js +24 -8
- package/lib/commonjs/network/conditions/NetworkConditionsControl.js +235 -0
- package/lib/commonjs/network/conditions/NetworkConditionsControl.test.js +136 -0
- package/lib/commonjs/network/conditions/NetworkConditionsIcon.js +33 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.js +310 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.test.js +219 -0
- package/lib/commonjs/network/conditions/store.js +75 -0
- package/lib/commonjs/network/conditions/wait.js +43 -0
- package/lib/commonjs/network/conditions/xhr.js +110 -0
- package/lib/commonjs/network/hooks/useNetworkEvents.js +1 -1
- package/lib/commonjs/network/overrides/applyToXhr.js +10 -8
- package/lib/commonjs/network/overrides/engine.js +2 -1
- package/lib/commonjs/network/sync/networkSyncAdapter.js +224 -3
- package/lib/commonjs/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/commonjs/network/utils/networkEventStore.js +5 -77
- package/lib/commonjs/network/utils/networkListener.js +245 -46
- package/lib/commonjs/network/utils/networkSavedStore.js +2 -2
- package/lib/commonjs/network/utils/selectSavedEvents.js +2 -2
- package/lib/commonjs/preset.js +2 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/network/components/NetworkEventDetailView.js +6 -1
- package/lib/module/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/module/network/components/NetworkModal.js +25 -9
- package/lib/module/network/conditions/NetworkConditionsControl.js +231 -0
- package/lib/module/network/conditions/NetworkConditionsControl.test.js +135 -0
- package/lib/module/network/conditions/NetworkConditionsIcon.js +29 -0
- package/lib/module/network/conditions/NetworkFloatingTool.js +304 -0
- package/lib/module/network/conditions/NetworkFloatingTool.test.js +218 -0
- package/lib/module/network/conditions/store.js +65 -0
- package/lib/module/network/conditions/wait.js +38 -0
- package/lib/module/network/conditions/xhr.js +106 -0
- package/lib/module/network/hooks/useNetworkEvents.js +1 -1
- package/lib/module/network/overrides/applyToXhr.js +9 -8
- package/lib/module/network/overrides/engine.js +2 -1
- package/lib/module/network/sync/networkSyncAdapter.js +225 -3
- package/lib/module/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/module/network/utils/networkEventStore.js +6 -77
- package/lib/module/network/utils/networkListener.js +246 -47
- package/lib/module/network/utils/networkSavedStore.js +2 -2
- package/lib/module/network/utils/selectSavedEvents.js +2 -2
- package/lib/module/preset.js +3 -2
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +2 -0
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +2 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts +3 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts +12 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts +4 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts +15 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/store.d.ts +30 -0
- package/lib/typescript/network/conditions/store.d.ts.map +1 -0
- package/lib/typescript/network/conditions/wait.d.ts +4 -0
- package/lib/typescript/network/conditions/wait.d.ts.map +1 -0
- package/lib/typescript/network/conditions/xhr.d.ts +5 -0
- package/lib/typescript/network/conditions/xhr.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts +1 -1
- package/lib/typescript/network/overrides/applyToXhr.d.ts +2 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/engine.d.ts +0 -10
- package/lib/typescript/network/overrides/engine.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +29 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +1 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkEventStore.d.ts +0 -15
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +13 -28
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +2 -2
- package/lib/typescript/network/utils/selectSavedEvents.d.ts +2 -2
- package/lib/typescript/preset.d.ts +0 -16
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +2 -2
package/lib/commonjs/preset.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.networkToolPreset = exports.createNetworkTool = void 0;
|
|
7
|
+
var _NetworkConditionsIcon = require("./network/conditions/NetworkConditionsIcon");
|
|
7
8
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
8
9
|
var _NetworkModal = require("./network/components/NetworkModal");
|
|
9
10
|
var _overrideRulesStore = require("./network/overrides/overrideRulesStore");
|
|
@@ -72,7 +73,7 @@ const networkToolPreset = exports.networkToolPreset = (0, _sharedUi.defineToolPr
|
|
|
72
73
|
slot: "both",
|
|
73
74
|
icon: ({
|
|
74
75
|
size
|
|
75
|
-
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
76
|
+
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkConditionsIcon.NetworkConditionsIcon, {
|
|
76
77
|
size: size
|
|
77
78
|
}),
|
|
78
79
|
component: _NetworkModal.NetworkModal,
|
package/lib/module/index.js
CHANGED
|
@@ -99,4 +99,5 @@ export { networkEventStore } from "./network/utils/networkEventStore";
|
|
|
99
99
|
export { networkSavedStore, flagsForEventId, FREE_TIER_SAVED_LIMIT, MAX_PINNED, MAX_SAVED } from "./network/utils/networkSavedStore";
|
|
100
100
|
export { generateCurl } from "./network/utils/generateCurl";
|
|
101
101
|
/** @internal */
|
|
102
|
-
export { networkListener, addNetworkListener, removeAllNetworkListeners } from "./network/utils/networkListener";
|
|
102
|
+
export { networkListener, addNetworkListener, removeAllNetworkListeners } from "./network/utils/networkListener";
|
|
103
|
+
export { NETWORK_CONDITIONS, getNetworkCondition, setNetworkCondition, subscribeNetworkConditions } from "./network/conditions/store";
|
package/lib/module/index.web.js
CHANGED
|
@@ -78,4 +78,5 @@ export { ruleMatches } from "./network/overrides/matchRule";
|
|
|
78
78
|
export { filterNetworkEvents, applyIgnoredPatterns } from "./network/utils/filterNetworkEvents";
|
|
79
79
|
export { splitPinned } from "./network/utils/withPinnedFirst";
|
|
80
80
|
export { extractOperationName } from "./network/utils/extractOperationName";
|
|
81
|
-
export { generateCurl } from "./network/utils/generateCurl";
|
|
81
|
+
export { generateCurl } from "./network/utils/generateCurl";
|
|
82
|
+
export { NETWORK_CONDITIONS, getNetworkCondition, setNetworkCondition, subscribeNetworkConditions } from "./network/conditions/store";
|
|
@@ -446,7 +446,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
446
446
|
style: styles.pendingBadgeText,
|
|
447
447
|
children: "Pending"
|
|
448
448
|
})]
|
|
449
|
-
}) : null, event.
|
|
449
|
+
}) : null, event.condition && /*#__PURE__*/_jsxs(Text, {
|
|
450
|
+
style: {
|
|
451
|
+
color: "#FFCC66"
|
|
452
|
+
},
|
|
453
|
+
children: ["Conditions: ", event.condition.profile, " (+", event.condition.delayMs, " ms)"]
|
|
454
|
+
}), event.duration ? /*#__PURE__*/_jsxs(View, {
|
|
450
455
|
style: styles.httpDuration,
|
|
451
456
|
children: [/*#__PURE__*/_jsx(Clock, {
|
|
452
457
|
size: 10,
|
|
@@ -76,11 +76,15 @@ export function NetworkHeaderMenuItem({
|
|
|
76
76
|
icon,
|
|
77
77
|
label,
|
|
78
78
|
detail,
|
|
79
|
+
testID,
|
|
79
80
|
onPress
|
|
80
81
|
}) {
|
|
81
82
|
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
82
83
|
onPress: onPress,
|
|
83
84
|
"sentry-label": "ignore header menu item",
|
|
85
|
+
testID: testID,
|
|
86
|
+
accessibilityRole: "button",
|
|
87
|
+
accessibilityLabel: label,
|
|
84
88
|
style: styles.item,
|
|
85
89
|
children: [/*#__PURE__*/_jsx(View, {
|
|
86
90
|
style: styles.itemIcon,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { createRemoteConditionsSource, openNetworkFloatingTool } from "../conditions/NetworkFloatingTool";
|
|
3
4
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
5
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
5
|
-
import { Search, Filter, Bookmark, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, night, persistentStorage, NightSegmented, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns, windowDragProps } from "@buoy-gg/shared-ui";
|
|
6
|
+
import { Search, Wifi, Filter, Bookmark, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, night, persistentStorage, NightSegmented, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns, windowDragProps } from "@buoy-gg/shared-ui";
|
|
6
7
|
import { NetworkFilterViewV3 } from "./NetworkFilterViewV3";
|
|
7
8
|
import { TickProvider } from "../hooks/useTickEveryMinute";
|
|
8
9
|
import { NetworkEventDetailView } from "./NetworkEventDetailView";
|
|
@@ -178,7 +179,8 @@ function NetworkModalInner({
|
|
|
178
179
|
enableSharedModalDimensions = false,
|
|
179
180
|
inline = false,
|
|
180
181
|
paused: pausedProp,
|
|
181
|
-
onPausedChange
|
|
182
|
+
onPausedChange,
|
|
183
|
+
remoteConditions
|
|
182
184
|
}) {
|
|
183
185
|
const {
|
|
184
186
|
isPro
|
|
@@ -208,6 +210,12 @@ function NetworkModalInner({
|
|
|
208
210
|
const [showSavedView, setShowSavedView] = useState(false);
|
|
209
211
|
const [showOverridesView, setShowOverridesView] = useState(false);
|
|
210
212
|
const [showHeaderMenu, setShowHeaderMenu] = useState(false);
|
|
213
|
+
const [minimizeRequest, setMinimizeRequest] = useState(0);
|
|
214
|
+
const [remoteConditionsSource] = useState(createRemoteConditionsSource);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
remoteConditionsSource.update(remoteConditions);
|
|
217
|
+
}, [remoteConditionsSource, remoteConditions]);
|
|
218
|
+
useEffect(() => () => remoteConditionsSource.update(undefined), [remoteConditionsSource]);
|
|
211
219
|
/**
|
|
212
220
|
* Overrides are a DEVICE capability — a rule runs inside the app's own
|
|
213
221
|
* `fetch`/`XMLHttpRequest`, which the dashboard doesn't have.
|
|
@@ -488,7 +496,7 @@ function NetworkModalInner({
|
|
|
488
496
|
}) : null, /*#__PURE__*/_jsx(PowerToggleButton, {
|
|
489
497
|
isEnabled: !paused,
|
|
490
498
|
onToggle: () => setPaused(prev => !prev),
|
|
491
|
-
accessibilityLabel: "
|
|
499
|
+
accessibilityLabel: "Pause network list",
|
|
492
500
|
testID: "buoy-network-power"
|
|
493
501
|
}), /*#__PURE__*/_jsx(NetworkClearButton, {
|
|
494
502
|
paused: paused
|
|
@@ -586,6 +594,18 @@ function NetworkModalInner({
|
|
|
586
594
|
}), showHeaderMenu ? /*#__PURE__*/_jsxs(NetworkHeaderMenu, {
|
|
587
595
|
onClose: () => setShowHeaderMenu(false),
|
|
588
596
|
children: [/*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
|
|
597
|
+
icon: /*#__PURE__*/_jsx(Wifi, {
|
|
598
|
+
size: 14,
|
|
599
|
+
color: night.color.textSecondary
|
|
600
|
+
}),
|
|
601
|
+
label: "Network throttling",
|
|
602
|
+
testID: "buoy-network-menu-throttling",
|
|
603
|
+
onPress: () => {
|
|
604
|
+
setShowHeaderMenu(false);
|
|
605
|
+
openNetworkFloatingTool(remoteConditions ? remoteConditionsSource : undefined);
|
|
606
|
+
if (!inline) setMinimizeRequest(value => value + 1);
|
|
607
|
+
}
|
|
608
|
+
}), /*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
|
|
589
609
|
icon: /*#__PURE__*/_jsx(Filter, {
|
|
590
610
|
size: 14,
|
|
591
611
|
color: hasActiveFilter ? night.color.accent : night.color.textSecondary
|
|
@@ -624,12 +644,7 @@ function NetworkModalInner({
|
|
|
624
644
|
})]
|
|
625
645
|
}) : null, /*#__PURE__*/_jsx(UpgradeModal, {
|
|
626
646
|
visible: showUpgradeModal,
|
|
627
|
-
onClose: () => setShowUpgradeModal(false)
|
|
628
|
-
// "saved" is a count limit a free key can raise; "overrides" is one it
|
|
629
|
-
// can't (1 on both tiers) and "export" is Pro-only — freeKeyRaises
|
|
630
|
-
// sorts those out, so the mapping here can stay literal.
|
|
631
|
-
,
|
|
632
|
-
limitKey: upgradeReason === "saved" ? "network.saved" : upgradeReason === "overrides" ? "network.overrideRules" : undefined,
|
|
647
|
+
onClose: () => setShowUpgradeModal(false),
|
|
633
648
|
featureName: upgradeReason === "saved" ? "Saved Requests" : upgradeReason === "overrides" ? "Response Overrides" : "Network Export",
|
|
634
649
|
description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : upgradeReason === "overrides" ? "Force any endpoint to fail, stall or return something else — no backend changes, no code changes. Free keeps one active rule." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
|
|
635
650
|
benefits: upgradeReason === "overrides" ? [`Run ${MAX_OVERRIDE_RULES} rules at once (free keeps ${FREE_TIER_OVERRIDE_LIMIT})`, "Force a 500, a 401 or an empty list in one tap", "Simulate offline, timeouts and slow connections", "Rules survive an app reload, so you can test the boot path"] : upgradeReason === "saved" ? [`Save up to ${MAX_SAVED} requests (free keeps ${FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
|
|
@@ -652,6 +667,7 @@ function NetworkModalInner({
|
|
|
652
667
|
visible: visible,
|
|
653
668
|
onClose: onClose,
|
|
654
669
|
onMinimize: onMinimize,
|
|
670
|
+
minimizeRequest: minimizeRequest,
|
|
655
671
|
persistenceKey: persistenceKey,
|
|
656
672
|
variant: "night",
|
|
657
673
|
header: {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
5
|
+
import { ChevronDown, Check, night } from "@buoy-gg/shared-ui";
|
|
6
|
+
import { conditionsCanApply, getNetworkCondition, setNetworkCondition, subscribeNetworkConditions } from "./store";
|
|
7
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
const options = [{
|
|
9
|
+
profile: "normal",
|
|
10
|
+
label: "No throttling"
|
|
11
|
+
}, {
|
|
12
|
+
profile: "slow",
|
|
13
|
+
label: "Slow"
|
|
14
|
+
}, {
|
|
15
|
+
profile: "verySlow",
|
|
16
|
+
label: "Very slow"
|
|
17
|
+
}, {
|
|
18
|
+
profile: "offline",
|
|
19
|
+
label: "Offline"
|
|
20
|
+
}];
|
|
21
|
+
const summaries = {
|
|
22
|
+
normal: "No added delay",
|
|
23
|
+
slow: "+500 ms before each new request",
|
|
24
|
+
verySlow: "+2 seconds before each new request",
|
|
25
|
+
offline: "New requests fail before sending"
|
|
26
|
+
};
|
|
27
|
+
export function NetworkConditionsControl({
|
|
28
|
+
remote
|
|
29
|
+
}) {
|
|
30
|
+
const local = useSyncExternalStore(subscribeNetworkConditions, getNetworkCondition, getNetworkCondition);
|
|
31
|
+
const localAvailable = useSyncExternalStore(subscribeNetworkConditions, conditionsCanApply, conditionsCanApply);
|
|
32
|
+
const [expanded, setExpanded] = useState(false);
|
|
33
|
+
const [error, setError] = useState();
|
|
34
|
+
const [pending, setPending] = useState(false);
|
|
35
|
+
const busy = useRef(false);
|
|
36
|
+
const mounted = useRef(true);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
mounted.current = true;
|
|
39
|
+
return () => {
|
|
40
|
+
mounted.current = false;
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
const profile = remote ? remote.profile : local;
|
|
44
|
+
const available = remote ? remote.available : localAvailable;
|
|
45
|
+
const active = profile !== null && profile !== "normal";
|
|
46
|
+
const tint = profile === "offline" ? night.color.danger : active ? night.color.warning : night.color.accent;
|
|
47
|
+
async function select(value) {
|
|
48
|
+
if (!available || busy.current) return;
|
|
49
|
+
if (value === profile) {
|
|
50
|
+
setExpanded(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
busy.current = true;
|
|
54
|
+
setPending(true);
|
|
55
|
+
setError(undefined);
|
|
56
|
+
try {
|
|
57
|
+
if (remote) await remote.onChange(value);else setNetworkCondition(value);
|
|
58
|
+
if (mounted.current) setExpanded(false);
|
|
59
|
+
} catch (failure) {
|
|
60
|
+
if (mounted.current) setError(failure instanceof Error ? failure.message : String(failure));
|
|
61
|
+
} finally {
|
|
62
|
+
busy.current = false;
|
|
63
|
+
if (mounted.current) setPending(false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const label = options.find(option => option.profile === profile)?.label ?? "Unavailable";
|
|
67
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
68
|
+
style: styles.container,
|
|
69
|
+
testID: "buoy-network-throttling-screen",
|
|
70
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
71
|
+
style: styles.heading,
|
|
72
|
+
children: "Connection profile"
|
|
73
|
+
}), /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
74
|
+
testID: "buoy-network-conditions",
|
|
75
|
+
accessibilityRole: "button",
|
|
76
|
+
accessibilityLabel: `Network throttling: ${label}`,
|
|
77
|
+
accessibilityState: {
|
|
78
|
+
expanded,
|
|
79
|
+
disabled: !available || pending
|
|
80
|
+
},
|
|
81
|
+
disabled: !available || pending,
|
|
82
|
+
onPress: () => setExpanded(value => !value),
|
|
83
|
+
style: [styles.dropdown, expanded && styles.dropdownOpen, !available && styles.disabled],
|
|
84
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
85
|
+
style: [styles.value, active && {
|
|
86
|
+
color: tint
|
|
87
|
+
}],
|
|
88
|
+
children: label
|
|
89
|
+
}), pending ? /*#__PURE__*/_jsx(ActivityIndicator, {
|
|
90
|
+
size: "small",
|
|
91
|
+
color: night.color.textSecondary
|
|
92
|
+
}) : /*#__PURE__*/_jsx(ChevronDown, {
|
|
93
|
+
size: 16,
|
|
94
|
+
color: night.color.textSecondary
|
|
95
|
+
})]
|
|
96
|
+
}), expanded && /*#__PURE__*/_jsx(View, {
|
|
97
|
+
accessibilityRole: "radiogroup",
|
|
98
|
+
accessibilityLabel: "Connection profile",
|
|
99
|
+
style: styles.options,
|
|
100
|
+
children: options.map(option => {
|
|
101
|
+
const selected = profile === option.profile;
|
|
102
|
+
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
103
|
+
testID: `buoy-network-condition-${option.profile}`,
|
|
104
|
+
accessibilityRole: "radio",
|
|
105
|
+
accessibilityLabel: option.label,
|
|
106
|
+
accessibilityState: {
|
|
107
|
+
checked: selected,
|
|
108
|
+
disabled: !available || pending
|
|
109
|
+
},
|
|
110
|
+
disabled: !available || pending,
|
|
111
|
+
onPress: () => void select(option.profile),
|
|
112
|
+
style: [styles.option, selected && styles.selected],
|
|
113
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
114
|
+
style: styles.optionBody,
|
|
115
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
116
|
+
style: [styles.optionTitle, selected && {
|
|
117
|
+
color: tint
|
|
118
|
+
}],
|
|
119
|
+
children: option.label
|
|
120
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
121
|
+
style: styles.caption,
|
|
122
|
+
children: summaries[option.profile]
|
|
123
|
+
})]
|
|
124
|
+
}), selected && /*#__PURE__*/_jsx(Check, {
|
|
125
|
+
size: 16,
|
|
126
|
+
color: tint
|
|
127
|
+
})]
|
|
128
|
+
}, option.profile);
|
|
129
|
+
})
|
|
130
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
131
|
+
style: styles.caption,
|
|
132
|
+
accessibilityLiveRegion: "polite",
|
|
133
|
+
children: pending ? "Applying to device…" : !available ? remote ? "Conditions unavailable on this device" : "Requires a signed-in account and a development build" : profile ? summaries[profile] : "Waiting for device status"
|
|
134
|
+
}), !!error && /*#__PURE__*/_jsx(Text, {
|
|
135
|
+
accessibilityRole: "alert",
|
|
136
|
+
style: styles.error,
|
|
137
|
+
children: error
|
|
138
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
139
|
+
style: styles.help,
|
|
140
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
141
|
+
style: styles.heading,
|
|
142
|
+
children: "How it works"
|
|
143
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
144
|
+
style: styles.caption,
|
|
145
|
+
children: "Affects new HTTP requests through global fetch and XHR. Slow adds a wait before sending; download speed stays unchanged."
|
|
146
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
147
|
+
style: styles.caption,
|
|
148
|
+
children: "Stays active when you close this tool or pause capture. Choose No throttling to stop. Reloading the app resets it."
|
|
149
|
+
})]
|
|
150
|
+
})]
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const styles = StyleSheet.create({
|
|
154
|
+
container: {
|
|
155
|
+
padding: night.gutter,
|
|
156
|
+
gap: 10
|
|
157
|
+
},
|
|
158
|
+
heading: {
|
|
159
|
+
fontSize: night.font.caption,
|
|
160
|
+
fontWeight: "600",
|
|
161
|
+
color: night.color.textSecondary
|
|
162
|
+
},
|
|
163
|
+
dropdown: {
|
|
164
|
+
flexDirection: "row",
|
|
165
|
+
alignItems: "center",
|
|
166
|
+
justifyContent: "space-between",
|
|
167
|
+
gap: 12,
|
|
168
|
+
minHeight: 44,
|
|
169
|
+
paddingHorizontal: 12,
|
|
170
|
+
paddingVertical: 10,
|
|
171
|
+
borderRadius: night.radius.segment,
|
|
172
|
+
borderWidth: 1,
|
|
173
|
+
borderColor: night.color.separator,
|
|
174
|
+
backgroundColor: night.color.surfaceElevated
|
|
175
|
+
},
|
|
176
|
+
dropdownOpen: {
|
|
177
|
+
borderColor: night.color.accentBorderStrong
|
|
178
|
+
},
|
|
179
|
+
value: {
|
|
180
|
+
flex: 1,
|
|
181
|
+
fontSize: night.font.body,
|
|
182
|
+
color: night.color.text
|
|
183
|
+
},
|
|
184
|
+
options: {
|
|
185
|
+
padding: 4,
|
|
186
|
+
borderRadius: night.radius.row,
|
|
187
|
+
borderWidth: 1,
|
|
188
|
+
borderColor: night.color.border,
|
|
189
|
+
backgroundColor: night.color.surface
|
|
190
|
+
},
|
|
191
|
+
option: {
|
|
192
|
+
flexDirection: "row",
|
|
193
|
+
alignItems: "center",
|
|
194
|
+
gap: 12,
|
|
195
|
+
minHeight: 54,
|
|
196
|
+
padding: 10,
|
|
197
|
+
borderRadius: night.radius.segment
|
|
198
|
+
},
|
|
199
|
+
optionBody: {
|
|
200
|
+
flex: 1,
|
|
201
|
+
gap: 4
|
|
202
|
+
},
|
|
203
|
+
optionTitle: {
|
|
204
|
+
fontSize: night.font.caption,
|
|
205
|
+
fontWeight: "600",
|
|
206
|
+
color: night.color.text
|
|
207
|
+
},
|
|
208
|
+
selected: {
|
|
209
|
+
backgroundColor: night.color.surfaceElevated
|
|
210
|
+
},
|
|
211
|
+
disabled: {
|
|
212
|
+
opacity: 0.45
|
|
213
|
+
},
|
|
214
|
+
caption: {
|
|
215
|
+
fontSize: night.font.caption,
|
|
216
|
+
lineHeight: 19,
|
|
217
|
+
color: night.color.textSecondary
|
|
218
|
+
},
|
|
219
|
+
help: {
|
|
220
|
+
marginTop: 14,
|
|
221
|
+
paddingTop: 16,
|
|
222
|
+
gap: 10,
|
|
223
|
+
borderTopWidth: 1,
|
|
224
|
+
borderTopColor: night.color.separator
|
|
225
|
+
},
|
|
226
|
+
error: {
|
|
227
|
+
fontSize: night.font.caption,
|
|
228
|
+
lineHeight: 18,
|
|
229
|
+
color: night.color.danger
|
|
230
|
+
}
|
|
231
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// @vitest-environment jsdom
|
|
4
|
+
import { act, createElement } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
7
|
+
import { NetworkConditionsControl } from "./NetworkConditionsControl";
|
|
8
|
+
vi.mock("@buoy-gg/shared-ui", async () => ({
|
|
9
|
+
ChevronDown: () => null,
|
|
10
|
+
Check: () => null,
|
|
11
|
+
night: (await vi.importActual("../../../../shared/src/ui/night/nightTheme")).night
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("react-native", () => ({
|
|
14
|
+
StyleSheet: {
|
|
15
|
+
create: value => value
|
|
16
|
+
},
|
|
17
|
+
View: ({
|
|
18
|
+
children
|
|
19
|
+
}) => /*#__PURE__*/createElement("div", null, children),
|
|
20
|
+
Text: ({
|
|
21
|
+
children
|
|
22
|
+
}) => /*#__PURE__*/createElement("span", null, children),
|
|
23
|
+
ActivityIndicator: () => /*#__PURE__*/createElement("span", null, "pending"),
|
|
24
|
+
TouchableOpacity: ({
|
|
25
|
+
children,
|
|
26
|
+
onPress,
|
|
27
|
+
disabled,
|
|
28
|
+
accessibilityLabel,
|
|
29
|
+
accessibilityState,
|
|
30
|
+
testID
|
|
31
|
+
}) => /*#__PURE__*/createElement("button", {
|
|
32
|
+
onClick: onPress,
|
|
33
|
+
disabled,
|
|
34
|
+
"aria-label": accessibilityLabel,
|
|
35
|
+
"aria-checked": accessibilityState?.checked,
|
|
36
|
+
"data-testid": testID
|
|
37
|
+
}, children)
|
|
38
|
+
}));
|
|
39
|
+
vi.mock("./store", () => ({
|
|
40
|
+
NETWORK_CONDITIONS: {
|
|
41
|
+
normal: {
|
|
42
|
+
label: "Normal"
|
|
43
|
+
},
|
|
44
|
+
slow: {
|
|
45
|
+
label: "Slow"
|
|
46
|
+
},
|
|
47
|
+
verySlow: {
|
|
48
|
+
label: "Very slow"
|
|
49
|
+
},
|
|
50
|
+
offline: {
|
|
51
|
+
label: "Offline"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
getNetworkCondition: () => "normal",
|
|
55
|
+
conditionsCanApply: () => true,
|
|
56
|
+
subscribeNetworkConditions: () => () => {},
|
|
57
|
+
setNetworkCondition: vi.fn()
|
|
58
|
+
}));
|
|
59
|
+
let root;
|
|
60
|
+
let container;
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
63
|
+
container = document.createElement("div");
|
|
64
|
+
root = createRoot(container);
|
|
65
|
+
});
|
|
66
|
+
afterEach(() => act(() => root.unmount()));
|
|
67
|
+
const button = profile => container.querySelector(`[data-testid="buoy-network-condition-${profile}"]`);
|
|
68
|
+
function render(remote) {
|
|
69
|
+
act(() => root.render(/*#__PURE__*/createElement(NetworkConditionsControl, {
|
|
70
|
+
remote
|
|
71
|
+
})));
|
|
72
|
+
}
|
|
73
|
+
const dropdown = () => container.querySelector('[data-testid="buoy-network-conditions"]');
|
|
74
|
+
const open = () => act(() => dropdown().click());
|
|
75
|
+
it("defaults to a closed No throttling dropdown and closes when the current option is chosen", () => {
|
|
76
|
+
const onChange = vi.fn();
|
|
77
|
+
render({
|
|
78
|
+
profile: "normal",
|
|
79
|
+
available: true,
|
|
80
|
+
onChange
|
|
81
|
+
});
|
|
82
|
+
expect(dropdown().textContent).toBe("No throttling");
|
|
83
|
+
expect(button("normal")).toBeNull();
|
|
84
|
+
open();
|
|
85
|
+
expect(["normal", "slow", "verySlow", "offline"].every(value => !!button(value))).toBe(true);
|
|
86
|
+
act(() => button("normal").click());
|
|
87
|
+
expect(button("normal")).toBeNull();
|
|
88
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
it("blocks duplicate remote changes and waits for the device profile before selecting it", async () => {
|
|
91
|
+
let finish;
|
|
92
|
+
const onChange = vi.fn(() => new Promise(resolve => {
|
|
93
|
+
finish = resolve;
|
|
94
|
+
}));
|
|
95
|
+
render({
|
|
96
|
+
profile: "normal",
|
|
97
|
+
available: true,
|
|
98
|
+
onChange
|
|
99
|
+
});
|
|
100
|
+
open();
|
|
101
|
+
act(() => button("slow").click());
|
|
102
|
+
expect(button("offline").disabled).toBe(true);
|
|
103
|
+
expect(button("normal").getAttribute("aria-checked")).toBe("true");
|
|
104
|
+
act(() => button("offline").click());
|
|
105
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith("slow");
|
|
106
|
+
await act(async () => finish());
|
|
107
|
+
render({
|
|
108
|
+
profile: "slow",
|
|
109
|
+
available: true,
|
|
110
|
+
onChange
|
|
111
|
+
});
|
|
112
|
+
expect(dropdown().textContent).toBe("Slow");
|
|
113
|
+
expect(button("slow")).toBeNull();
|
|
114
|
+
open();
|
|
115
|
+
expect(button("slow").getAttribute("aria-checked")).toBe("true");
|
|
116
|
+
});
|
|
117
|
+
it("reports a remote error, permits retry, and disables unavailable devices", async () => {
|
|
118
|
+
const onChange = vi.fn().mockRejectedValue(new Error("Device disconnected"));
|
|
119
|
+
render({
|
|
120
|
+
profile: "normal",
|
|
121
|
+
available: true,
|
|
122
|
+
onChange
|
|
123
|
+
});
|
|
124
|
+
open();
|
|
125
|
+
await act(async () => button("offline").click());
|
|
126
|
+
expect(container.textContent).toContain("Device disconnected");
|
|
127
|
+
expect(button("offline").disabled).toBe(false);
|
|
128
|
+
render({
|
|
129
|
+
profile: null,
|
|
130
|
+
available: false,
|
|
131
|
+
onChange
|
|
132
|
+
});
|
|
133
|
+
expect(button("normal").disabled).toBe(true);
|
|
134
|
+
expect(container.textContent).toContain("Conditions unavailable on this device");
|
|
135
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import { Text, View } from "react-native";
|
|
5
|
+
import { NetworkIcon } from "@buoy-gg/shared-ui";
|
|
6
|
+
import { getNetworkCondition, subscribeNetworkConditions, NETWORK_CONDITIONS } from "./store";
|
|
7
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
export function NetworkConditionsIcon({
|
|
9
|
+
size
|
|
10
|
+
}) {
|
|
11
|
+
const profile = useSyncExternalStore(subscribeNetworkConditions, getNetworkCondition, getNetworkCondition);
|
|
12
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
13
|
+
accessibilityLabel: profile === "normal" ? "Network" : `Network: ${NETWORK_CONDITIONS[profile].label}`,
|
|
14
|
+
children: [/*#__PURE__*/_jsx(NetworkIcon, {
|
|
15
|
+
size: size
|
|
16
|
+
}), profile !== "normal" && /*#__PURE__*/_jsx(Text, {
|
|
17
|
+
testID: "buoy-network-condition-indicator",
|
|
18
|
+
style: {
|
|
19
|
+
position: "absolute",
|
|
20
|
+
top: -6,
|
|
21
|
+
right: -8,
|
|
22
|
+
color: "#FFCC66",
|
|
23
|
+
fontSize: 10,
|
|
24
|
+
fontWeight: "700"
|
|
25
|
+
},
|
|
26
|
+
children: profile === "offline" ? "OFF" : `+${NETWORK_CONDITIONS[profile].delayMs}`
|
|
27
|
+
})]
|
|
28
|
+
});
|
|
29
|
+
}
|