@truewatchtech/react-native-mobile 0.4.2 → 0.4.3
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/FTMobileReactNativeSDK.podspec +7 -5
- package/android/build.gradle +10 -3
- package/android/src/main/java/com/ft/sdk/reactnative/FTLogImpl.java +10 -2
- package/android/src/main/java/com/ft/sdk/reactnative/FTMobileImpl.java +45 -10
- package/android/src/main/java/com/ft/sdk/reactnative/FTRUMImpl.java +131 -6
- package/android/src/main/java/com/ft/sdk/reactnative/FTTraceImpl.java +10 -3
- package/android/src/main/java/com/ft/sdk/reactnative/JsLongTaskMonitor.java +281 -0
- package/android/src/main/java/com/ft/sdk/reactnative/ViewTiming.java +17 -0
- package/android/src/main/java/com/ft/sdk/reactnative/VisitorIdConfigMapper.java +50 -0
- package/android/src/main/java/com/ft/sdk/reactnative/utils/ReactNativeUtils.java +5 -12
- package/android/src/newarch/java/com/ft/sdk/reactnative/FTMobileModule.java +14 -5
- package/android/src/newarch/java/com/ft/sdk/reactnative/FTRUMModule.java +44 -1
- package/android/src/newarch/java/com/ft/sdk/reactnative/FTTraceModule.java +14 -0
- package/android/src/oldarch/java/com/ft/sdk/reactnative/FTMobileModule.java +7 -2
- package/android/src/oldarch/java/com/ft/sdk/reactnative/FTRUMModule.java +39 -1
- package/android/src/oldarch/java/com/ft/sdk/reactnative/FTTraceModule.java +12 -0
- package/android/src/rnpost70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +21 -0
- package/android/src/rnpre70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +37 -0
- package/android/src/test/java/com/ft/sdk/reactnative/FTMobileImplTest.java +53 -0
- package/android/src/test/java/com/ft/sdk/reactnative/FTRUMModuleTest.java +117 -0
- package/android/src/test/java/com/ft/sdk/reactnative/JsLongTaskMonitorTest.java +446 -0
- package/android/src/test/java/com/ft/sdk/reactnative/utils/ReactNativeUtilsTest.java +33 -0
- package/ios/FTJSLongTaskMonitor.h +51 -0
- package/ios/FTJSLongTaskMonitor.m +250 -0
- package/ios/FTMobileReactNative.mm +54 -18
- package/ios/FTReactNativeLog.mm +9 -5
- package/ios/FTReactNativeRUM.mm +196 -31
- package/ios/FTReactNativeResource.h +4 -0
- package/ios/FTReactNativeResource.m +64 -0
- package/ios/FTReactNativeTrace.mm +59 -14
- package/ios/FTReactNativeUtils.m +10 -15
- package/ios/FTReactNativeViewTiming.h +18 -0
- package/ios/FTReactNativeWebSocket.h +10 -0
- package/ios/FTReactNativeWebSocket.mm +201 -0
- package/ios/FTWebSocketMetadataStore.h +25 -0
- package/ios/FTWebSocketMetadataStore.m +622 -0
- package/ios/FTWebSocketResourceData.h +20 -0
- package/ios/FTWebSocketResourceData.m +106 -0
- package/lib/commonjs/ft_logger.js +3 -1
- package/lib/commonjs/ft_logger.js.map +1 -1
- package/lib/commonjs/ft_mobile_agent.js +47 -4
- package/lib/commonjs/ft_mobile_agent.js.map +1 -1
- package/lib/commonjs/ft_rum.js +65 -5
- package/lib/commonjs/ft_rum.js.map +1 -1
- package/lib/commonjs/ft_tracing.js +14 -4
- package/lib/commonjs/ft_tracing.js.map +1 -1
- package/lib/commonjs/index.js +21 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/rum/FTBabelInteractionTracking.js +72 -0
- package/lib/commonjs/rum/FTBabelInteractionTracking.js.map +1 -0
- package/lib/commonjs/rum/FTBabelUtils.js +87 -0
- package/lib/commonjs/rum/FTBabelUtils.js.map +1 -0
- package/lib/commonjs/rum/FTRumActionTracking.js +10 -39
- package/lib/commonjs/rum/FTRumActionTracking.js.map +1 -1
- package/lib/commonjs/rum/FTRumActionTrackingContext.js +50 -0
- package/lib/commonjs/rum/FTRumActionTrackingContext.js.map +1 -0
- package/lib/commonjs/rum/FTRumViewTracking.js +147 -0
- package/lib/commonjs/rum/FTRumViewTracking.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketNative.js +73 -0
- package/lib/commonjs/rum/FTRumWebSocketNative.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketTiming.js +73 -0
- package/lib/commonjs/rum/FTRumWebSocketTiming.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketTracking.js +506 -0
- package/lib/commonjs/rum/FTRumWebSocketTracking.js.map +1 -0
- package/lib/commonjs/rum/longTasksUtils.js +25 -0
- package/lib/commonjs/rum/longTasksUtils.js.map +1 -0
- package/lib/commonjs/specs/NativeFTMobileReactNative.js.map +1 -1
- package/lib/commonjs/specs/NativeFTReactNativeRUM.js.map +1 -1
- package/lib/commonjs/specs/NativeFTReactNativeTrace.js.map +1 -1
- package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js +11 -0
- package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/commonjs/visitorIdConfig.js +37 -0
- package/lib/commonjs/visitorIdConfig.js.map +1 -0
- package/lib/module/ft_logger.js +3 -1
- package/lib/module/ft_logger.js.map +1 -1
- package/lib/module/ft_mobile_agent.js +47 -4
- package/lib/module/ft_mobile_agent.js.map +1 -1
- package/lib/module/ft_rum.js +65 -5
- package/lib/module/ft_rum.js.map +1 -1
- package/lib/module/ft_tracing.js +14 -2
- package/lib/module/ft_tracing.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/rum/FTBabelInteractionTracking.js +66 -0
- package/lib/module/rum/FTBabelInteractionTracking.js.map +1 -0
- package/lib/module/rum/FTBabelUtils.js +80 -0
- package/lib/module/rum/FTBabelUtils.js.map +1 -0
- package/lib/module/rum/FTRumActionTracking.js +10 -39
- package/lib/module/rum/FTRumActionTracking.js.map +1 -1
- package/lib/module/rum/FTRumActionTrackingContext.js +43 -0
- package/lib/module/rum/FTRumActionTrackingContext.js.map +1 -0
- package/lib/module/rum/FTRumViewTracking.js +140 -0
- package/lib/module/rum/FTRumViewTracking.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketNative.js +62 -0
- package/lib/module/rum/FTRumWebSocketNative.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketTiming.js +67 -0
- package/lib/module/rum/FTRumWebSocketTiming.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketTracking.js +499 -0
- package/lib/module/rum/FTRumWebSocketTracking.js.map +1 -0
- package/lib/module/rum/longTasksUtils.js +18 -0
- package/lib/module/rum/longTasksUtils.js.map +1 -0
- package/lib/module/specs/NativeFTMobileReactNative.js.map +1 -1
- package/lib/module/specs/NativeFTReactNativeRUM.js.map +1 -1
- package/lib/module/specs/NativeFTReactNativeTrace.js.map +1 -1
- package/lib/module/specs/NativeFTReactNativeWebSocket.js +8 -0
- package/lib/module/specs/NativeFTReactNativeWebSocket.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/module/visitorIdConfig.js +31 -0
- package/lib/module/visitorIdConfig.js.map +1 -0
- package/lib/typescript/ft_logger.d.ts +11 -1
- package/lib/typescript/ft_mobile_agent.d.ts +28 -0
- package/lib/typescript/ft_rum.d.ts +42 -5
- package/lib/typescript/ft_tracing.d.ts +9 -1
- package/lib/typescript/index.d.ts +5 -1
- package/lib/typescript/rum/FTBabelInteractionTracking.d.ts +30 -0
- package/lib/typescript/rum/FTBabelUtils.d.ts +1 -0
- package/lib/typescript/rum/FTRumActionTracking.d.ts +0 -2
- package/lib/typescript/rum/FTRumActionTrackingContext.d.ts +11 -0
- package/lib/typescript/rum/FTRumViewTracking.d.ts +32 -0
- package/lib/typescript/rum/FTRumWebSocketNative.d.ts +22 -0
- package/lib/typescript/rum/FTRumWebSocketTiming.d.ts +7 -0
- package/lib/typescript/rum/FTRumWebSocketTracking.d.ts +54 -0
- package/lib/typescript/rum/longTasksUtils.d.ts +8 -0
- package/lib/typescript/specs/NativeFTMobileReactNative.d.ts +15 -0
- package/lib/typescript/specs/NativeFTReactNativeRUM.d.ts +11 -1
- package/lib/typescript/specs/NativeFTReactNativeTrace.d.ts +12 -0
- package/lib/typescript/specs/NativeFTReactNativeWebSocket.d.ts +13 -0
- package/lib/typescript/version.d.ts +1 -1
- package/lib/typescript/visitorIdConfig.d.ts +11 -0
- package/package.json +6 -4
- package/src/ft_logger.tsx +11 -1
- package/src/ft_mobile_agent.tsx +79 -7
- package/src/ft_rum.tsx +111 -7
- package/src/ft_tracing.tsx +24 -2
- package/src/index.tsx +8 -0
- package/src/rum/FTBabelInteractionTracking.ts +124 -0
- package/src/rum/FTBabelUtils.tsx +99 -0
- package/src/rum/FTRumActionTracking.tsx +14 -43
- package/src/rum/FTRumActionTrackingContext.ts +68 -0
- package/src/rum/FTRumViewTracking.ts +179 -0
- package/src/rum/FTRumWebSocketNative.ts +107 -0
- package/src/rum/FTRumWebSocketTiming.ts +82 -0
- package/src/rum/FTRumWebSocketTracking.ts +722 -0
- package/src/rum/longTasksUtils.ts +17 -0
- package/src/specs/NativeFTMobileReactNative.ts +18 -0
- package/src/specs/NativeFTReactNativeRUM.ts +19 -1
- package/src/specs/NativeFTReactNativeTrace.ts +12 -0
- package/src/specs/NativeFTReactNativeWebSocket.ts +33 -0
- package/src/version.ts +1 -1
- package/src/visitorIdConfig.ts +123 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.ft.sdk.reactnative;
|
|
2
|
+
|
|
3
|
+
import android.os.Handler;
|
|
4
|
+
import android.os.Looper;
|
|
5
|
+
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
7
|
+
import com.facebook.react.bridge.queue.MessageQueueThread;
|
|
8
|
+
import com.facebook.react.bridge.queue.MessageQueueThreadImpl;
|
|
9
|
+
import com.facebook.react.bridge.queue.ReactQueueConfiguration;
|
|
10
|
+
|
|
11
|
+
final class ReactJsThreadExecutor implements JsLongTaskMonitor.Executor {
|
|
12
|
+
private final ReactApplicationContext reactContext;
|
|
13
|
+
|
|
14
|
+
ReactJsThreadExecutor(ReactApplicationContext reactContext) {
|
|
15
|
+
this.reactContext = reactContext;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Override
|
|
19
|
+
public boolean runOnJsThread(Runnable runnable) {
|
|
20
|
+
if (!reactContext.hasActiveCatalystInstance()) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
ReactQueueConfiguration configuration =
|
|
24
|
+
reactContext.getCatalystInstance().getReactQueueConfiguration();
|
|
25
|
+
if (configuration == null) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
MessageQueueThread queue = configuration.getJSQueueThread();
|
|
29
|
+
if (!(queue instanceof MessageQueueThreadImpl)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
Looper looper = ((MessageQueueThreadImpl) queue).getLooper();
|
|
33
|
+
// Older RN queue APIs return void. Post SDK work to the same JS looper so
|
|
34
|
+
// a stopped queue can explicitly reject it, without waiting for a callback.
|
|
35
|
+
return looper != null && new Handler(looper).post(runnable);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.ft.sdk.reactnative;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertEquals;
|
|
4
|
+
|
|
5
|
+
import com.ft.sdk.FTSDKConfig;
|
|
6
|
+
|
|
7
|
+
import org.junit.Test;
|
|
8
|
+
|
|
9
|
+
public class FTMobileImplTest {
|
|
10
|
+
@Test
|
|
11
|
+
public void buildsConfigWithoutInitialUploadAddress() {
|
|
12
|
+
FTSDKConfig config = FTMobileImpl.buildSDKConfig(null, null, null);
|
|
13
|
+
|
|
14
|
+
assertEquals("", config.getDatakitUrl());
|
|
15
|
+
assertEquals("", config.getDatawayUrl());
|
|
16
|
+
assertEquals("", config.getClientToken());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
public void ignoresIncompleteDatawayConfig() {
|
|
21
|
+
FTSDKConfig config = FTMobileImpl.buildSDKConfig(null, "https://ingest.example.com", null);
|
|
22
|
+
|
|
23
|
+
assertEquals("", config.getDatakitUrl());
|
|
24
|
+
assertEquals("", config.getDatawayUrl());
|
|
25
|
+
assertEquals("", config.getClientToken());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test
|
|
29
|
+
public void keepsExistingDatakitConfigBehavior() {
|
|
30
|
+
FTSDKConfig config = FTMobileImpl.buildSDKConfig(
|
|
31
|
+
"http://10.0.0.1:9529",
|
|
32
|
+
"https://ingest.example.com",
|
|
33
|
+
"client-token"
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
assertEquals("http://10.0.0.1:9529", config.getDatakitUrl());
|
|
37
|
+
assertEquals("", config.getDatawayUrl());
|
|
38
|
+
assertEquals("", config.getClientToken());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
public void keepsExistingDatawayConfigBehavior() {
|
|
43
|
+
FTSDKConfig config = FTMobileImpl.buildSDKConfig(
|
|
44
|
+
null,
|
|
45
|
+
"https://ingest.example.com",
|
|
46
|
+
"client-token"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
assertEquals("", config.getDatakitUrl());
|
|
50
|
+
assertEquals("https://ingest.example.com", config.getDatawayUrl());
|
|
51
|
+
assertEquals("client-token", config.getClientToken());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package com.ft.sdk.reactnative;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise;
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
5
|
+
|
|
6
|
+
import org.junit.Test;
|
|
7
|
+
|
|
8
|
+
import java.lang.reflect.Method;
|
|
9
|
+
|
|
10
|
+
import static org.junit.Assume.assumeTrue;
|
|
11
|
+
import static org.mockito.ArgumentMatchers.any;
|
|
12
|
+
import static org.mockito.Mockito.mock;
|
|
13
|
+
import static org.mockito.Mockito.times;
|
|
14
|
+
import static org.mockito.Mockito.verify;
|
|
15
|
+
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
|
16
|
+
import static org.mockito.Mockito.when;
|
|
17
|
+
import static org.junit.Assert.assertFalse;
|
|
18
|
+
import static org.junit.Assert.assertTrue;
|
|
19
|
+
|
|
20
|
+
public class FTRUMModuleTest {
|
|
21
|
+
@Test
|
|
22
|
+
public void bridgeContextUpdatesDoNotStartOrReviveMonitor() {
|
|
23
|
+
ReactApplicationContext context = mock(ReactApplicationContext.class);
|
|
24
|
+
// Exercise the real module forwarding separately: only a snapshot is installed.
|
|
25
|
+
FTRUMModule module = new FTRUMModule(context);
|
|
26
|
+
assertTrue(module.setLongTaskContext(null));
|
|
27
|
+
module.invalidate();
|
|
28
|
+
assertFalse(module.setLongTaskContext(null));
|
|
29
|
+
}
|
|
30
|
+
@Test
|
|
31
|
+
public void shutdownResolvesWhenJSQueueIsUnavailable() {
|
|
32
|
+
ReactApplicationContext context = mock(ReactApplicationContext.class);
|
|
33
|
+
FTRUMModule module = new FTRUMModule(context);
|
|
34
|
+
Promise promise = mock(Promise.class);
|
|
35
|
+
|
|
36
|
+
module.stopLongTaskTracking(promise);
|
|
37
|
+
|
|
38
|
+
verify(promise, times(1)).resolve(null);
|
|
39
|
+
verifyNoMoreInteractions(promise);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
public void shutdownResolvesWhenRNRejectsDispatch() throws Exception {
|
|
44
|
+
Method dispatch = ReactApplicationContext.class.getMethod("runOnJSQueueThread", Runnable.class);
|
|
45
|
+
// RN 0.63 has a void API. Reflection keeps this test compilable on both APIs.
|
|
46
|
+
assumeTrue(dispatch.getReturnType() == boolean.class);
|
|
47
|
+
ReactApplicationContext context = mock(ReactApplicationContext.class);
|
|
48
|
+
when(dispatch.invoke(context, any(Runnable.class))).thenReturn(false);
|
|
49
|
+
FTRUMModule module = new FTRUMModule(context);
|
|
50
|
+
Promise promise = mock(Promise.class);
|
|
51
|
+
|
|
52
|
+
module.stopLongTaskTracking(promise);
|
|
53
|
+
|
|
54
|
+
dispatch.invoke(verify(context, times(1)), any(Runnable.class));
|
|
55
|
+
verify(promise, times(1)).resolve(null);
|
|
56
|
+
verifyNoMoreInteractions(promise);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
public void shutdownResolvesWhenRNQueueIsNotInitialized() throws Exception {
|
|
61
|
+
Method dispatch = ReactApplicationContext.class.getMethod("runOnJSQueueThread", Runnable.class);
|
|
62
|
+
assumeTrue(dispatch.getReturnType() == boolean.class);
|
|
63
|
+
ReactApplicationContext context = mock(ReactApplicationContext.class);
|
|
64
|
+
when(dispatch.invoke(context, any(Runnable.class))).thenThrow(new AssertionError("JS queue is null"));
|
|
65
|
+
FTRUMModule module = new FTRUMModule(context);
|
|
66
|
+
Promise promise = mock(Promise.class);
|
|
67
|
+
|
|
68
|
+
module.stopLongTaskTracking(promise);
|
|
69
|
+
|
|
70
|
+
verify(promise, times(1)).resolve(null);
|
|
71
|
+
verifyNoMoreInteractions(promise);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
public void invalidateAndLegacyDestroyShareOneCleanup() {
|
|
76
|
+
ReactApplicationContext context = mock(ReactApplicationContext.class);
|
|
77
|
+
FTRUMModule module = new FTRUMModule(context);
|
|
78
|
+
|
|
79
|
+
module.invalidate();
|
|
80
|
+
module.invalidate();
|
|
81
|
+
module.onCatalystInstanceDestroy();
|
|
82
|
+
|
|
83
|
+
verify(context, times(1)).removeLifecycleEventListener(any(FTRUMImpl.class));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Test
|
|
87
|
+
public void legacyDestroyThenInvalidateAlsoCleansUpOnce() {
|
|
88
|
+
ReactApplicationContext context = mock(ReactApplicationContext.class);
|
|
89
|
+
FTRUMModule module = new FTRUMModule(context);
|
|
90
|
+
|
|
91
|
+
module.onCatalystInstanceDestroy();
|
|
92
|
+
module.invalidate();
|
|
93
|
+
|
|
94
|
+
verify(context, times(1)).removeLifecycleEventListener(any(FTRUMImpl.class));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
public void destroyedRUMDoesNotResumeMonitoring() {
|
|
99
|
+
ReactApplicationContext context = mock(ReactApplicationContext.class);
|
|
100
|
+
JsLongTaskMonitor.Executor executor = mock(JsLongTaskMonitor.Executor.class);
|
|
101
|
+
JsLongTaskMonitor monitor = new JsLongTaskMonitor(
|
|
102
|
+
executor,
|
|
103
|
+
mock(JsLongTaskMonitor.FrameSchedulerFactory.class),
|
|
104
|
+
mock(JsLongTaskMonitor.Reporter.class)
|
|
105
|
+
);
|
|
106
|
+
FTRUMImpl rum = new FTRUMImpl(context, monitor);
|
|
107
|
+
|
|
108
|
+
rum.destroy();
|
|
109
|
+
rum.destroy();
|
|
110
|
+
monitor.setThresholdMilliseconds(100);
|
|
111
|
+
rum.onHostResume();
|
|
112
|
+
|
|
113
|
+
verify(context, times(1)).removeLifecycleEventListener(rum);
|
|
114
|
+
verify(executor, times(1)).runOnJsThread(any(Runnable.class));
|
|
115
|
+
verifyNoMoreInteractions(executor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
package com.ft.sdk.reactnative;
|
|
2
|
+
|
|
3
|
+
import org.junit.Before;
|
|
4
|
+
import org.junit.Test;
|
|
5
|
+
|
|
6
|
+
import java.util.ArrayList;
|
|
7
|
+
import java.util.List;
|
|
8
|
+
import java.util.Map;
|
|
9
|
+
import java.util.HashMap;
|
|
10
|
+
import java.util.Collections;
|
|
11
|
+
|
|
12
|
+
import static org.junit.Assert.assertEquals;
|
|
13
|
+
import static org.junit.Assert.assertFalse;
|
|
14
|
+
import static org.junit.Assert.assertTrue;
|
|
15
|
+
|
|
16
|
+
public class JsLongTaskMonitorTest {
|
|
17
|
+
|
|
18
|
+
private FakeExecutor executor;
|
|
19
|
+
private FakeSchedulerFactory schedulerFactory;
|
|
20
|
+
private FakeReporter reporter;
|
|
21
|
+
private JsLongTaskMonitor monitor;
|
|
22
|
+
|
|
23
|
+
@Before
|
|
24
|
+
public void setUp() {
|
|
25
|
+
executor = new FakeExecutor();
|
|
26
|
+
schedulerFactory = new FakeSchedulerFactory();
|
|
27
|
+
reporter = new FakeReporter();
|
|
28
|
+
monitor = new JsLongTaskMonitor(executor, schedulerFactory, reporter);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
public void createsFrameSchedulerOnlyOnJsThread() {
|
|
33
|
+
monitor.setThresholdMilliseconds(100);
|
|
34
|
+
monitor.start();
|
|
35
|
+
|
|
36
|
+
assertEquals(0, schedulerFactory.createCount);
|
|
37
|
+
assertEquals(1, executor.runnables.size());
|
|
38
|
+
|
|
39
|
+
executor.runAll();
|
|
40
|
+
|
|
41
|
+
assertEquals(1, schedulerFactory.createCount);
|
|
42
|
+
assertEquals(1, schedulerFactory.scheduler.postCount);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
public void bridgeContextUpdatesBeforeDelayedStartAndSnapshotsEachDetection() {
|
|
47
|
+
Map<String, Object> context = new HashMap<>();
|
|
48
|
+
context.put("sdk_bridge_info", "{\"react_native\":\"test-version\"}");
|
|
49
|
+
context.put("wgt_id", "first");
|
|
50
|
+
monitor.setBridgeContext(context);
|
|
51
|
+
monitor.setThresholdMilliseconds(100);
|
|
52
|
+
monitor.start();
|
|
53
|
+
context.put("wgt_id", "second");
|
|
54
|
+
monitor.setBridgeContext(context);
|
|
55
|
+
executor.runAll();
|
|
56
|
+
schedulerFactory.scheduler.fire(1_000_000_000L);
|
|
57
|
+
schedulerFactory.scheduler.fire(1_250_000_000L);
|
|
58
|
+
Map<String, Object> firstEvent = reporter.properties.get(0);
|
|
59
|
+
Map<String, Object> expected = new HashMap<>(context);
|
|
60
|
+
context.put("wgt_id", "third");
|
|
61
|
+
monitor.setBridgeContext(context);
|
|
62
|
+
schedulerFactory.scheduler.fire(1_500_000_000L);
|
|
63
|
+
|
|
64
|
+
assertEquals(java.util.Arrays.asList(250_000_000L, 250_000_000L), reporter.durations);
|
|
65
|
+
assertEquals(expected, firstEvent);
|
|
66
|
+
assertEquals(context, reporter.properties.get(1));
|
|
67
|
+
monitor.disable(null);
|
|
68
|
+
context.put("wgt_id", "after-stop");
|
|
69
|
+
monitor.setBridgeContext(context);
|
|
70
|
+
schedulerFactory.scheduler.fire(2_000_000_000L);
|
|
71
|
+
assertEquals(2, reporter.durations.size());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
public void noBridgeContextReportsEmptyProperties() {
|
|
76
|
+
startWithThreshold(100);
|
|
77
|
+
schedulerFactory.scheduler.fire(1_000_000_000L);
|
|
78
|
+
schedulerFactory.scheduler.fire(1_250_000_000L);
|
|
79
|
+
assertEquals(250_000_000L, reporter.durations.get(0).longValue());
|
|
80
|
+
assertEquals(Collections.emptyMap(), reporter.properties.get(0));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
public void firstFrameIsBaselineAndThresholdComparisonIsStrict() {
|
|
85
|
+
startWithThreshold(100);
|
|
86
|
+
|
|
87
|
+
schedulerFactory.scheduler.fire(1_000_000_000L);
|
|
88
|
+
schedulerFactory.scheduler.fire(1_100_000_000L);
|
|
89
|
+
schedulerFactory.scheduler.fire(1_200_000_001L);
|
|
90
|
+
|
|
91
|
+
assertEquals(1, reporter.durations.size());
|
|
92
|
+
assertEquals(100_000_001L, reporter.durations.get(0).longValue());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
public void reportsEveryQualifyingFullFrameInterval() {
|
|
97
|
+
startWithThreshold(100);
|
|
98
|
+
|
|
99
|
+
schedulerFactory.scheduler.fire(1_000_000_000L);
|
|
100
|
+
schedulerFactory.scheduler.fire(1_050_000_000L);
|
|
101
|
+
schedulerFactory.scheduler.fire(1_200_000_000L);
|
|
102
|
+
schedulerFactory.scheduler.fire(1_450_000_000L);
|
|
103
|
+
|
|
104
|
+
assertEquals(2, reporter.durations.size());
|
|
105
|
+
assertEquals(150_000_000L, reporter.durations.get(0).longValue());
|
|
106
|
+
assertEquals(250_000_000L, reporter.durations.get(1).longValue());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Test
|
|
110
|
+
public void preservesFractionalMillisecondThresholds() {
|
|
111
|
+
startWithThreshold(200.5);
|
|
112
|
+
|
|
113
|
+
schedulerFactory.scheduler.fire(1_000_000_000L);
|
|
114
|
+
schedulerFactory.scheduler.fire(1_200_500_000L);
|
|
115
|
+
schedulerFactory.scheduler.fire(1_401_000_001L);
|
|
116
|
+
|
|
117
|
+
assertEquals(1, reporter.durations.size());
|
|
118
|
+
assertEquals(200_500_001L, reporter.durations.get(0).longValue());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test
|
|
122
|
+
public void stopRemovesCallbackAndRestartResetsBaseline() {
|
|
123
|
+
startWithThreshold(100);
|
|
124
|
+
FakeScheduler firstScheduler = schedulerFactory.scheduler;
|
|
125
|
+
firstScheduler.fire(1_000_000_000L);
|
|
126
|
+
|
|
127
|
+
monitor.stop();
|
|
128
|
+
firstScheduler.fire(1_500_000_000L);
|
|
129
|
+
executor.runAll();
|
|
130
|
+
|
|
131
|
+
assertEquals(1, firstScheduler.removeCount);
|
|
132
|
+
assertTrue(reporter.durations.isEmpty());
|
|
133
|
+
|
|
134
|
+
monitor.start();
|
|
135
|
+
executor.runAll();
|
|
136
|
+
FakeScheduler secondScheduler = schedulerFactory.scheduler;
|
|
137
|
+
secondScheduler.fire(2_000_000_000L);
|
|
138
|
+
secondScheduler.fire(2_100_000_001L);
|
|
139
|
+
|
|
140
|
+
assertEquals(1, reporter.durations.size());
|
|
141
|
+
assertEquals(100_000_001L, reporter.durations.get(0).longValue());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Test
|
|
145
|
+
public void repeatedStartKeepsOnlyLatestRequest() {
|
|
146
|
+
monitor.setThresholdMilliseconds(100);
|
|
147
|
+
monitor.start();
|
|
148
|
+
monitor.start();
|
|
149
|
+
|
|
150
|
+
executor.runAll();
|
|
151
|
+
|
|
152
|
+
assertEquals(1, schedulerFactory.createCount);
|
|
153
|
+
assertEquals(1, schedulerFactory.scheduler.postCount);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
public void zeroThresholdDisablesMonitoring() {
|
|
158
|
+
startWithThreshold(100);
|
|
159
|
+
FakeScheduler scheduler = schedulerFactory.scheduler;
|
|
160
|
+
|
|
161
|
+
monitor.setThresholdMilliseconds(0);
|
|
162
|
+
executor.runAll();
|
|
163
|
+
scheduler.fire(1_000_000_000L);
|
|
164
|
+
scheduler.fire(2_000_000_000L);
|
|
165
|
+
|
|
166
|
+
assertEquals(1, scheduler.removeCount);
|
|
167
|
+
assertTrue(reporter.durations.isEmpty());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@Test
|
|
171
|
+
public void schedulerFailureDoesNotCrashOrPostFrames() {
|
|
172
|
+
schedulerFactory.shouldThrow = true;
|
|
173
|
+
monitor.setThresholdMilliseconds(100);
|
|
174
|
+
monitor.start();
|
|
175
|
+
|
|
176
|
+
executor.runAll();
|
|
177
|
+
|
|
178
|
+
assertEquals(1, schedulerFactory.createCount);
|
|
179
|
+
assertFalse(schedulerFactory.scheduler.wasPosted);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Test
|
|
183
|
+
public void javascriptQueueFailureDoesNotCrash() {
|
|
184
|
+
executor.shouldThrow = true;
|
|
185
|
+
monitor.setThresholdMilliseconds(100);
|
|
186
|
+
|
|
187
|
+
monitor.start();
|
|
188
|
+
monitor.stop();
|
|
189
|
+
|
|
190
|
+
assertEquals(0, schedulerFactory.createCount);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Test
|
|
194
|
+
public void rejectedStartDoesNotCreateScheduler() {
|
|
195
|
+
executor.acceptsTasks = false;
|
|
196
|
+
monitor.setThresholdMilliseconds(100);
|
|
197
|
+
|
|
198
|
+
monitor.start();
|
|
199
|
+
|
|
200
|
+
assertEquals(0, schedulerFactory.createCount);
|
|
201
|
+
assertTrue(executor.runnables.isEmpty());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@Test
|
|
205
|
+
public void rejectedStopCompletesAndSuppressesExistingFrameCallbacks() {
|
|
206
|
+
startWithThreshold(100);
|
|
207
|
+
FakeScheduler scheduler = schedulerFactory.scheduler;
|
|
208
|
+
scheduler.fire(1_000_000_000L);
|
|
209
|
+
int posts = scheduler.postCount;
|
|
210
|
+
executor.acceptsTasks = false;
|
|
211
|
+
final int[] completions = {0};
|
|
212
|
+
|
|
213
|
+
monitor.disable(() -> completions[0]++);
|
|
214
|
+
scheduler.fire(1_500_000_000L);
|
|
215
|
+
monitor.start();
|
|
216
|
+
|
|
217
|
+
assertEquals(1, completions[0]);
|
|
218
|
+
assertEquals(posts, scheduler.postCount);
|
|
219
|
+
assertEquals(1, schedulerFactory.createCount);
|
|
220
|
+
assertTrue(reporter.durations.isEmpty());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@Test
|
|
224
|
+
public void throwingQueueCompletesStopExactlyOnce() {
|
|
225
|
+
executor.shouldThrow = true;
|
|
226
|
+
final int[] completions = {0};
|
|
227
|
+
|
|
228
|
+
monitor.stop(() -> completions[0]++);
|
|
229
|
+
|
|
230
|
+
assertEquals(1, completions[0]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@Test
|
|
234
|
+
public void inlineCompletionFailureIsNotInvokedTwice() {
|
|
235
|
+
JsLongTaskMonitor inlineMonitor = new JsLongTaskMonitor(
|
|
236
|
+
runnable -> {
|
|
237
|
+
runnable.run();
|
|
238
|
+
return true;
|
|
239
|
+
},
|
|
240
|
+
schedulerFactory,
|
|
241
|
+
reporter
|
|
242
|
+
);
|
|
243
|
+
final int[] completions = {0};
|
|
244
|
+
|
|
245
|
+
inlineMonitor.stop(() -> {
|
|
246
|
+
completions[0]++;
|
|
247
|
+
throw new IllegalStateException("Promise is already unavailable");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
assertEquals(1, completions[0]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@Test
|
|
254
|
+
public void framePostFailureDisablesMonitoring() {
|
|
255
|
+
schedulerFactory.throwOnPost = true;
|
|
256
|
+
monitor.setThresholdMilliseconds(100);
|
|
257
|
+
|
|
258
|
+
monitor.start();
|
|
259
|
+
executor.runAll();
|
|
260
|
+
|
|
261
|
+
assertEquals(1, schedulerFactory.createCount);
|
|
262
|
+
assertFalse(schedulerFactory.scheduler.wasPosted);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@Test
|
|
266
|
+
public void stopCompletionRunsAfterCallbackRemoval() {
|
|
267
|
+
startWithThreshold(100);
|
|
268
|
+
final boolean[] completed = {false};
|
|
269
|
+
|
|
270
|
+
monitor.stop(new Runnable() {
|
|
271
|
+
@Override
|
|
272
|
+
public void run() {
|
|
273
|
+
completed[0] = true;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
assertFalse(completed[0]);
|
|
278
|
+
assertEquals(0, schedulerFactory.scheduler.removeCount);
|
|
279
|
+
|
|
280
|
+
executor.runAll();
|
|
281
|
+
|
|
282
|
+
assertTrue(completed[0]);
|
|
283
|
+
assertEquals(1, schedulerFactory.scheduler.removeCount);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@Test
|
|
287
|
+
public void disablePreventsRestartUntilConfiguredAgain() {
|
|
288
|
+
startWithThreshold(100);
|
|
289
|
+
FakeScheduler firstScheduler = schedulerFactory.scheduler;
|
|
290
|
+
firstScheduler.fire(1_000_000_000L);
|
|
291
|
+
final int[] completions = {0};
|
|
292
|
+
|
|
293
|
+
monitor.disable(() -> completions[0]++);
|
|
294
|
+
// A foreground event can arrive before the JS queue finishes stopping.
|
|
295
|
+
monitor.start();
|
|
296
|
+
firstScheduler.fire(1_500_000_000L);
|
|
297
|
+
assertEquals(0, completions[0]);
|
|
298
|
+
executor.runAll();
|
|
299
|
+
|
|
300
|
+
assertEquals(1, completions[0]);
|
|
301
|
+
assertEquals(1, firstScheduler.removeCount);
|
|
302
|
+
assertTrue(reporter.durations.isEmpty());
|
|
303
|
+
|
|
304
|
+
// Further background/foreground transitions must not re-enable monitoring.
|
|
305
|
+
monitor.stop();
|
|
306
|
+
monitor.start();
|
|
307
|
+
executor.runAll();
|
|
308
|
+
assertEquals(1, schedulerFactory.createCount);
|
|
309
|
+
|
|
310
|
+
monitor.setThresholdMilliseconds(200);
|
|
311
|
+
monitor.start();
|
|
312
|
+
executor.runAll();
|
|
313
|
+
assertEquals(2, schedulerFactory.createCount);
|
|
314
|
+
schedulerFactory.scheduler.fire(10_000_000_000L);
|
|
315
|
+
schedulerFactory.scheduler.fire(10_150_000_000L);
|
|
316
|
+
assertTrue(reporter.durations.isEmpty());
|
|
317
|
+
schedulerFactory.scheduler.fire(10_400_000_000L);
|
|
318
|
+
assertEquals(1, reporter.durations.size());
|
|
319
|
+
assertEquals(250_000_000L, reporter.durations.get(0).longValue());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@Test
|
|
323
|
+
public void repeatedDisableCancelsPendingStartAndCompletesEachRequest() {
|
|
324
|
+
monitor.setThresholdMilliseconds(100);
|
|
325
|
+
monitor.start();
|
|
326
|
+
final int[] completions = {0};
|
|
327
|
+
|
|
328
|
+
monitor.disable(() -> completions[0]++);
|
|
329
|
+
monitor.disable(() -> completions[0]++);
|
|
330
|
+
monitor.start();
|
|
331
|
+
executor.runAll();
|
|
332
|
+
|
|
333
|
+
assertEquals(2, completions[0]);
|
|
334
|
+
assertEquals(0, schedulerFactory.createCount);
|
|
335
|
+
assertTrue(reporter.durations.isEmpty());
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@Test
|
|
339
|
+
public void reporterFailureDoesNotStopFrameCallbacks() {
|
|
340
|
+
reporter.shouldThrow = true;
|
|
341
|
+
startWithThreshold(100);
|
|
342
|
+
int initialPostCount = schedulerFactory.scheduler.postCount;
|
|
343
|
+
|
|
344
|
+
schedulerFactory.scheduler.fire(1_000_000_000L);
|
|
345
|
+
schedulerFactory.scheduler.fire(1_200_000_000L);
|
|
346
|
+
|
|
347
|
+
assertEquals(initialPostCount + 2, schedulerFactory.scheduler.postCount);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private void startWithThreshold(double thresholdMilliseconds) {
|
|
351
|
+
monitor.setThresholdMilliseconds(thresholdMilliseconds);
|
|
352
|
+
monitor.start();
|
|
353
|
+
executor.runAll();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private static final class FakeExecutor implements JsLongTaskMonitor.Executor {
|
|
357
|
+
private final List<Runnable> runnables = new ArrayList<>();
|
|
358
|
+
private boolean shouldThrow;
|
|
359
|
+
private boolean acceptsTasks = true;
|
|
360
|
+
|
|
361
|
+
@Override
|
|
362
|
+
public boolean runOnJsThread(Runnable runnable) {
|
|
363
|
+
if (shouldThrow) {
|
|
364
|
+
throw new IllegalStateException("JavaScript queue unavailable");
|
|
365
|
+
}
|
|
366
|
+
if (!acceptsTasks) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
runnables.add(runnable);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private void runAll() {
|
|
374
|
+
while (!runnables.isEmpty()) {
|
|
375
|
+
runnables.remove(0).run();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private static final class FakeSchedulerFactory
|
|
381
|
+
implements JsLongTaskMonitor.FrameSchedulerFactory {
|
|
382
|
+
private int createCount;
|
|
383
|
+
private boolean shouldThrow;
|
|
384
|
+
private boolean throwOnPost;
|
|
385
|
+
private FakeScheduler scheduler = new FakeScheduler();
|
|
386
|
+
|
|
387
|
+
@Override
|
|
388
|
+
public JsLongTaskMonitor.FrameScheduler create(
|
|
389
|
+
JsLongTaskMonitor.FrameCallback callback
|
|
390
|
+
) {
|
|
391
|
+
createCount++;
|
|
392
|
+
if (shouldThrow) {
|
|
393
|
+
throw new IllegalStateException("No looper");
|
|
394
|
+
}
|
|
395
|
+
scheduler = new FakeScheduler();
|
|
396
|
+
scheduler.callback = callback;
|
|
397
|
+
scheduler.throwOnPost = throwOnPost;
|
|
398
|
+
return scheduler;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private static final class FakeScheduler
|
|
403
|
+
implements JsLongTaskMonitor.FrameScheduler {
|
|
404
|
+
private JsLongTaskMonitor.FrameCallback callback;
|
|
405
|
+
private int postCount;
|
|
406
|
+
private int removeCount;
|
|
407
|
+
private boolean wasPosted;
|
|
408
|
+
private boolean throwOnPost;
|
|
409
|
+
|
|
410
|
+
@Override
|
|
411
|
+
public void postFrameCallback() {
|
|
412
|
+
if (throwOnPost) {
|
|
413
|
+
throw new IllegalStateException("Frame scheduler unavailable");
|
|
414
|
+
}
|
|
415
|
+
postCount++;
|
|
416
|
+
wasPosted = true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@Override
|
|
420
|
+
public void removeFrameCallback() {
|
|
421
|
+
removeCount++;
|
|
422
|
+
wasPosted = false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private void fire(long timestampNanos) {
|
|
426
|
+
if (callback != null) {
|
|
427
|
+
callback.doFrame(timestampNanos);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private static final class FakeReporter implements JsLongTaskMonitor.Reporter {
|
|
433
|
+
private final List<Long> durations = new ArrayList<>();
|
|
434
|
+
private final List<Map<String, Object>> properties = new ArrayList<>();
|
|
435
|
+
private boolean shouldThrow;
|
|
436
|
+
|
|
437
|
+
@Override
|
|
438
|
+
public void reportLongTask(long durationNanos, Map<String, Object> property) {
|
|
439
|
+
if (shouldThrow) {
|
|
440
|
+
throw new IllegalStateException("Reporter unavailable");
|
|
441
|
+
}
|
|
442
|
+
durations.add(durationNanos);
|
|
443
|
+
properties.add(property);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.ft.sdk.reactnative.utils;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertFalse;
|
|
4
|
+
import static org.junit.Assert.assertTrue;
|
|
5
|
+
|
|
6
|
+
import org.junit.Test;
|
|
7
|
+
|
|
8
|
+
public class ReactNativeUtilsTest {
|
|
9
|
+
|
|
10
|
+
@Test
|
|
11
|
+
public void filtersAndroidEmulatorInspectorResource() {
|
|
12
|
+
assertTrue(ReactNativeUtils.isReactNativeDevUrl(
|
|
13
|
+
"http://10.0.2.2:8081/inspector/device?name=sdk_gphone64_arm64%20-%2012%20-%20API%2032"
|
|
14
|
+
+ "&app=com.ft.sdk.reactnative.example&device=a520746d2418fde7c33ceff1d852b112040a08c0"
|
|
15
|
+
));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
public void filtersReactNativeAndExpoDevelopmentResources() {
|
|
20
|
+
assertTrue(ReactNativeUtils.isReactNativeDevUrl("http://localhost:8081/hot?platform=android"));
|
|
21
|
+
assertTrue(ReactNativeUtils.isReactNativeDevUrl("ws://10.0.2.2:8081/message?device=emulator"));
|
|
22
|
+
assertTrue(ReactNativeUtils.isReactNativeDevUrl("http://192.168.1.2:8082/logs"));
|
|
23
|
+
assertTrue(ReactNativeUtils.isReactNativeDevUrl("http://[::1]:8081/debugger-proxy?role=client"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
public void doesNotFilterApplicationResources() {
|
|
28
|
+
assertFalse(ReactNativeUtils.isReactNativeDevUrl("https://api.example.com/inspector/device"));
|
|
29
|
+
assertFalse(ReactNativeUtils.isReactNativeDevUrl("http://10.0.2.2:9529/inspector/device"));
|
|
30
|
+
assertFalse(ReactNativeUtils.isReactNativeDevUrl("http://10.0.2.2:8081/business"));
|
|
31
|
+
assertFalse(ReactNativeUtils.isReactNativeDevUrl(null));
|
|
32
|
+
}
|
|
33
|
+
}
|