@buoy-gg/network 7.0.36 → 7.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +45 -12
  2. package/lib/commonjs/index.js +26 -1
  3. package/lib/commonjs/index.web.js +26 -1
  4. package/lib/commonjs/network/components/NetworkEventDetailView.js +6 -1
  5. package/lib/commonjs/network/components/NetworkHeaderMenu.js +4 -0
  6. package/lib/commonjs/network/components/NetworkModal.js +24 -8
  7. package/lib/commonjs/network/conditions/NetworkConditionsControl.js +235 -0
  8. package/lib/commonjs/network/conditions/NetworkConditionsControl.test.js +136 -0
  9. package/lib/commonjs/network/conditions/NetworkConditionsIcon.js +33 -0
  10. package/lib/commonjs/network/conditions/NetworkFloatingTool.js +310 -0
  11. package/lib/commonjs/network/conditions/NetworkFloatingTool.test.js +219 -0
  12. package/lib/commonjs/network/conditions/store.js +75 -0
  13. package/lib/commonjs/network/conditions/wait.js +43 -0
  14. package/lib/commonjs/network/conditions/xhr.js +110 -0
  15. package/lib/commonjs/network/hooks/useNetworkEvents.js +1 -1
  16. package/lib/commonjs/network/overrides/applyToXhr.js +10 -8
  17. package/lib/commonjs/network/overrides/engine.js +2 -1
  18. package/lib/commonjs/network/sync/networkSyncAdapter.js +26 -1
  19. package/lib/commonjs/network/utils/generateNetworkCopyText.js +2 -0
  20. package/lib/commonjs/network/utils/networkEventStore.js +5 -77
  21. package/lib/commonjs/network/utils/networkListener.js +96 -51
  22. package/lib/commonjs/network/utils/networkSavedStore.js +2 -2
  23. package/lib/commonjs/network/utils/selectSavedEvents.js +2 -2
  24. package/lib/commonjs/preset.js +2 -1
  25. package/lib/module/index.js +2 -1
  26. package/lib/module/index.web.js +2 -1
  27. package/lib/module/network/components/NetworkEventDetailView.js +6 -1
  28. package/lib/module/network/components/NetworkHeaderMenu.js +4 -0
  29. package/lib/module/network/components/NetworkModal.js +25 -9
  30. package/lib/module/network/conditions/NetworkConditionsControl.js +231 -0
  31. package/lib/module/network/conditions/NetworkConditionsControl.test.js +135 -0
  32. package/lib/module/network/conditions/NetworkConditionsIcon.js +29 -0
  33. package/lib/module/network/conditions/NetworkFloatingTool.js +304 -0
  34. package/lib/module/network/conditions/NetworkFloatingTool.test.js +218 -0
  35. package/lib/module/network/conditions/store.js +65 -0
  36. package/lib/module/network/conditions/wait.js +38 -0
  37. package/lib/module/network/conditions/xhr.js +106 -0
  38. package/lib/module/network/hooks/useNetworkEvents.js +1 -1
  39. package/lib/module/network/overrides/applyToXhr.js +9 -8
  40. package/lib/module/network/overrides/engine.js +2 -1
  41. package/lib/module/network/sync/networkSyncAdapter.js +26 -1
  42. package/lib/module/network/utils/generateNetworkCopyText.js +2 -0
  43. package/lib/module/network/utils/networkEventStore.js +6 -77
  44. package/lib/module/network/utils/networkListener.js +97 -52
  45. package/lib/module/network/utils/networkSavedStore.js +2 -2
  46. package/lib/module/network/utils/selectSavedEvents.js +2 -2
  47. package/lib/module/preset.js +3 -2
  48. package/lib/typescript/index.d.ts +2 -0
  49. package/lib/typescript/index.d.ts.map +1 -1
  50. package/lib/typescript/index.web.d.ts +2 -0
  51. package/lib/typescript/index.web.d.ts.map +1 -1
  52. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  53. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +2 -1
  54. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -1
  55. package/lib/typescript/network/components/NetworkModal.d.ts +3 -0
  56. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  57. package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts +12 -0
  58. package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts.map +1 -0
  59. package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts +2 -0
  60. package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts.map +1 -0
  61. package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts +4 -0
  62. package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts.map +1 -0
  63. package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts +15 -0
  64. package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts.map +1 -0
  65. package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts +2 -0
  66. package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts.map +1 -0
  67. package/lib/typescript/network/conditions/store.d.ts +30 -0
  68. package/lib/typescript/network/conditions/store.d.ts.map +1 -0
  69. package/lib/typescript/network/conditions/wait.d.ts +4 -0
  70. package/lib/typescript/network/conditions/wait.d.ts.map +1 -0
  71. package/lib/typescript/network/conditions/xhr.d.ts +5 -0
  72. package/lib/typescript/network/conditions/xhr.d.ts.map +1 -0
  73. package/lib/typescript/network/hooks/useNetworkEvents.d.ts +1 -1
  74. package/lib/typescript/network/overrides/applyToXhr.d.ts +2 -0
  75. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
  76. package/lib/typescript/network/overrides/engine.d.ts +0 -10
  77. package/lib/typescript/network/overrides/engine.d.ts.map +1 -1
  78. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +17 -0
  79. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  80. package/lib/typescript/network/types/index.d.ts +1 -0
  81. package/lib/typescript/network/types/index.d.ts.map +1 -1
  82. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -1
  83. package/lib/typescript/network/utils/networkEventStore.d.ts +0 -15
  84. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  85. package/lib/typescript/network/utils/networkListener.d.ts +2 -1
  86. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  87. package/lib/typescript/network/utils/networkSavedStore.d.ts +2 -2
  88. package/lib/typescript/network/utils/selectSavedEvents.d.ts +2 -2
  89. package/lib/typescript/preset.d.ts +0 -16
  90. package/lib/typescript/preset.d.ts.map +1 -1
  91. package/package.json +2 -2
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+
3
+ var _react = require("react");
4
+ var _client = require("react-dom/client");
5
+ var _vitest = require("vitest");
6
+ var _NetworkConditionsControl = require("./NetworkConditionsControl");
7
+ // @vitest-environment jsdom
8
+
9
+ _vitest.vi.mock("@buoy-gg/shared-ui", async () => ({
10
+ ChevronDown: () => null,
11
+ Check: () => null,
12
+ night: (await _vitest.vi.importActual("../../../../shared/src/ui/night/nightTheme")).night
13
+ }));
14
+ _vitest.vi.mock("react-native", () => ({
15
+ StyleSheet: {
16
+ create: value => value
17
+ },
18
+ View: ({
19
+ children
20
+ }) => /*#__PURE__*/(0, _react.createElement)("div", null, children),
21
+ Text: ({
22
+ children
23
+ }) => /*#__PURE__*/(0, _react.createElement)("span", null, children),
24
+ ActivityIndicator: () => /*#__PURE__*/(0, _react.createElement)("span", null, "pending"),
25
+ TouchableOpacity: ({
26
+ children,
27
+ onPress,
28
+ disabled,
29
+ accessibilityLabel,
30
+ accessibilityState,
31
+ testID
32
+ }) => /*#__PURE__*/(0, _react.createElement)("button", {
33
+ onClick: onPress,
34
+ disabled,
35
+ "aria-label": accessibilityLabel,
36
+ "aria-checked": accessibilityState?.checked,
37
+ "data-testid": testID
38
+ }, children)
39
+ }));
40
+ _vitest.vi.mock("./store", () => ({
41
+ NETWORK_CONDITIONS: {
42
+ normal: {
43
+ label: "Normal"
44
+ },
45
+ slow: {
46
+ label: "Slow"
47
+ },
48
+ verySlow: {
49
+ label: "Very slow"
50
+ },
51
+ offline: {
52
+ label: "Offline"
53
+ }
54
+ },
55
+ getNetworkCondition: () => "normal",
56
+ conditionsCanApply: () => true,
57
+ subscribeNetworkConditions: () => () => {},
58
+ setNetworkCondition: _vitest.vi.fn()
59
+ }));
60
+ let root;
61
+ let container;
62
+ (0, _vitest.beforeEach)(() => {
63
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
64
+ container = document.createElement("div");
65
+ root = (0, _client.createRoot)(container);
66
+ });
67
+ (0, _vitest.afterEach)(() => (0, _react.act)(() => root.unmount()));
68
+ const button = profile => container.querySelector(`[data-testid="buoy-network-condition-${profile}"]`);
69
+ function render(remote) {
70
+ (0, _react.act)(() => root.render(/*#__PURE__*/(0, _react.createElement)(_NetworkConditionsControl.NetworkConditionsControl, {
71
+ remote
72
+ })));
73
+ }
74
+ const dropdown = () => container.querySelector('[data-testid="buoy-network-conditions"]');
75
+ const open = () => (0, _react.act)(() => dropdown().click());
76
+ (0, _vitest.it)("defaults to a closed No throttling dropdown and closes when the current option is chosen", () => {
77
+ const onChange = _vitest.vi.fn();
78
+ render({
79
+ profile: "normal",
80
+ available: true,
81
+ onChange
82
+ });
83
+ (0, _vitest.expect)(dropdown().textContent).toBe("No throttling");
84
+ (0, _vitest.expect)(button("normal")).toBeNull();
85
+ open();
86
+ (0, _vitest.expect)(["normal", "slow", "verySlow", "offline"].every(value => !!button(value))).toBe(true);
87
+ (0, _react.act)(() => button("normal").click());
88
+ (0, _vitest.expect)(button("normal")).toBeNull();
89
+ (0, _vitest.expect)(onChange).not.toHaveBeenCalled();
90
+ });
91
+ (0, _vitest.it)("blocks duplicate remote changes and waits for the device profile before selecting it", async () => {
92
+ let finish;
93
+ const onChange = _vitest.vi.fn(() => new Promise(resolve => {
94
+ finish = resolve;
95
+ }));
96
+ render({
97
+ profile: "normal",
98
+ available: true,
99
+ onChange
100
+ });
101
+ open();
102
+ (0, _react.act)(() => button("slow").click());
103
+ (0, _vitest.expect)(button("offline").disabled).toBe(true);
104
+ (0, _vitest.expect)(button("normal").getAttribute("aria-checked")).toBe("true");
105
+ (0, _react.act)(() => button("offline").click());
106
+ (0, _vitest.expect)(onChange).toHaveBeenCalledExactlyOnceWith("slow");
107
+ await (0, _react.act)(async () => finish());
108
+ render({
109
+ profile: "slow",
110
+ available: true,
111
+ onChange
112
+ });
113
+ (0, _vitest.expect)(dropdown().textContent).toBe("Slow");
114
+ (0, _vitest.expect)(button("slow")).toBeNull();
115
+ open();
116
+ (0, _vitest.expect)(button("slow").getAttribute("aria-checked")).toBe("true");
117
+ });
118
+ (0, _vitest.it)("reports a remote error, permits retry, and disables unavailable devices", async () => {
119
+ const onChange = _vitest.vi.fn().mockRejectedValue(new Error("Device disconnected"));
120
+ render({
121
+ profile: "normal",
122
+ available: true,
123
+ onChange
124
+ });
125
+ open();
126
+ await (0, _react.act)(async () => button("offline").click());
127
+ (0, _vitest.expect)(container.textContent).toContain("Device disconnected");
128
+ (0, _vitest.expect)(button("offline").disabled).toBe(false);
129
+ render({
130
+ profile: null,
131
+ available: false,
132
+ onChange
133
+ });
134
+ (0, _vitest.expect)(button("normal").disabled).toBe(true);
135
+ (0, _vitest.expect)(container.textContent).toContain("Conditions unavailable on this device");
136
+ });
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NetworkConditionsIcon = NetworkConditionsIcon;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
10
+ var _store = require("./store");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ function NetworkConditionsIcon({
13
+ size
14
+ }) {
15
+ const profile = (0, _react.useSyncExternalStore)(_store.subscribeNetworkConditions, _store.getNetworkCondition, _store.getNetworkCondition);
16
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
17
+ accessibilityLabel: profile === "normal" ? "Network" : `Network: ${_store.NETWORK_CONDITIONS[profile].label}`,
18
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.NetworkIcon, {
19
+ size: size
20
+ }), profile !== "normal" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
21
+ testID: "buoy-network-condition-indicator",
22
+ style: {
23
+ position: "absolute",
24
+ top: -6,
25
+ right: -8,
26
+ color: "#FFCC66",
27
+ fontSize: 10,
28
+ fontWeight: "700"
29
+ },
30
+ children: profile === "offline" ? "OFF" : `+${_store.NETWORK_CONDITIONS[profile].delayMs}`
31
+ })]
32
+ });
33
+ }
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NetworkFloatingTool = NetworkFloatingTool;
7
+ exports.createRemoteConditionsSource = createRemoteConditionsSource;
8
+ exports.openNetworkFloatingTool = openNetworkFloatingTool;
9
+ var _react = require("react");
10
+ var _reactNative = require("react-native");
11
+ var _sharedUi = require("@buoy-gg/shared-ui");
12
+ var _store = require("./store");
13
+ var _jsxRuntime = require("react/jsx-runtime");
14
+ /** A live source keeps the desktop strip current after its launching panel closes. */
15
+ function createRemoteConditionsSource() {
16
+ let value;
17
+ const listeners = new Set();
18
+ return {
19
+ getSnapshot: () => value,
20
+ subscribe: listener => {
21
+ listeners.add(listener);
22
+ return () => {
23
+ listeners.delete(listener);
24
+ };
25
+ },
26
+ update(next) {
27
+ value = next;
28
+ listeners.forEach(listener => listener());
29
+ }
30
+ };
31
+ }
32
+ const noSubscribe = () => () => {};
33
+ const noRemote = () => undefined;
34
+ const options = [{
35
+ id: "normal",
36
+ label: "No throttling",
37
+ detail: "No added delay"
38
+ }, {
39
+ id: "slow",
40
+ label: "Slow",
41
+ detail: "+500 ms before sending"
42
+ }, {
43
+ id: "verySlow",
44
+ label: "Very slow",
45
+ detail: "+2 seconds before sending"
46
+ }, {
47
+ id: "offline",
48
+ label: "Offline",
49
+ detail: "New requests fail"
50
+ }];
51
+ const localRegistration = {
52
+ id: "network-throttling",
53
+ render: context => /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFloatingTool, {
54
+ ...context
55
+ })
56
+ };
57
+ if (typeof __DEV__ !== "undefined" && __DEV__ && typeof navigator !== "undefined" && navigator.product === "ReactNative") {
58
+ (0, _sharedUi.registerPersistentFloatingTool)(localRegistration, _sharedUi.persistentStorage);
59
+ }
60
+ function openNetworkFloatingTool(source) {
61
+ (0, _sharedUi.openFloatingTool)({
62
+ id: "network-throttling",
63
+ render: context => /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFloatingTool, {
64
+ ...context,
65
+ source: source
66
+ })
67
+ });
68
+ }
69
+ function NetworkFloatingTool({
70
+ id,
71
+ revealToken,
72
+ dismiss,
73
+ source
74
+ }) {
75
+ const local = (0, _react.useSyncExternalStore)(_store.subscribeNetworkConditions, _store.getNetworkCondition, _store.getNetworkCondition);
76
+ const localAvailable = (0, _react.useSyncExternalStore)(_store.subscribeNetworkConditions, _store.conditionsCanApply, _store.conditionsCanApply);
77
+ const remote = (0, _react.useSyncExternalStore)(source?.subscribe ?? noSubscribe, source?.getSnapshot ?? noRemote, noRemote);
78
+ const profile = source ? remote?.profile ?? null : local;
79
+ const available = source ? remote?.available === true : localAvailable;
80
+ const [pending, setPending] = (0, _react.useState)(false);
81
+ const [error, setError] = (0, _react.useState)();
82
+ const busy = (0, _react.useRef)(false);
83
+ const operation = (0, _react.useRef)(0);
84
+ const mounted = (0, _react.useRef)(true);
85
+ const {
86
+ width
87
+ } = (0, _reactNative.useWindowDimensions)();
88
+ (0, _react.useEffect)(() => {
89
+ mounted.current = true;
90
+ return () => {
91
+ mounted.current = false;
92
+ };
93
+ }, []);
94
+ (0, _react.useEffect)(() => {
95
+ operation.current += 1;
96
+ busy.current = false;
97
+ setPending(false);
98
+ setError(undefined);
99
+ }, [source, remote?.targetId]);
100
+ const active = profile !== null && profile !== "normal";
101
+ const selected = profile ?? "normal";
102
+ const choice = options.find(option => option.id === selected);
103
+ const next = options[(options.findIndex(option => option.id === selected) + 1) % options.length];
104
+ const bars = !available || selected === "offline" ? 0 : selected === "verySlow" ? 1 : selected === "slow" ? 2 : 4;
105
+ const detail = pending ? "Applying…" : !available ? "Unavailable" : selected === "normal" ? "0 ms" : selected === "slow" ? "+500 ms" : selected === "verySlow" ? "+2000 ms" : "Blocked";
106
+ async function apply(next, close = false) {
107
+ if (busy.current) return;
108
+ const currentOperation = ++operation.current;
109
+ busy.current = true;
110
+ setPending(true);
111
+ setError(undefined);
112
+ const startingRemote = source?.getSnapshot();
113
+ try {
114
+ if (source) {
115
+ if (!startingRemote?.available) throw new Error("Device unavailable. Open Network again to reconnect these controls.");
116
+ await startingRemote.onChange(next);
117
+ } else (0, _store.setNetworkCondition)(next);
118
+ if (mounted.current && operation.current === currentOperation) {
119
+ if (close) dismiss();
120
+ }
121
+ } catch (failure) {
122
+ if (mounted.current && operation.current === currentOperation) setError(failure instanceof Error ? failure.message : String(failure));
123
+ } finally {
124
+ if (operation.current === currentOperation) {
125
+ busy.current = false;
126
+ if (mounted.current) setPending(false);
127
+ }
128
+ }
129
+ }
130
+ const accessory = error ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
131
+ style: styles.menu,
132
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
133
+ accessibilityRole: "alert",
134
+ style: styles.error,
135
+ children: error
136
+ })
137
+ }) : undefined;
138
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.FloatingTool, {
139
+ id: id,
140
+ revealToken: revealToken,
141
+ variant: "strip",
142
+ dragFromSurface: true,
143
+ accessibilityLabel: `Move or hide network throttling. ${active ? "Active" : "Off"}`,
144
+ accessory: accessory,
145
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
146
+ style: [styles.row, {
147
+ width: Math.max(220, Math.min(292, width - 64))
148
+ }],
149
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
150
+ testID: "throttling-cycle",
151
+ accessibilityRole: "button",
152
+ accessibilityLabel: `Network: ${choice.label}. Switch to ${next.label}`,
153
+ accessibilityState: {
154
+ disabled: pending || !available
155
+ },
156
+ disabled: pending || !available,
157
+ onPress: () => void apply(next.id),
158
+ style: styles.signalButton,
159
+ children: pending ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
160
+ size: "small",
161
+ color: _sharedUi.night.color.accent
162
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
163
+ testID: `throttling-signal-${selected}`,
164
+ style: styles.signal,
165
+ children: [[7, 12, 18, 24].map((height, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
166
+ style: {
167
+ width: 4,
168
+ height,
169
+ borderRadius: 2,
170
+ backgroundColor: index < bars ? selected === "offline" ? _sharedUi.night.color.danger : _sharedUi.night.color.accent : _sharedUi.night.color.fillTertiary
171
+ }
172
+ }, height)), selected === "offline" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
173
+ style: styles.slash
174
+ })]
175
+ })
176
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
177
+ style: styles.profile,
178
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
179
+ testID: "throttling-profile",
180
+ accessibilityLabel: `Connection profile: ${choice.label}`,
181
+ accessibilityLiveRegion: "polite",
182
+ numberOfLines: 1,
183
+ style: styles.profileLabel,
184
+ children: choice.label
185
+ })
186
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
187
+ testID: "throttling-status",
188
+ numberOfLines: 1,
189
+ accessibilityLiveRegion: "polite",
190
+ style: styles.status,
191
+ children: detail
192
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
193
+ style: styles.separator
194
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
195
+ testID: "throttling-close",
196
+ accessibilityRole: "button",
197
+ accessibilityLabel: "Close and turn off throttling",
198
+ disabled: pending,
199
+ accessibilityState: {
200
+ disabled: pending
201
+ },
202
+ onPress: () => {
203
+ if (profile === "normal") dismiss();else void apply("normal", true);
204
+ },
205
+ style: [styles.action, pending && styles.disabled],
206
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
207
+ style: styles.actionText,
208
+ children: "Close"
209
+ })
210
+ })]
211
+ })
212
+ });
213
+ }
214
+ const styles = _reactNative.StyleSheet.create({
215
+ row: {
216
+ flexDirection: "row",
217
+ alignItems: "center",
218
+ gap: 6,
219
+ paddingVertical: 4,
220
+ minHeight: 48
221
+ },
222
+ signalButton: {
223
+ width: 36,
224
+ minHeight: 44,
225
+ alignItems: "center",
226
+ justifyContent: "center"
227
+ },
228
+ signal: {
229
+ flexDirection: "row",
230
+ alignItems: "flex-end",
231
+ gap: 3,
232
+ height: 24
233
+ },
234
+ slash: {
235
+ position: "absolute",
236
+ width: 30,
237
+ height: 2,
238
+ backgroundColor: _sharedUi.night.color.danger,
239
+ top: 11,
240
+ left: -2,
241
+ transform: [{
242
+ rotate: "-45deg"
243
+ }]
244
+ },
245
+ separator: {
246
+ height: 20,
247
+ width: 1,
248
+ backgroundColor: _sharedUi.night.color.separator,
249
+ marginHorizontal: 2
250
+ },
251
+ status: {
252
+ color: _sharedUi.night.color.textSecondary,
253
+ fontSize: 10,
254
+ lineHeight: 15,
255
+ flexShrink: 1
256
+ },
257
+ profile: {
258
+ flex: 1,
259
+ flexDirection: "row",
260
+ alignItems: "center",
261
+ gap: 6,
262
+ minHeight: 44
263
+ },
264
+ profileLabel: {
265
+ color: _sharedUi.night.color.text,
266
+ fontSize: 13,
267
+ fontWeight: "500",
268
+ flexShrink: 1
269
+ },
270
+ action: {
271
+ minHeight: 36,
272
+ minWidth: 52,
273
+ borderRadius: _sharedUi.night.radius.input,
274
+ borderWidth: 1,
275
+ borderColor: _sharedUi.night.color.accentBorderStrong,
276
+ alignItems: "center",
277
+ justifyContent: "center",
278
+ paddingHorizontal: 10
279
+ },
280
+ actionText: {
281
+ color: _sharedUi.night.color.accent,
282
+ fontSize: 12,
283
+ fontWeight: "600"
284
+ },
285
+ disabled: {
286
+ opacity: 0.4
287
+ },
288
+ menu: {
289
+ marginVertical: 8,
290
+ padding: 6,
291
+ borderRadius: _sharedUi.night.radius.card,
292
+ borderWidth: 1,
293
+ borderColor: _sharedUi.night.color.separator,
294
+ backgroundColor: _sharedUi.night.color.surface,
295
+ shadowColor: "#000",
296
+ shadowOffset: {
297
+ width: 0,
298
+ height: 6
299
+ },
300
+ shadowOpacity: 0.25,
301
+ shadowRadius: 16,
302
+ elevation: 8
303
+ },
304
+ error: {
305
+ color: _sharedUi.night.color.danger,
306
+ fontSize: 12,
307
+ lineHeight: 18,
308
+ padding: 10
309
+ }
310
+ });
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+
3
+ var _react = require("react");
4
+ var _client = require("react-dom/client");
5
+ var _vitest = require("vitest");
6
+ var _NetworkFloatingTool = require("./NetworkFloatingTool");
7
+ // @vitest-environment jsdom
8
+
9
+ const state = _vitest.vi.hoisted(() => ({
10
+ profile: "normal",
11
+ available: true,
12
+ listeners: new Set(),
13
+ set: _vitest.vi.fn()
14
+ }));
15
+ _vitest.vi.mock("./store", () => ({
16
+ getNetworkCondition: () => state.profile,
17
+ conditionsCanApply: () => state.available,
18
+ subscribeNetworkConditions: listener => {
19
+ state.listeners.add(listener);
20
+ return () => state.listeners.delete(listener);
21
+ },
22
+ setNetworkCondition: profile => {
23
+ state.set(profile);
24
+ state.profile = profile;
25
+ state.listeners.forEach(listener => listener());
26
+ }
27
+ }));
28
+ _vitest.vi.mock("@buoy-gg/shared-ui", async () => ({
29
+ night: (await _vitest.vi.importActual("../../../../shared/src/ui/night/nightTheme")).night,
30
+ ChevronDown: () => null,
31
+ Check: () => null,
32
+ openFloatingTool: _vitest.vi.fn(),
33
+ FloatingTool: ({
34
+ children,
35
+ accessory
36
+ }) => /*#__PURE__*/(0, _react.createElement)("div", null, children, accessory)
37
+ }));
38
+ _vitest.vi.mock("react-native", () => ({
39
+ StyleSheet: {
40
+ create: value => value
41
+ },
42
+ useWindowDimensions: () => ({
43
+ width: 390,
44
+ height: 844
45
+ }),
46
+ View: ({
47
+ children,
48
+ testID
49
+ }) => /*#__PURE__*/(0, _react.createElement)("div", {
50
+ "data-testid": testID
51
+ }, children),
52
+ Text: ({
53
+ children,
54
+ testID
55
+ }) => /*#__PURE__*/(0, _react.createElement)("span", {
56
+ "data-testid": testID
57
+ }, children),
58
+ ActivityIndicator: () => null,
59
+ TouchableOpacity: ({
60
+ children,
61
+ onPress,
62
+ disabled,
63
+ testID,
64
+ accessibilityLabel
65
+ }) => /*#__PURE__*/(0, _react.createElement)("button", {
66
+ onClick: onPress,
67
+ disabled,
68
+ "data-testid": testID,
69
+ "aria-label": accessibilityLabel
70
+ }, children)
71
+ }));
72
+ let root;
73
+ let container;
74
+ const dismiss = _vitest.vi.fn();
75
+ (0, _vitest.beforeEach)(() => {
76
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
77
+ state.profile = "normal";
78
+ state.available = true;
79
+ state.set.mockClear();
80
+ dismiss.mockClear();
81
+ container = document.createElement("div");
82
+ root = (0, _client.createRoot)(container);
83
+ });
84
+ (0, _vitest.afterEach)(() => (0, _react.act)(() => root.unmount()));
85
+ const button = id => container.querySelector(`[data-testid="${id}"]`);
86
+ const tap = async id => (0, _react.act)(async () => button(id).click());
87
+ const status = () => container.querySelector('[data-testid="throttling-status"]')?.textContent;
88
+ function render(source) {
89
+ (0, _react.act)(() => root.render(/*#__PURE__*/(0, _react.createElement)(_NetworkFloatingTool.NetworkFloatingTool, {
90
+ id: "network-throttling",
91
+ revealToken: 1,
92
+ dismiss,
93
+ source
94
+ })));
95
+ }
96
+ async function choose(value) {
97
+ const order = ["normal", "slow", "verySlow", "offline"];
98
+ const count = (order.indexOf(value) - order.indexOf(state.profile) + order.length) % order.length;
99
+ for (let i = 0; i < count; i++) await tap("throttling-cycle");
100
+ }
101
+ (0, _vitest.it)("cycles and applies every profile immediately, including wrapping to normal", async () => {
102
+ render();
103
+ for (const [profile, label] of [["slow", "+500 ms"], ["verySlow", "+2000 ms"], ["offline", "Blocked"], ["normal", "0 ms"]]) {
104
+ await tap("throttling-cycle");
105
+ (0, _vitest.expect)(state.profile).toBe(profile);
106
+ (0, _vitest.expect)(container.querySelector(`[data-testid="throttling-signal-${profile}"]`)).not.toBeNull();
107
+ (0, _vitest.expect)(status()).toBe(label);
108
+ }
109
+ (0, _vitest.expect)(state.set).toHaveBeenCalledTimes(4);
110
+ });
111
+ (0, _vitest.it)("keeps the profile label informational when tapped", async () => {
112
+ render();
113
+ await tap("throttling-profile");
114
+ (0, _vitest.expect)(state.set).not.toHaveBeenCalled();
115
+ (0, _vitest.expect)(state.profile).toBe("normal");
116
+ (0, _vitest.expect)(button("throttling-profile").closest("button")).toBeNull();
117
+ });
118
+ (0, _vitest.it)("changes active profiles and stops before dismissal", async () => {
119
+ state.profile = "slow";
120
+ render();
121
+ await choose("offline");
122
+ (0, _vitest.expect)(state.profile).toBe("offline");
123
+ await tap("throttling-close");
124
+ (0, _vitest.expect)(state.profile).toBe("normal");
125
+ (0, _vitest.expect)(dismiss).toHaveBeenCalledOnce();
126
+ });
127
+ (0, _vitest.it)("reflects external changes and disables cycling when account admission is revoked", () => {
128
+ render();
129
+ (0, _react.act)(() => {
130
+ state.profile = "offline";
131
+ state.listeners.forEach(listener => listener());
132
+ });
133
+ (0, _vitest.expect)(status()).toBe("Blocked");
134
+ (0, _react.act)(() => {
135
+ state.profile = "normal";
136
+ state.available = false;
137
+ state.listeners.forEach(listener => listener());
138
+ });
139
+ (0, _vitest.expect)(status()).toBe("Unavailable");
140
+ (0, _vitest.expect)(button("throttling-cycle").disabled).toBe(true);
141
+ });
142
+ (0, _vitest.it)("keeps remote failures visible and does not claim a profile was applied", async () => {
143
+ const source = (0, _NetworkFloatingTool.createRemoteConditionsSource)();
144
+ const onChange = _vitest.vi.fn().mockRejectedValue(new Error("Connection lost"));
145
+ source.update({
146
+ profile: "normal",
147
+ available: true,
148
+ onChange
149
+ });
150
+ render(source);
151
+ await choose("slow");
152
+ (0, _vitest.expect)(container.textContent).toContain("Connection lost");
153
+ (0, _vitest.expect)(status()).toBe("0 ms");
154
+ (0, _vitest.expect)(state.set).not.toHaveBeenCalled();
155
+ });
156
+ (0, _vitest.it)("prevents duplicate remote operations and follows confirmed snapshots", async () => {
157
+ const source = (0, _NetworkFloatingTool.createRemoteConditionsSource)();
158
+ let resolve;
159
+ const onChange = _vitest.vi.fn(() => new Promise(done => {
160
+ resolve = done;
161
+ }));
162
+ source.update({
163
+ profile: "normal",
164
+ available: true,
165
+ onChange
166
+ });
167
+ render(source);
168
+ await tap("throttling-cycle");
169
+ await tap("throttling-cycle");
170
+ (0, _vitest.expect)(onChange).toHaveBeenCalledOnce();
171
+ (0, _vitest.expect)(status()).toBe("Applying…");
172
+ await (0, _react.act)(async () => resolve());
173
+ (0, _vitest.expect)(status()).toBe("0 ms");
174
+ (0, _react.act)(() => source.update({
175
+ profile: "slow",
176
+ available: true,
177
+ onChange
178
+ }));
179
+ (0, _vitest.expect)(status()).toBe("+500 ms");
180
+ (0, _react.act)(() => source.update(undefined));
181
+ (0, _vitest.expect)(status()).toBe("Unavailable");
182
+ });
183
+ (0, _vitest.it)("ignores a previous device's completion after switching devices", async () => {
184
+ const source = (0, _NetworkFloatingTool.createRemoteConditionsSource)();
185
+ let reject;
186
+ source.update({
187
+ targetId: "first",
188
+ profile: "normal",
189
+ available: true,
190
+ onChange: () => new Promise((_resolve, fail) => {
191
+ reject = fail;
192
+ })
193
+ });
194
+ render(source);
195
+ await choose("slow");
196
+ (0, _react.act)(() => source.update({
197
+ targetId: "second",
198
+ profile: "normal",
199
+ available: true,
200
+ onChange: _vitest.vi.fn()
201
+ }));
202
+ await (0, _react.act)(async () => reject(new Error("Old device failed")));
203
+ (0, _vitest.expect)(container.textContent).not.toContain("Old device failed");
204
+ (0, _vitest.expect)(status()).toBe("0 ms");
205
+ (0, _vitest.expect)(button("throttling-profile").textContent).toBe("No throttling");
206
+ });
207
+ (0, _vitest.it)("keeps the strip open when stopping a remote device fails", async () => {
208
+ const source = (0, _NetworkFloatingTool.createRemoteConditionsSource)();
209
+ source.update({
210
+ profile: "offline",
211
+ available: true,
212
+ onChange: _vitest.vi.fn().mockRejectedValue(new Error("Could not stop"))
213
+ });
214
+ render(source);
215
+ await tap("throttling-close");
216
+ (0, _vitest.expect)(dismiss).not.toHaveBeenCalled();
217
+ (0, _vitest.expect)(status()).toBe("Blocked");
218
+ (0, _vitest.expect)(container.textContent).toContain("Could not stop");
219
+ });