@@ -28,6 +28,11 @@ const logger = createLogger({
2828 name : 'FixtureUtils' ,
2929} ) ;
3030
31+ let androidDeviceProxyConfigured = false ;
32+ let iosDeviceProxyConfigured = false ;
33+ let iosProxyConfiguredViaNetworkSetup = false ;
34+ let iosNetworkServicesWithProxy : string [ ] = [ ] ;
35+
3136/**
3237 * Maps ResourceType to fallback port.
3338 * These ports are used in fixture data and are mapped to actual allocated ports
@@ -115,6 +120,228 @@ export async function cleanupAllAndroidPortForwarding(): Promise<void> {
115120 logger . debug ( '✓ Cleaned up test port forwarding' ) ;
116121}
117122
123+ /**
124+ * Enables emulator/simulator HTTP(S) proxy so device-level traffic is captured by MockServer.
125+ * This is especially useful for network calls that don't go through the app JS bridge
126+ * (e.g., requests made inside WebViews).
127+ *
128+ * - Android: Uses global proxy settings pointing to fallback MockServer port (8000),
129+ * which is already mapped to the allocated host port via adb reverse.
130+ * - iOS: Tries simulator-local proxy config first (simctl + defaults). If unavailable,
131+ * falls back to host networkservice proxy configuration via networksetup.
132+ */
133+ export async function enableDeviceTrafficProxyForMockServer (
134+ mockServerPort : number ,
135+ ) : Promise < void > {
136+ if ( isBrowserStack ( ) ) {
137+ logger . info (
138+ 'BrowserStack mode: skipping device-level proxy configuration for MockServer' ,
139+ ) ;
140+ return ;
141+ }
142+
143+ if ( await PlatformDetector . isAndroid ( ) ) {
144+ let deviceFlag = '' ;
145+ if ( FrameworkDetector . isDetox ( ) ) {
146+ const deviceId = device . id || '' ;
147+ deviceFlag = deviceId ? `-s ${ deviceId } ` : '' ;
148+ }
149+
150+ const proxyHost = '127.0.0.1' ;
151+ const proxyPort = FALLBACK_MOCKSERVER_PORT ;
152+ const exclusionList = 'localhost,127.0.0.1,10.0.2.2' ;
153+
154+ await execAsync (
155+ `adb ${ deviceFlag } shell settings put global http_proxy ${ proxyHost } :${ proxyPort } ` ,
156+ ) ;
157+ await execAsync (
158+ `adb ${ deviceFlag } shell settings put global global_http_proxy_host ${ proxyHost } ` ,
159+ ) ;
160+ await execAsync (
161+ `adb ${ deviceFlag } shell settings put global global_http_proxy_port ${ proxyPort } ` ,
162+ ) ;
163+ await execAsync (
164+ `adb ${ deviceFlag } shell settings put global global_http_proxy_exclusion_list ${ exclusionList } ` ,
165+ ) ;
166+
167+ androidDeviceProxyConfigured = true ;
168+ logger . info (
169+ `Enabled Android device proxy ${ proxyHost } :${ proxyPort } (MockServer allocated port: ${ mockServerPort } )` ,
170+ ) ;
171+ return ;
172+ }
173+
174+ if ( ! ( await PlatformDetector . isIOS ( ) ) ) {
175+ return ;
176+ }
177+
178+ if ( process . platform !== 'darwin' ) {
179+ logger . warn (
180+ 'Skipping iOS simulator proxy setup because host platform is not macOS' ,
181+ ) ;
182+ return ;
183+ }
184+
185+ const proxyHost = '127.0.0.1' ;
186+
187+ // Prefer simulator-local proxy settings first to avoid mutating host network services.
188+ try {
189+ const targetSimulator =
190+ FrameworkDetector . isDetox ( ) && device ?. id ? device . id : 'booted' ;
191+
192+ await execAsync (
193+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPEnable -int 1` ,
194+ ) ;
195+ await execAsync (
196+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPProxy -string ${ proxyHost } ` ,
197+ ) ;
198+ await execAsync (
199+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPPort -int ${ mockServerPort } ` ,
200+ ) ;
201+ await execAsync (
202+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPSEnable -int 1` ,
203+ ) ;
204+ await execAsync (
205+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPSProxy -string ${ proxyHost } ` ,
206+ ) ;
207+ await execAsync (
208+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPSPort -int ${ mockServerPort } ` ,
209+ ) ;
210+
211+ iosDeviceProxyConfigured = true ;
212+ iosProxyConfiguredViaNetworkSetup = false ;
213+ logger . info (
214+ `Enabled iOS simulator proxy via simctl defaults (${ proxyHost } :${ mockServerPort } )` ,
215+ ) ;
216+ return ;
217+ } catch ( simctlError ) {
218+ logger . warn (
219+ 'Failed to enable iOS simulator proxy via simctl defaults, trying networksetup fallback' ,
220+ simctlError ,
221+ ) ;
222+ }
223+
224+ // Fallback: mutate host network service proxy settings.
225+ const { stdout } = await execAsync ( 'networksetup -listallnetworkservices' ) ;
226+ const services = stdout
227+ . split ( '\n' )
228+ . map ( ( line ) => line . trim ( ) )
229+ . filter (
230+ ( line ) =>
231+ line &&
232+ ! line . startsWith ( 'An asterisk' ) &&
233+ ! line . startsWith ( '*' ) ,
234+ ) ;
235+
236+ const configuredServices : string [ ] = [ ] ;
237+ for ( const service of services ) {
238+ try {
239+ await execAsync (
240+ `networksetup -setwebproxy "${ service } " ${ proxyHost } ${ mockServerPort } ` ,
241+ ) ;
242+ await execAsync (
243+ `networksetup -setsecurewebproxy "${ service } " ${ proxyHost } ${ mockServerPort } ` ,
244+ ) ;
245+ await execAsync ( `networksetup -setwebproxystate "${ service } " on` ) ;
246+ await execAsync ( `networksetup -setsecurewebproxystate "${ service } " on` ) ;
247+ await execAsync (
248+ `networksetup -setproxybypassdomains "${ service } " localhost 127.0.0.1 ::1` ,
249+ ) ;
250+ configuredServices . push ( service ) ;
251+ } catch ( serviceError ) {
252+ logger . warn (
253+ `Failed enabling proxy on macOS network service "${ service } "` ,
254+ serviceError ,
255+ ) ;
256+ }
257+ }
258+
259+ if ( configuredServices . length === 0 ) {
260+ throw new Error (
261+ 'Could not enable iOS simulator proxy: no network service accepted proxy configuration' ,
262+ ) ;
263+ }
264+
265+ iosDeviceProxyConfigured = true ;
266+ iosProxyConfiguredViaNetworkSetup = true ;
267+ iosNetworkServicesWithProxy = configuredServices ;
268+ logger . info (
269+ `Enabled iOS simulator proxy via networksetup on ${ configuredServices . length } service(s): ${ configuredServices . join ( ', ' ) } ` ,
270+ ) ;
271+ }
272+
273+ /**
274+ * Disables previously configured emulator/simulator proxy settings.
275+ * Safe to call multiple times.
276+ */
277+ export async function disableDeviceTrafficProxy ( ) : Promise < void > {
278+ if ( isBrowserStack ( ) ) {
279+ return ;
280+ }
281+
282+ if ( ( await PlatformDetector . isAndroid ( ) ) && androidDeviceProxyConfigured ) {
283+ let deviceFlag = '' ;
284+ if ( FrameworkDetector . isDetox ( ) ) {
285+ const deviceId = device . id || '' ;
286+ deviceFlag = deviceId ? `-s ${ deviceId } ` : '' ;
287+ }
288+
289+ await execAsync ( `adb ${ deviceFlag } shell settings put global http_proxy :0` ) ;
290+ await execAsync (
291+ `adb ${ deviceFlag } shell settings delete global global_http_proxy_host` ,
292+ ) ;
293+ await execAsync (
294+ `adb ${ deviceFlag } shell settings delete global global_http_proxy_port` ,
295+ ) ;
296+ await execAsync (
297+ `adb ${ deviceFlag } shell settings delete global global_http_proxy_exclusion_list` ,
298+ ) ;
299+
300+ androidDeviceProxyConfigured = false ;
301+ logger . info ( 'Disabled Android device proxy settings' ) ;
302+ return ;
303+ }
304+
305+ if ( ! ( await PlatformDetector . isIOS ( ) ) || ! iosDeviceProxyConfigured ) {
306+ return ;
307+ }
308+
309+ if ( process . platform !== 'darwin' ) {
310+ iosDeviceProxyConfigured = false ;
311+ iosProxyConfiguredViaNetworkSetup = false ;
312+ iosNetworkServicesWithProxy = [ ] ;
313+ return ;
314+ }
315+
316+ if ( iosProxyConfiguredViaNetworkSetup ) {
317+ for ( const service of iosNetworkServicesWithProxy ) {
318+ try {
319+ await execAsync ( `networksetup -setwebproxystate "${ service } " off` ) ;
320+ await execAsync ( `networksetup -setsecurewebproxystate "${ service } " off` ) ;
321+ } catch ( serviceError ) {
322+ logger . warn (
323+ `Failed disabling proxy on macOS network service "${ service } "` ,
324+ serviceError ,
325+ ) ;
326+ }
327+ }
328+ } else {
329+ const targetSimulator =
330+ FrameworkDetector . isDetox ( ) && device ?. id ? device . id : 'booted' ;
331+ await execAsync (
332+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPEnable -int 0` ,
333+ ) ;
334+ await execAsync (
335+ `xcrun simctl spawn ${ targetSimulator } defaults write com.apple.CFNetwork ProxiesHTTPSEnable -int 0` ,
336+ ) ;
337+ }
338+
339+ iosDeviceProxyConfigured = false ;
340+ iosProxyConfiguredViaNetworkSetup = false ;
341+ iosNetworkServicesWithProxy = [ ] ;
342+ logger . info ( 'Disabled iOS simulator proxy settings' ) ;
343+ }
344+
118345/**
119346 * Sets up adb reverse for Android to map fallback port to actual allocated port.
120347 *
0 commit comments