@react-native-firebase/app 25.1.0 → 26.0.0
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/CHANGELOG.md +43 -0
- package/RNFBApp.podspec +7 -13
- package/android/build.gradle +25 -6
- package/android/src/reactnative/java/io/invertase/firebase/app/{ReactNativeFirebaseAppModule.java → NativeRNFBTurboApp.java} +81 -45
- package/android/src/reactnative/java/io/invertase/firebase/app/ReactNativeFirebaseAppPackage.java +3 -3
- package/android/src/reactnative/java/io/invertase/firebase/app/ReactNativeFirebaseVersion.java +2 -2
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboAppSpec.java +136 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/java/com/facebook/fbreact/specs/NativeRNFBTurboUtilsSpec.java +94 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/CMakeLists.txt +36 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/RNFBAppTurboModules-generated.cpp +170 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/RNFBAppTurboModules.h +39 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/react/renderer/components/RNFBAppTurboModules/RNFBAppTurboModulesJSI-generated.cpp +187 -0
- package/android/src/reactnative/java/io/invertase/firebase/app/generated/jni/react/renderer/components/RNFBAppTurboModules/RNFBAppTurboModulesJSI.h +606 -0
- package/android/src/reactnative/java/io/invertase/firebase/common/RCTConvertFirebase.java +3 -3
- package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitter.java +475 -20
- package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseMeta.java +8 -0
- package/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebasePreferences.java +8 -0
- package/android/src/reactnative/java/io/invertase/firebase/utils/{ReactNativeFirebaseUtilsModule.java → NativeRNFBTurboUtils.java} +57 -40
- package/android/src/test/java/io/invertase/firebase/app/NativeRNFBTurboAppTest.java +400 -0
- package/android/src/test/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitterTest.java +1823 -0
- package/app.plugin.js +1 -1
- package/dist/module/FirebaseApp.js +0 -9
- package/dist/module/FirebaseApp.js.map +1 -1
- package/dist/module/common/index.js +0 -645
- package/dist/module/common/index.js.map +1 -1
- package/dist/module/common/unitTestUtils.js +18 -54
- package/dist/module/common/unitTestUtils.js.map +1 -1
- package/dist/module/index.js +0 -1
- package/dist/module/index.js.map +1 -1
- package/dist/module/internal/RNFBNativeEventEmitter.js +12 -6
- package/dist/module/internal/RNFBNativeEventEmitter.js.map +1 -1
- package/dist/module/internal/constants.js +2 -2
- package/dist/module/internal/constants.js.map +1 -1
- package/dist/module/internal/index.js +1 -1
- package/dist/module/internal/index.js.map +1 -1
- package/dist/module/internal/nativeModuleAndroidIos.js +50 -10
- package/dist/module/internal/nativeModuleAndroidIos.js.map +1 -1
- package/dist/module/internal/nativeModuleWeb.js +12 -4
- package/dist/module/internal/nativeModuleWeb.js.map +1 -1
- package/dist/module/internal/registry/app.js +20 -12
- package/dist/module/internal/registry/app.js.map +1 -1
- package/dist/module/internal/registry/modular.js +73 -0
- package/dist/module/internal/registry/modular.js.map +1 -0
- package/dist/module/internal/registry/nativeModule.js +116 -95
- package/dist/module/internal/registry/nativeModule.js.map +1 -1
- package/dist/module/modular.js +44 -37
- package/dist/module/modular.js.map +1 -1
- package/dist/module/types/app.js +0 -3
- package/dist/module/types/app.js.map +1 -1
- package/dist/module/types/internal.js +0 -2
- package/dist/module/utils/UtilsStatics.js +2 -2
- package/dist/module/utils/UtilsStatics.js.map +1 -1
- package/dist/module/utils/index.js +21 -17
- package/dist/module/utils/index.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/lib/FirebaseApp.d.ts +1 -2
- package/dist/typescript/lib/FirebaseApp.d.ts.map +1 -1
- package/dist/typescript/lib/common/index.d.ts +0 -8
- package/dist/typescript/lib/common/index.d.ts.map +1 -1
- package/dist/typescript/lib/common/unitTestUtils.d.ts +1 -4
- package/dist/typescript/lib/common/unitTestUtils.d.ts.map +1 -1
- package/dist/typescript/lib/index.d.ts +0 -1
- package/dist/typescript/lib/index.d.ts.map +1 -1
- package/dist/typescript/lib/internal/NativeFirebaseError.d.ts +1 -1
- package/dist/typescript/lib/internal/NativeFirebaseError.d.ts.map +1 -1
- package/dist/typescript/lib/internal/RNFBNativeEventEmitter.d.ts.map +1 -1
- package/dist/typescript/lib/internal/constants.d.ts +2 -3
- package/dist/typescript/lib/internal/constants.d.ts.map +1 -1
- package/dist/typescript/lib/internal/index.d.ts +1 -1
- package/dist/typescript/lib/internal/index.d.ts.map +1 -1
- package/dist/typescript/lib/internal/nativeModuleAndroidIos.d.ts +2 -6
- package/dist/typescript/lib/internal/nativeModuleAndroidIos.d.ts.map +1 -1
- package/dist/typescript/lib/internal/nativeModuleWeb.d.ts.map +1 -1
- package/dist/typescript/lib/internal/registry/app.d.ts +2 -2
- package/dist/typescript/lib/internal/registry/app.d.ts.map +1 -1
- package/dist/typescript/lib/internal/registry/modular.d.ts +22 -0
- package/dist/typescript/lib/internal/registry/modular.d.ts.map +1 -0
- package/dist/typescript/lib/internal/registry/nativeModule.d.ts +3 -11
- package/dist/typescript/lib/internal/registry/nativeModule.d.ts.map +1 -1
- package/dist/typescript/lib/modular.d.ts +21 -7
- package/dist/typescript/lib/modular.d.ts.map +1 -1
- package/dist/typescript/lib/types/app.d.ts +4 -4
- package/dist/typescript/lib/types/app.d.ts.map +1 -1
- package/dist/typescript/lib/types/internal.d.ts +1 -33
- package/dist/typescript/lib/types/internal.d.ts.map +1 -1
- package/dist/typescript/lib/utils/index.d.ts +7 -3
- package/dist/typescript/lib/utils/index.d.ts.map +1 -1
- package/dist/typescript/lib/version.d.ts +1 -1
- package/firebase-schema.json +5 -1
- package/ios/RNFBApp/RNFBAppModule.h +1 -3
- package/ios/RNFBApp/{RNFBAppModule.m → RNFBAppModule.mm} +74 -77
- package/ios/RNFBApp/RNFBUtilsModule.h +1 -3
- package/ios/RNFBApp/{RNFBUtilsModule.m → RNFBUtilsModule.mm} +68 -25
- package/ios/RNFBApp/RNFBVersion.m +1 -1
- package/ios/RNFBApp.xcodeproj/project.pbxproj +12 -12
- package/ios/generated/RCTAppDependencyProvider.h +25 -0
- package/ios/generated/RCTAppDependencyProvider.mm +55 -0
- package/ios/generated/RCTModuleProviders.h +16 -0
- package/ios/generated/RCTModuleProviders.mm +52 -0
- package/ios/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
- package/ios/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
- package/ios/generated/RCTThirdPartyComponentsProvider.h +16 -0
- package/ios/generated/RCTThirdPartyComponentsProvider.mm +23 -0
- package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.h +14 -0
- package/ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.mm +19 -0
- package/ios/generated/RNFBAppTurboModules/RNFBAppTurboModules-generated.mm +210 -0
- package/ios/generated/RNFBAppTurboModules/RNFBAppTurboModules.h +399 -0
- package/ios/generated/RNFBAppTurboModulesJSI-generated.cpp +187 -0
- package/ios/generated/RNFBAppTurboModulesJSI.h +606 -0
- package/ios/generated/ReactAppDependencyProvider.podspec +34 -0
- package/ios/generated/ReactCodegen.podspec +111 -0
- package/lib/FirebaseApp.ts +1 -10
- package/lib/common/index.ts +0 -742
- package/lib/common/unitTestUtils.ts +17 -73
- package/lib/index.ts +0 -1
- package/lib/internal/NativeFirebaseError.ts +1 -1
- package/lib/internal/RNFBNativeEventEmitter.ts +12 -6
- package/lib/internal/constants.ts +3 -25
- package/lib/internal/global.d.ts +0 -4
- package/lib/internal/index.ts +1 -1
- package/lib/internal/nativeModuleAndroidIos.ts +69 -12
- package/lib/internal/nativeModuleWeb.ts +11 -5
- package/lib/internal/registry/app.ts +24 -22
- package/lib/internal/registry/modular.ts +97 -0
- package/lib/internal/registry/nativeModule.ts +136 -107
- package/lib/modular.ts +47 -72
- package/lib/types/app.ts +6 -11
- package/lib/types/internal.ts +1 -46
- package/lib/utils/UtilsStatics.ts +2 -2
- package/lib/utils/index.ts +24 -18
- package/lib/version.ts +1 -1
- package/package.json +41 -3
- package/plugin/build/app.plugin.d.ts +2 -0
- package/plugin/build/app.plugin.js +6 -0
- package/plugin/src/app.plugin.ts +3 -0
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/react-native.config.js +2 -0
- package/specs/NativeRNFBTurboApp.ts +57 -0
- package/specs/NativeRNFBTurboUtils.ts +34 -0
- package/typedoc.json +1 -2
- package/dist/module/internal/registry/namespace.js +0 -262
- package/dist/module/internal/registry/namespace.js.map +0 -1
- package/dist/module/namespaced.js +0 -25
- package/dist/module/namespaced.js.map +0 -1
- package/dist/typescript/lib/internal/registry/namespace.d.ts +0 -26
- package/dist/typescript/lib/internal/registry/namespace.d.ts.map +0 -1
- package/dist/typescript/lib/namespaced.d.ts +0 -5
- package/dist/typescript/lib/namespaced.d.ts.map +0 -1
- package/lib/internal/registry/namespace.ts +0 -338
- package/lib/namespaced.ts +0 -24
package/android/src/test/java/io/invertase/firebase/common/ReactNativeFirebaseEventEmitterTest.java
ADDED
|
@@ -0,0 +1,1823 @@
|
|
|
1
|
+
package io.invertase.firebase.common;
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2016-present Invertase Limited & Contributors
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this library except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import static org.junit.Assert.assertEquals;
|
|
21
|
+
import static org.junit.Assert.assertFalse;
|
|
22
|
+
import static org.junit.Assert.assertNull;
|
|
23
|
+
import static org.junit.Assert.assertTrue;
|
|
24
|
+
import static org.mockito.ArgumentMatchers.anyString;
|
|
25
|
+
import static org.mockito.ArgumentMatchers.eq;
|
|
26
|
+
import static org.mockito.Mockito.doThrow;
|
|
27
|
+
import static org.mockito.Mockito.mock;
|
|
28
|
+
import static org.mockito.Mockito.mockStatic;
|
|
29
|
+
import static org.mockito.Mockito.never;
|
|
30
|
+
import static org.mockito.Mockito.verify;
|
|
31
|
+
import static org.mockito.Mockito.when;
|
|
32
|
+
|
|
33
|
+
import android.app.Application;
|
|
34
|
+
import android.content.Context;
|
|
35
|
+
import androidx.annotation.Nullable;
|
|
36
|
+
import com.facebook.react.ReactApplication;
|
|
37
|
+
import com.facebook.react.ReactHost;
|
|
38
|
+
import com.facebook.react.ReactInstanceManager;
|
|
39
|
+
import com.facebook.react.ReactNativeHost;
|
|
40
|
+
import com.facebook.react.bridge.Arguments;
|
|
41
|
+
import com.facebook.react.bridge.ReactContext;
|
|
42
|
+
import com.facebook.react.bridge.WritableMap;
|
|
43
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
44
|
+
import io.invertase.firebase.app.ReactNativeFirebaseApp;
|
|
45
|
+
import io.invertase.firebase.interfaces.NativeEvent;
|
|
46
|
+
import java.lang.ref.WeakReference;
|
|
47
|
+
import java.lang.reflect.Constructor;
|
|
48
|
+
import java.lang.reflect.Field;
|
|
49
|
+
import java.lang.reflect.Method;
|
|
50
|
+
import java.util.HashMap;
|
|
51
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
52
|
+
import org.junit.Before;
|
|
53
|
+
import org.junit.Test;
|
|
54
|
+
import org.junit.runner.RunWith;
|
|
55
|
+
import org.mockito.MockedStatic;
|
|
56
|
+
import org.robolectric.RobolectricTestRunner;
|
|
57
|
+
import org.robolectric.annotation.Config;
|
|
58
|
+
import org.robolectric.shadows.ShadowLooper;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* JVM coverage for {@link ReactNativeFirebaseEventEmitter} ReactContext generation overlap.
|
|
62
|
+
*
|
|
63
|
+
* <p>Uses Robolectric for {@link android.os.Handler}/{@link android.os.Looper} and Mockito for RN
|
|
64
|
+
* host/context doubles. The contracts under test are Java state-machine behaviour and do not
|
|
65
|
+
* require the Detox/Jet harness.
|
|
66
|
+
*/
|
|
67
|
+
@RunWith(RobolectricTestRunner.class)
|
|
68
|
+
@Config(sdk = 34, application = ReactNativeFirebaseEventEmitterTest.HostApplication.class)
|
|
69
|
+
public class ReactNativeFirebaseEventEmitterTest {
|
|
70
|
+
|
|
71
|
+
private static final String EVENT = "auth_state_changed";
|
|
72
|
+
|
|
73
|
+
private HostApplication application;
|
|
74
|
+
private ReactHost reactHost;
|
|
75
|
+
private ReactContext staleContext;
|
|
76
|
+
private ReactContext liveContext;
|
|
77
|
+
private DeviceEventManagerModule.RCTDeviceEventEmitter staleJsEmitter;
|
|
78
|
+
private DeviceEventManagerModule.RCTDeviceEventEmitter liveJsEmitter;
|
|
79
|
+
private WritableMap eventBody;
|
|
80
|
+
|
|
81
|
+
public static class HostApplication extends Application implements ReactApplication {
|
|
82
|
+
ReactHost reactHost;
|
|
83
|
+
ReactNativeHost reactNativeHost;
|
|
84
|
+
|
|
85
|
+
@Override
|
|
86
|
+
public ReactNativeHost getReactNativeHost() {
|
|
87
|
+
if (reactNativeHost == null) {
|
|
88
|
+
throw new RuntimeException("bridgeless-only test host");
|
|
89
|
+
}
|
|
90
|
+
return reactNativeHost;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Override
|
|
94
|
+
public ReactHost getReactHost() {
|
|
95
|
+
return reactHost;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Before
|
|
100
|
+
public void setUp() throws Exception {
|
|
101
|
+
application = (HostApplication) org.robolectric.RuntimeEnvironment.getApplication();
|
|
102
|
+
reactHost = mock(ReactHost.class);
|
|
103
|
+
application.reactHost = reactHost;
|
|
104
|
+
application.reactNativeHost = null;
|
|
105
|
+
|
|
106
|
+
staleContext = mock(ReactContext.class);
|
|
107
|
+
liveContext = mock(ReactContext.class);
|
|
108
|
+
staleJsEmitter = mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
109
|
+
liveJsEmitter = mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
110
|
+
eventBody = mock(WritableMap.class);
|
|
111
|
+
|
|
112
|
+
when(staleContext.getApplicationContext()).thenReturn(application);
|
|
113
|
+
when(liveContext.getApplicationContext()).thenReturn(application);
|
|
114
|
+
// Bridgeless stale contexts still report an active instance via host-level state.
|
|
115
|
+
when(staleContext.hasActiveReactInstance()).thenReturn(true);
|
|
116
|
+
when(liveContext.hasActiveReactInstance()).thenReturn(true);
|
|
117
|
+
when(staleContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
118
|
+
.thenReturn(staleJsEmitter);
|
|
119
|
+
when(liveContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
120
|
+
.thenReturn(liveJsEmitter);
|
|
121
|
+
|
|
122
|
+
resetSharedInstance();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Emit must target the host's current ReactContext, not a previously attached generation that no
|
|
127
|
+
* longer hosts JS listeners.
|
|
128
|
+
*/
|
|
129
|
+
@Test
|
|
130
|
+
public void emit_deliversToHostCurrentContext_whenAttachedContextIsStale() {
|
|
131
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
132
|
+
|
|
133
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
134
|
+
emitter.attachReactContext(staleContext);
|
|
135
|
+
ShadowLooper.idleMainLooper();
|
|
136
|
+
|
|
137
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
138
|
+
|
|
139
|
+
emitter.notifyJsReady(true);
|
|
140
|
+
ShadowLooper.idleMainLooper();
|
|
141
|
+
emitter.addListener(EVENT);
|
|
142
|
+
ShadowLooper.idleMainLooper();
|
|
143
|
+
|
|
144
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
145
|
+
ShadowLooper.idleMainLooper();
|
|
146
|
+
|
|
147
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
148
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* When the host cannot name a current context (reload gap), a late attach from a dying generation
|
|
153
|
+
* must not displace the live attachment, and a subsequent detach of that dying context must not
|
|
154
|
+
* clear live listener / jsReady state.
|
|
155
|
+
*/
|
|
156
|
+
@Test
|
|
157
|
+
public void attachDetach_preservesLiveListeners_whenHostCurrentNullAndStaleOverlaps()
|
|
158
|
+
throws Exception {
|
|
159
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
160
|
+
|
|
161
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
162
|
+
emitter.attachReactContext(liveContext);
|
|
163
|
+
ShadowLooper.idleMainLooper();
|
|
164
|
+
|
|
165
|
+
emitter.notifyJsReady(true);
|
|
166
|
+
ShadowLooper.idleMainLooper();
|
|
167
|
+
emitter.addListener(EVENT);
|
|
168
|
+
ShadowLooper.idleMainLooper();
|
|
169
|
+
|
|
170
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
171
|
+
assertTrue(jsReady(emitter));
|
|
172
|
+
|
|
173
|
+
when(reactHost.getCurrentReactContext()).thenReturn(null);
|
|
174
|
+
emitter.attachReactContext(staleContext);
|
|
175
|
+
ShadowLooper.idleMainLooper();
|
|
176
|
+
|
|
177
|
+
emitter.detachReactContext(staleContext);
|
|
178
|
+
ShadowLooper.idleMainLooper();
|
|
179
|
+
|
|
180
|
+
assertEquals(
|
|
181
|
+
"live jsListeners must survive late stale attach+detach when host current is null",
|
|
182
|
+
1,
|
|
183
|
+
listenerCount(emitter, EVENT));
|
|
184
|
+
assertTrue(
|
|
185
|
+
"jsReady must survive late stale attach+detach when host current is null",
|
|
186
|
+
jsReady(emitter));
|
|
187
|
+
assertEquals(
|
|
188
|
+
"attached context must remain the live generation", liveContext, attachedContext(emitter));
|
|
189
|
+
assertDiagnostics(emitter, /* jsReady= */ true, liveContext, /* current= */ null);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Live module initialize while the host still names the dying context must converge: pending
|
|
194
|
+
* attach is promoted on dying detach, listeners registered by the live JS survive, and emit
|
|
195
|
+
* prefers the promoted attachment over the lagging host current.
|
|
196
|
+
*/
|
|
197
|
+
@Test
|
|
198
|
+
public void attachDetachEmit_convergesToLiveContext_whenHostLagsOnDyingGeneration()
|
|
199
|
+
throws Exception {
|
|
200
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
201
|
+
|
|
202
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
203
|
+
emitter.attachReactContext(staleContext);
|
|
204
|
+
ShadowLooper.idleMainLooper();
|
|
205
|
+
|
|
206
|
+
emitter.attachReactContext(liveContext);
|
|
207
|
+
ShadowLooper.idleMainLooper();
|
|
208
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
209
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
210
|
+
|
|
211
|
+
emitter.notifyJsReady(true);
|
|
212
|
+
ShadowLooper.idleMainLooper();
|
|
213
|
+
emitter.addListener(EVENT);
|
|
214
|
+
ShadowLooper.idleMainLooper();
|
|
215
|
+
|
|
216
|
+
emitter.detachReactContext(staleContext);
|
|
217
|
+
ShadowLooper.idleMainLooper();
|
|
218
|
+
|
|
219
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
220
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
221
|
+
assertTrue(jsReady(emitter));
|
|
222
|
+
|
|
223
|
+
// Host still names dying generation; emit must not deliver into it.
|
|
224
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
225
|
+
ShadowLooper.idleMainLooper();
|
|
226
|
+
|
|
227
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
228
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
229
|
+
|
|
230
|
+
assertDiagnostics(emitter, /* jsReady= */ true, liveContext, staleContext);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* After the live generation has converged ahead of the host, a late attach of the lagging host
|
|
235
|
+
* current must not overwrite the live attachment.
|
|
236
|
+
*/
|
|
237
|
+
@Test
|
|
238
|
+
public void attach_rejectsHostLagContext_afterLiveGenerationPromoted() throws Exception {
|
|
239
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
240
|
+
|
|
241
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
242
|
+
emitter.attachReactContext(staleContext);
|
|
243
|
+
ShadowLooper.idleMainLooper();
|
|
244
|
+
emitter.attachReactContext(liveContext);
|
|
245
|
+
ShadowLooper.idleMainLooper();
|
|
246
|
+
emitter.notifyJsReady(true);
|
|
247
|
+
emitter.addListener(EVENT);
|
|
248
|
+
ShadowLooper.idleMainLooper();
|
|
249
|
+
emitter.detachReactContext(staleContext);
|
|
250
|
+
ShadowLooper.idleMainLooper();
|
|
251
|
+
|
|
252
|
+
emitter.attachReactContext(staleContext);
|
|
253
|
+
ShadowLooper.idleMainLooper();
|
|
254
|
+
|
|
255
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
256
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
257
|
+
assertTrue(jsReady(emitter));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* When the host catches up to a pending live context before the dying generation detaches,
|
|
262
|
+
* attachment converges without wiping listener registrations already made by live JS.
|
|
263
|
+
*/
|
|
264
|
+
@Test
|
|
265
|
+
public void attach_convergesPending_whenHostCatchesUpBeforeDyingDetach() throws Exception {
|
|
266
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
267
|
+
|
|
268
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
269
|
+
emitter.attachReactContext(staleContext);
|
|
270
|
+
ShadowLooper.idleMainLooper();
|
|
271
|
+
emitter.attachReactContext(liveContext);
|
|
272
|
+
ShadowLooper.idleMainLooper();
|
|
273
|
+
|
|
274
|
+
emitter.notifyJsReady(true);
|
|
275
|
+
emitter.addListener(EVENT);
|
|
276
|
+
ShadowLooper.idleMainLooper();
|
|
277
|
+
|
|
278
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
279
|
+
emitter.attachReactContext(liveContext);
|
|
280
|
+
ShadowLooper.idleMainLooper();
|
|
281
|
+
|
|
282
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
283
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
284
|
+
assertTrue(jsReady(emitter));
|
|
285
|
+
assertEquals(null, pendingContext(emitter));
|
|
286
|
+
|
|
287
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
288
|
+
ShadowLooper.idleMainLooper();
|
|
289
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A confirmed host switch onto a new context resets listener and jsReady accounting that belonged
|
|
294
|
+
* to the previous generation.
|
|
295
|
+
*/
|
|
296
|
+
@Test
|
|
297
|
+
public void attach_resetsListenerState_onHostConfirmedContextSwitch() throws Exception {
|
|
298
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
299
|
+
|
|
300
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
301
|
+
emitter.attachReactContext(staleContext);
|
|
302
|
+
ShadowLooper.idleMainLooper();
|
|
303
|
+
emitter.notifyJsReady(true);
|
|
304
|
+
emitter.addListener(EVENT);
|
|
305
|
+
ShadowLooper.idleMainLooper();
|
|
306
|
+
|
|
307
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
308
|
+
emitter.attachReactContext(liveContext);
|
|
309
|
+
ShadowLooper.idleMainLooper();
|
|
310
|
+
|
|
311
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
312
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
313
|
+
assertFalse(jsReady(emitter));
|
|
314
|
+
assertDiagnostics(emitter, /* jsReady= */ false, liveContext, liveContext);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Dying detach after the pointer already moved to the live generation is a no-op for listener
|
|
319
|
+
* state.
|
|
320
|
+
*/
|
|
321
|
+
@Test
|
|
322
|
+
public void detach_isNoOp_whenAttachedPointerAlreadyMoved() throws Exception {
|
|
323
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
324
|
+
|
|
325
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
326
|
+
emitter.attachReactContext(staleContext);
|
|
327
|
+
ShadowLooper.idleMainLooper();
|
|
328
|
+
// Force live attach while host already names live (confirmed switch).
|
|
329
|
+
emitter.attachReactContext(liveContext);
|
|
330
|
+
ShadowLooper.idleMainLooper();
|
|
331
|
+
emitter.notifyJsReady(true);
|
|
332
|
+
emitter.addListener(EVENT);
|
|
333
|
+
ShadowLooper.idleMainLooper();
|
|
334
|
+
|
|
335
|
+
emitter.detachReactContext(staleContext);
|
|
336
|
+
ShadowLooper.idleMainLooper();
|
|
337
|
+
|
|
338
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
339
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
340
|
+
assertTrue(jsReady(emitter));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* When ReactHost is present but reports a null current context, resolution falls through to
|
|
345
|
+
* ReactNativeHost so hybrid hosts still participate in arbitration.
|
|
346
|
+
*/
|
|
347
|
+
@Test
|
|
348
|
+
public void hostResolution_fallsThroughToReactNativeHost_whenReactHostCurrentNull()
|
|
349
|
+
throws Exception {
|
|
350
|
+
ReactNativeHost reactNativeHost = mock(ReactNativeHost.class);
|
|
351
|
+
ReactInstanceManager instanceManager = mock(ReactInstanceManager.class);
|
|
352
|
+
application.reactNativeHost = reactNativeHost;
|
|
353
|
+
when(reactHost.getCurrentReactContext()).thenReturn(null);
|
|
354
|
+
when(reactNativeHost.hasInstance()).thenReturn(true);
|
|
355
|
+
when(reactNativeHost.getReactInstanceManager()).thenReturn(instanceManager);
|
|
356
|
+
when(instanceManager.getCurrentReactContext()).thenReturn(liveContext);
|
|
357
|
+
|
|
358
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
359
|
+
emitter.attachReactContext(staleContext);
|
|
360
|
+
ShadowLooper.idleMainLooper();
|
|
361
|
+
emitter.notifyJsReady(true);
|
|
362
|
+
emitter.addListener(EVENT);
|
|
363
|
+
ShadowLooper.idleMainLooper();
|
|
364
|
+
|
|
365
|
+
// Confirmed switch via bridge host fallthrough.
|
|
366
|
+
emitter.attachReactContext(liveContext);
|
|
367
|
+
ShadowLooper.idleMainLooper();
|
|
368
|
+
|
|
369
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
370
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
371
|
+
assertFalse(jsReady(emitter));
|
|
372
|
+
assertDiagnostics(emitter, /* jsReady= */ false, liveContext, liveContext);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Emit prefers the pending live context while the host/attached pointer still names the dying
|
|
377
|
+
* generation, without waiting for dying detach.
|
|
378
|
+
*/
|
|
379
|
+
@Test
|
|
380
|
+
public void emit_deliversToPendingLiveContext_whileHostStillOnDyingGeneration() {
|
|
381
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
382
|
+
|
|
383
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
384
|
+
emitter.attachReactContext(staleContext);
|
|
385
|
+
ShadowLooper.idleMainLooper();
|
|
386
|
+
emitter.attachReactContext(liveContext);
|
|
387
|
+
ShadowLooper.idleMainLooper();
|
|
388
|
+
|
|
389
|
+
emitter.notifyJsReady(true);
|
|
390
|
+
emitter.addListener(EVENT);
|
|
391
|
+
ShadowLooper.idleMainLooper();
|
|
392
|
+
|
|
393
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
394
|
+
ShadowLooper.idleMainLooper();
|
|
395
|
+
|
|
396
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
397
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Dying same-context re-attach (resume) while the host still names the dying generation must not
|
|
402
|
+
* wipe a different pending live replacement — otherwise dying detach later resets listeners for
|
|
403
|
+
* the rest of the session (#8374-class).
|
|
404
|
+
*/
|
|
405
|
+
@Test
|
|
406
|
+
public void attach_preservesPending_whenDyingSameContextReattachWhileHostLags() throws Exception {
|
|
407
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
408
|
+
|
|
409
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
410
|
+
emitter.attachReactContext(staleContext);
|
|
411
|
+
ShadowLooper.idleMainLooper();
|
|
412
|
+
emitter.attachReactContext(liveContext);
|
|
413
|
+
ShadowLooper.idleMainLooper();
|
|
414
|
+
|
|
415
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
416
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
417
|
+
|
|
418
|
+
emitter.notifyJsReady(true);
|
|
419
|
+
emitter.addListener(EVENT);
|
|
420
|
+
ShadowLooper.idleMainLooper();
|
|
421
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
422
|
+
|
|
423
|
+
// Dying generation resume / re-attach while host still names it.
|
|
424
|
+
emitter.attachReactContext(staleContext);
|
|
425
|
+
ShadowLooper.idleMainLooper();
|
|
426
|
+
|
|
427
|
+
assertEquals(
|
|
428
|
+
"pending live replacement must survive dying same-context re-attach",
|
|
429
|
+
liveContext,
|
|
430
|
+
pendingContext(emitter));
|
|
431
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
432
|
+
assertEquals(staleContext, hostLagContext(emitter));
|
|
433
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
434
|
+
assertTrue(jsReady(emitter));
|
|
435
|
+
|
|
436
|
+
emitter.detachReactContext(staleContext);
|
|
437
|
+
ShadowLooper.idleMainLooper();
|
|
438
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
439
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
440
|
+
assertTrue(jsReady(emitter));
|
|
441
|
+
|
|
442
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
443
|
+
ShadowLooper.idleMainLooper();
|
|
444
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
445
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Pending-only invalidate must cancel the pending slot rather than early-returning on the
|
|
450
|
+
* attached-identity check, and must restore the surviving attached generation's listener /
|
|
451
|
+
* jsReady snapshot taken when pending first entered — otherwise the dying JS (which already
|
|
452
|
+
* registered once) leaves the attached runtime deaf for the rest of the session.
|
|
453
|
+
*/
|
|
454
|
+
@Test
|
|
455
|
+
public void detach_restoresAttachedListeners_whenPendingOnlyCancelled() throws Exception {
|
|
456
|
+
ReactContext doomedPending = mock(ReactContext.class);
|
|
457
|
+
DeviceEventManagerModule.RCTDeviceEventEmitter doomedJsEmitter =
|
|
458
|
+
mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
459
|
+
when(doomedPending.getApplicationContext()).thenReturn(application);
|
|
460
|
+
when(doomedPending.hasActiveReactInstance()).thenReturn(true);
|
|
461
|
+
when(doomedPending.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
462
|
+
.thenReturn(doomedJsEmitter);
|
|
463
|
+
|
|
464
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
465
|
+
|
|
466
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
467
|
+
emitter.attachReactContext(staleContext);
|
|
468
|
+
ShadowLooper.idleMainLooper();
|
|
469
|
+
|
|
470
|
+
// Attached generation registers before pending enters.
|
|
471
|
+
emitter.notifyJsReady(true);
|
|
472
|
+
emitter.addListener(EVENT);
|
|
473
|
+
ShadowLooper.idleMainLooper();
|
|
474
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
475
|
+
assertTrue(jsReady(emitter));
|
|
476
|
+
|
|
477
|
+
emitter.attachReactContext(doomedPending);
|
|
478
|
+
ShadowLooper.idleMainLooper();
|
|
479
|
+
|
|
480
|
+
assertEquals(doomedPending, pendingContext(emitter));
|
|
481
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
482
|
+
assertEquals(
|
|
483
|
+
"pending entry resets live map so events queue until pending JS re-registers",
|
|
484
|
+
0,
|
|
485
|
+
listenerCount(emitter, EVENT));
|
|
486
|
+
assertFalse(jsReady(emitter));
|
|
487
|
+
|
|
488
|
+
// Pending JS registers its own accounting (must be discarded on cancel, not kept).
|
|
489
|
+
emitter.notifyJsReady(true);
|
|
490
|
+
emitter.addListener(EVENT);
|
|
491
|
+
ShadowLooper.idleMainLooper();
|
|
492
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
493
|
+
|
|
494
|
+
emitter.detachReactContext(doomedPending);
|
|
495
|
+
ShadowLooper.idleMainLooper();
|
|
496
|
+
|
|
497
|
+
assertEquals(null, pendingContext(emitter));
|
|
498
|
+
assertEquals(null, hostLagContext(emitter));
|
|
499
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
500
|
+
assertEquals(
|
|
501
|
+
"pending-only cancel must restore attached generation listener accounting",
|
|
502
|
+
1,
|
|
503
|
+
listenerCount(emitter, EVENT));
|
|
504
|
+
assertTrue("pending-only cancel must restore attached generation jsReady", jsReady(emitter));
|
|
505
|
+
|
|
506
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
507
|
+
ShadowLooper.idleMainLooper();
|
|
508
|
+
verify(staleJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
509
|
+
verify(doomedJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Events raised while pending entry wiped attached listeners are queued. Pending-only cancel must
|
|
514
|
+
* restore accounting and flush the queue — attached JS will not re-register.
|
|
515
|
+
*/
|
|
516
|
+
@Test
|
|
517
|
+
public void detach_flushesQueuedEvents_whenPendingOnlyCancelledAfterQueueDuringWipe()
|
|
518
|
+
throws Exception {
|
|
519
|
+
ReactContext doomedPending = mock(ReactContext.class);
|
|
520
|
+
DeviceEventManagerModule.RCTDeviceEventEmitter doomedJsEmitter =
|
|
521
|
+
mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
522
|
+
when(doomedPending.getApplicationContext()).thenReturn(application);
|
|
523
|
+
when(doomedPending.hasActiveReactInstance()).thenReturn(true);
|
|
524
|
+
when(doomedPending.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
525
|
+
.thenReturn(doomedJsEmitter);
|
|
526
|
+
|
|
527
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
528
|
+
|
|
529
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
530
|
+
emitter.attachReactContext(staleContext);
|
|
531
|
+
ShadowLooper.idleMainLooper();
|
|
532
|
+
|
|
533
|
+
emitter.notifyJsReady(true);
|
|
534
|
+
emitter.addListener(EVENT);
|
|
535
|
+
ShadowLooper.idleMainLooper();
|
|
536
|
+
|
|
537
|
+
emitter.attachReactContext(doomedPending);
|
|
538
|
+
ShadowLooper.idleMainLooper();
|
|
539
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
540
|
+
assertFalse(jsReady(emitter));
|
|
541
|
+
|
|
542
|
+
// Queued during the wiped pending window (messaging-style / #8374-class).
|
|
543
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
544
|
+
ShadowLooper.idleMainLooper();
|
|
545
|
+
assertEquals(1, queuedCount(emitter));
|
|
546
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
547
|
+
verify(doomedJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
548
|
+
|
|
549
|
+
emitter.detachReactContext(doomedPending);
|
|
550
|
+
ShadowLooper.idleMainLooper();
|
|
551
|
+
|
|
552
|
+
assertEquals(0, queuedCount(emitter));
|
|
553
|
+
verify(staleJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
554
|
+
verify(doomedJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Same pending identity re-attach while the host still names the dying generation (async
|
|
559
|
+
* onHostResume / NativeRNFBTurboApp) must not resetListenerState — otherwise live JS
|
|
560
|
+
* registrations made after the first pending entry are wiped for the rest of the session
|
|
561
|
+
* (#8374-class).
|
|
562
|
+
*/
|
|
563
|
+
@Test
|
|
564
|
+
public void attach_preservesPendingListeners_whenSamePendingReattachWhileHostLags()
|
|
565
|
+
throws Exception {
|
|
566
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
567
|
+
|
|
568
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
569
|
+
emitter.attachReactContext(staleContext);
|
|
570
|
+
ShadowLooper.idleMainLooper();
|
|
571
|
+
emitter.attachReactContext(liveContext);
|
|
572
|
+
ShadowLooper.idleMainLooper();
|
|
573
|
+
|
|
574
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
575
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
576
|
+
|
|
577
|
+
emitter.notifyJsReady(true);
|
|
578
|
+
emitter.addListener(EVENT);
|
|
579
|
+
ShadowLooper.idleMainLooper();
|
|
580
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
581
|
+
assertTrue(jsReady(emitter));
|
|
582
|
+
|
|
583
|
+
// Same pending identity re-attach while host still lags (e.g. onHostResume).
|
|
584
|
+
emitter.attachReactContext(liveContext);
|
|
585
|
+
ShadowLooper.idleMainLooper();
|
|
586
|
+
|
|
587
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
588
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
589
|
+
assertEquals(staleContext, hostLagContext(emitter));
|
|
590
|
+
assertEquals(
|
|
591
|
+
"same-pending re-attach must not wipe listeners registered after first pending entry",
|
|
592
|
+
1,
|
|
593
|
+
listenerCount(emitter, EVENT));
|
|
594
|
+
assertTrue(
|
|
595
|
+
"same-pending re-attach must not clear jsReady registered after first pending entry",
|
|
596
|
+
jsReady(emitter));
|
|
597
|
+
|
|
598
|
+
emitter.detachReactContext(staleContext);
|
|
599
|
+
ShadowLooper.idleMainLooper();
|
|
600
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
601
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
602
|
+
assertTrue(jsReady(emitter));
|
|
603
|
+
|
|
604
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
605
|
+
ShadowLooper.idleMainLooper();
|
|
606
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
607
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Detach of a superseded (non-current) pending identity must not wipe listener / jsReady state
|
|
612
|
+
* owned by a newer live pending candidate.
|
|
613
|
+
*/
|
|
614
|
+
@Test
|
|
615
|
+
public void detach_doesNotClearListeners_whenSupersededPendingDetaches() throws Exception {
|
|
616
|
+
ReactContext middlePending = mock(ReactContext.class);
|
|
617
|
+
DeviceEventManagerModule.RCTDeviceEventEmitter middleJsEmitter =
|
|
618
|
+
mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
619
|
+
when(middlePending.getApplicationContext()).thenReturn(application);
|
|
620
|
+
when(middlePending.hasActiveReactInstance()).thenReturn(true);
|
|
621
|
+
when(middlePending.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
622
|
+
.thenReturn(middleJsEmitter);
|
|
623
|
+
|
|
624
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
625
|
+
|
|
626
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
627
|
+
emitter.attachReactContext(staleContext);
|
|
628
|
+
ShadowLooper.idleMainLooper();
|
|
629
|
+
emitter.attachReactContext(middlePending);
|
|
630
|
+
ShadowLooper.idleMainLooper();
|
|
631
|
+
emitter.attachReactContext(liveContext);
|
|
632
|
+
ShadowLooper.idleMainLooper();
|
|
633
|
+
|
|
634
|
+
emitter.notifyJsReady(true);
|
|
635
|
+
emitter.addListener(EVENT);
|
|
636
|
+
ShadowLooper.idleMainLooper();
|
|
637
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
638
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
639
|
+
|
|
640
|
+
emitter.detachReactContext(middlePending);
|
|
641
|
+
ShadowLooper.idleMainLooper();
|
|
642
|
+
|
|
643
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
644
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
645
|
+
assertTrue(jsReady(emitter));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* A third overlapping candidate while the host still names the dying attached generation is
|
|
650
|
+
* last-writer-wins: pending becomes the newest candidate and prior pending listener accounting is
|
|
651
|
+
* reset so the newest JS re-registers.
|
|
652
|
+
*/
|
|
653
|
+
@Test
|
|
654
|
+
public void attach_pendingLastWriterWins_whenThirdOverlappingCandidateArrives() throws Exception {
|
|
655
|
+
ReactContext middlePending = mock(ReactContext.class);
|
|
656
|
+
DeviceEventManagerModule.RCTDeviceEventEmitter middleJsEmitter =
|
|
657
|
+
mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
658
|
+
when(middlePending.getApplicationContext()).thenReturn(application);
|
|
659
|
+
when(middlePending.hasActiveReactInstance()).thenReturn(true);
|
|
660
|
+
when(middlePending.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
661
|
+
.thenReturn(middleJsEmitter);
|
|
662
|
+
|
|
663
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
664
|
+
|
|
665
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
666
|
+
emitter.attachReactContext(staleContext);
|
|
667
|
+
ShadowLooper.idleMainLooper();
|
|
668
|
+
emitter.attachReactContext(middlePending);
|
|
669
|
+
ShadowLooper.idleMainLooper();
|
|
670
|
+
|
|
671
|
+
emitter.notifyJsReady(true);
|
|
672
|
+
emitter.addListener(EVENT);
|
|
673
|
+
ShadowLooper.idleMainLooper();
|
|
674
|
+
assertEquals(middlePending, pendingContext(emitter));
|
|
675
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
676
|
+
|
|
677
|
+
// Third overlapping generation while host still on dying attached.
|
|
678
|
+
emitter.attachReactContext(liveContext);
|
|
679
|
+
ShadowLooper.idleMainLooper();
|
|
680
|
+
|
|
681
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
682
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
683
|
+
assertEquals(staleContext, hostLagContext(emitter));
|
|
684
|
+
assertEquals(
|
|
685
|
+
"last-writer-wins pending must reset prior pending listener accounting",
|
|
686
|
+
0,
|
|
687
|
+
listenerCount(emitter, EVENT));
|
|
688
|
+
assertFalse(jsReady(emitter));
|
|
689
|
+
|
|
690
|
+
emitter.notifyJsReady(true);
|
|
691
|
+
emitter.addListener(EVENT);
|
|
692
|
+
ShadowLooper.idleMainLooper();
|
|
693
|
+
|
|
694
|
+
// Detach of superseded middle pending must not clear the newer pending.
|
|
695
|
+
emitter.detachReactContext(middlePending);
|
|
696
|
+
ShadowLooper.idleMainLooper();
|
|
697
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
698
|
+
|
|
699
|
+
emitter.detachReactContext(staleContext);
|
|
700
|
+
ShadowLooper.idleMainLooper();
|
|
701
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
702
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
703
|
+
|
|
704
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
705
|
+
ShadowLooper.idleMainLooper();
|
|
706
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
707
|
+
verify(middleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
708
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* When something is already attached and the host names a third context, an attach candidate that
|
|
713
|
+
* is neither host-current nor the previous attachment is ignored (fail-closed).
|
|
714
|
+
*/
|
|
715
|
+
@Test
|
|
716
|
+
public void attach_ignoresNonCurrentCandidate_whenHostAlreadyMovedToThirdContext()
|
|
717
|
+
throws Exception {
|
|
718
|
+
ReactContext thirdContext = mock(ReactContext.class);
|
|
719
|
+
when(thirdContext.getApplicationContext()).thenReturn(application);
|
|
720
|
+
when(thirdContext.hasActiveReactInstance()).thenReturn(true);
|
|
721
|
+
|
|
722
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
723
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
724
|
+
emitter.attachReactContext(staleContext);
|
|
725
|
+
ShadowLooper.idleMainLooper();
|
|
726
|
+
|
|
727
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
728
|
+
// Candidate is neither host current (live) nor previous attached (stale).
|
|
729
|
+
emitter.attachReactContext(thirdContext);
|
|
730
|
+
ShadowLooper.idleMainLooper();
|
|
731
|
+
|
|
732
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
733
|
+
assertNull(pendingContext(emitter));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Detach of the sole attached generation with no pending replacement clears attachment, listener
|
|
738
|
+
* accounting, and convergence slots while intentionally retaining queued events.
|
|
739
|
+
*/
|
|
740
|
+
@Test
|
|
741
|
+
public void detach_clearsAttachedState_whenNoPendingReplacement() throws Exception {
|
|
742
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
743
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
744
|
+
emitter.attachReactContext(liveContext);
|
|
745
|
+
ShadowLooper.idleMainLooper();
|
|
746
|
+
emitter.notifyJsReady(true);
|
|
747
|
+
emitter.addListener(EVENT);
|
|
748
|
+
ShadowLooper.idleMainLooper();
|
|
749
|
+
|
|
750
|
+
emitter.detachReactContext(liveContext);
|
|
751
|
+
ShadowLooper.idleMainLooper();
|
|
752
|
+
|
|
753
|
+
assertNull(attachedContext(emitter));
|
|
754
|
+
assertNull(pendingContext(emitter));
|
|
755
|
+
assertNull(hostLagContext(emitter));
|
|
756
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
757
|
+
assertFalse(jsReady(emitter));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** Repeated addListener for the same event increments the per-event count. */
|
|
761
|
+
@Test
|
|
762
|
+
public void addListener_incrementsExistingEventCount() throws Exception {
|
|
763
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
764
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
765
|
+
emitter.attachReactContext(liveContext);
|
|
766
|
+
ShadowLooper.idleMainLooper();
|
|
767
|
+
|
|
768
|
+
emitter.addListener(EVENT);
|
|
769
|
+
emitter.addListener(EVENT);
|
|
770
|
+
ShadowLooper.idleMainLooper();
|
|
771
|
+
|
|
772
|
+
assertEquals(2, listenerCount(emitter, EVENT));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** removeListener(all=false) with count > 1 decrements rather than removing the key. */
|
|
776
|
+
@Test
|
|
777
|
+
public void removeListener_decrementsWhenMultipleRemain() throws Exception {
|
|
778
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
779
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
780
|
+
emitter.attachReactContext(liveContext);
|
|
781
|
+
ShadowLooper.idleMainLooper();
|
|
782
|
+
|
|
783
|
+
emitter.addListener(EVENT);
|
|
784
|
+
emitter.addListener(EVENT);
|
|
785
|
+
emitter.removeListener(EVENT, false);
|
|
786
|
+
ShadowLooper.idleMainLooper();
|
|
787
|
+
|
|
788
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** removeListener removes the event key when the last registration is dropped. */
|
|
792
|
+
@Test
|
|
793
|
+
public void removeListener_removesKey_whenLastRegistrationDropped() throws Exception {
|
|
794
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
795
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
796
|
+
emitter.attachReactContext(liveContext);
|
|
797
|
+
ShadowLooper.idleMainLooper();
|
|
798
|
+
|
|
799
|
+
emitter.addListener(EVENT);
|
|
800
|
+
emitter.removeListener(EVENT, false);
|
|
801
|
+
ShadowLooper.idleMainLooper();
|
|
802
|
+
|
|
803
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** removeListener(all=true) removes the key even when multiple registrations remain. */
|
|
807
|
+
@Test
|
|
808
|
+
public void removeListener_removesKey_whenAllFlagTrue() throws Exception {
|
|
809
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
810
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
811
|
+
emitter.attachReactContext(liveContext);
|
|
812
|
+
ShadowLooper.idleMainLooper();
|
|
813
|
+
|
|
814
|
+
emitter.addListener(EVENT);
|
|
815
|
+
emitter.addListener(EVENT);
|
|
816
|
+
emitter.removeListener(EVENT, true);
|
|
817
|
+
ShadowLooper.idleMainLooper();
|
|
818
|
+
|
|
819
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Same-context re-attach while the host already names that context clears residual convergence
|
|
824
|
+
* slots (e.g. leftover hostLag) when there is no different pending replacement.
|
|
825
|
+
*/
|
|
826
|
+
@Test
|
|
827
|
+
public void attach_clearsConvergence_whenSameContextReattachAndHostCurrent() throws Exception {
|
|
828
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
829
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
830
|
+
emitter.attachReactContext(liveContext);
|
|
831
|
+
ShadowLooper.idleMainLooper();
|
|
832
|
+
|
|
833
|
+
setWeakRefField(emitter, "hostLagReactContext", staleContext);
|
|
834
|
+
assertEquals(staleContext, hostLagContext(emitter));
|
|
835
|
+
|
|
836
|
+
emitter.attachReactContext(liveContext);
|
|
837
|
+
ShadowLooper.idleMainLooper();
|
|
838
|
+
|
|
839
|
+
assertNull(hostLagContext(emitter));
|
|
840
|
+
assertNull(pendingContext(emitter));
|
|
841
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Events queued before a matching listener exists are drained by sendQueuedEvents once the
|
|
846
|
+
* listener is registered.
|
|
847
|
+
*/
|
|
848
|
+
@Test
|
|
849
|
+
public void sendQueuedEvents_drainsAndEmits_whenListenerRegistersAfterQueue() {
|
|
850
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
851
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
852
|
+
emitter.attachReactContext(liveContext);
|
|
853
|
+
ShadowLooper.idleMainLooper();
|
|
854
|
+
emitter.notifyJsReady(true);
|
|
855
|
+
ShadowLooper.idleMainLooper();
|
|
856
|
+
|
|
857
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
858
|
+
ShadowLooper.idleMainLooper();
|
|
859
|
+
verify(liveJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
860
|
+
|
|
861
|
+
emitter.addListener(EVENT);
|
|
862
|
+
ShadowLooper.idleMainLooper();
|
|
863
|
+
|
|
864
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Emit aborts immediately when jsReady is still false. */
|
|
868
|
+
@Test
|
|
869
|
+
public void emit_returnsFalse_whenJsReadyFalse() {
|
|
870
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
871
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
872
|
+
emitter.attachReactContext(liveContext);
|
|
873
|
+
ShadowLooper.idleMainLooper();
|
|
874
|
+
emitter.addListener(EVENT);
|
|
875
|
+
ShadowLooper.idleMainLooper();
|
|
876
|
+
|
|
877
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
878
|
+
ShadowLooper.idleMainLooper();
|
|
879
|
+
|
|
880
|
+
verify(liveJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Emit falls back to the pending context hint when the attached WeakReference has already cleared
|
|
885
|
+
* (dying generation GC'd) while a pending replacement remains.
|
|
886
|
+
*/
|
|
887
|
+
@Test
|
|
888
|
+
public void emit_usesPendingHint_whenAttachedWeakReferenceCleared() throws Exception {
|
|
889
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
890
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
891
|
+
emitter.attachReactContext(staleContext);
|
|
892
|
+
ShadowLooper.idleMainLooper();
|
|
893
|
+
emitter.attachReactContext(liveContext);
|
|
894
|
+
ShadowLooper.idleMainLooper();
|
|
895
|
+
|
|
896
|
+
emitter.notifyJsReady(true);
|
|
897
|
+
emitter.addListener(EVENT);
|
|
898
|
+
ShadowLooper.idleMainLooper();
|
|
899
|
+
|
|
900
|
+
setWeakRefField(emitter, "attachedReactContext", null);
|
|
901
|
+
|
|
902
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
903
|
+
ShadowLooper.idleMainLooper();
|
|
904
|
+
|
|
905
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* If jsReady flips false between the unlocked host resolve and the second monitor entry, emit
|
|
910
|
+
* must abort rather than delivering.
|
|
911
|
+
*/
|
|
912
|
+
@Test
|
|
913
|
+
public void emit_returnsFalse_whenJsReadyClearedDuringHostResolve() throws Exception {
|
|
914
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
915
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
916
|
+
emitter.attachReactContext(liveContext);
|
|
917
|
+
ShadowLooper.idleMainLooper();
|
|
918
|
+
emitter.notifyJsReady(true);
|
|
919
|
+
emitter.addListener(EVENT);
|
|
920
|
+
ShadowLooper.idleMainLooper();
|
|
921
|
+
|
|
922
|
+
AtomicBoolean cleared = new AtomicBoolean(false);
|
|
923
|
+
when(reactHost.getCurrentReactContext())
|
|
924
|
+
.thenAnswer(
|
|
925
|
+
invocation -> {
|
|
926
|
+
if (cleared.compareAndSet(false, true)) {
|
|
927
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField("jsReady");
|
|
928
|
+
field.setAccessible(true);
|
|
929
|
+
field.setBoolean(emitter, false);
|
|
930
|
+
}
|
|
931
|
+
return liveContext;
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
935
|
+
ShadowLooper.idleMainLooper();
|
|
936
|
+
|
|
937
|
+
verify(liveJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Emit refuses delivery when the resolved context reports no active React instance. */
|
|
941
|
+
@Test
|
|
942
|
+
public void emit_returnsFalse_whenResolvedContextHasNoActiveInstance() {
|
|
943
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
944
|
+
when(liveContext.hasActiveReactInstance()).thenReturn(false);
|
|
945
|
+
|
|
946
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
947
|
+
emitter.attachReactContext(liveContext);
|
|
948
|
+
ShadowLooper.idleMainLooper();
|
|
949
|
+
emitter.notifyJsReady(true);
|
|
950
|
+
emitter.addListener(EVENT);
|
|
951
|
+
ShadowLooper.idleMainLooper();
|
|
952
|
+
|
|
953
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
954
|
+
ShadowLooper.idleMainLooper();
|
|
955
|
+
|
|
956
|
+
verify(liveJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** Emit catches DeviceEventEmitter failures and returns false rather than propagating. */
|
|
960
|
+
@Test
|
|
961
|
+
public void emit_returnsFalse_whenDeviceEventEmitterThrows() {
|
|
962
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
963
|
+
doThrow(new RuntimeException("emit failed"))
|
|
964
|
+
.when(liveJsEmitter)
|
|
965
|
+
.emit(anyString(), eq(eventBody));
|
|
966
|
+
|
|
967
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
968
|
+
emitter.attachReactContext(liveContext);
|
|
969
|
+
ShadowLooper.idleMainLooper();
|
|
970
|
+
emitter.notifyJsReady(true);
|
|
971
|
+
emitter.addListener(EVENT);
|
|
972
|
+
ShadowLooper.idleMainLooper();
|
|
973
|
+
|
|
974
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
975
|
+
ShadowLooper.idleMainLooper();
|
|
976
|
+
|
|
977
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* While a pending replacement owns listeners, an unrelated third host current must not receive
|
|
982
|
+
* events (that runtime has no RNFB JS subscribers). Emit prefers pending so delivery is not
|
|
983
|
+
* silently lost without re-queue.
|
|
984
|
+
*/
|
|
985
|
+
@Test
|
|
986
|
+
public void emit_deliversToPending_whenHostMovedToUnrelatedThirdContext() throws Exception {
|
|
987
|
+
ReactContext thirdContext = mock(ReactContext.class);
|
|
988
|
+
DeviceEventManagerModule.RCTDeviceEventEmitter thirdJsEmitter =
|
|
989
|
+
mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
990
|
+
when(thirdContext.getApplicationContext()).thenReturn(application);
|
|
991
|
+
when(thirdContext.hasActiveReactInstance()).thenReturn(true);
|
|
992
|
+
when(thirdContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
993
|
+
.thenReturn(thirdJsEmitter);
|
|
994
|
+
|
|
995
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
996
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
997
|
+
emitter.attachReactContext(staleContext);
|
|
998
|
+
ShadowLooper.idleMainLooper();
|
|
999
|
+
emitter.attachReactContext(liveContext);
|
|
1000
|
+
ShadowLooper.idleMainLooper();
|
|
1001
|
+
|
|
1002
|
+
emitter.notifyJsReady(true);
|
|
1003
|
+
emitter.addListener(EVENT);
|
|
1004
|
+
ShadowLooper.idleMainLooper();
|
|
1005
|
+
|
|
1006
|
+
when(reactHost.getCurrentReactContext()).thenReturn(thirdContext);
|
|
1007
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1008
|
+
ShadowLooper.idleMainLooper();
|
|
1009
|
+
|
|
1010
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1011
|
+
verify(thirdJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1012
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* After pending-only cancel following a host advance to an unrelated third context, restored
|
|
1017
|
+
* listeners belong to the surviving attached generation. Emit must not prefer that unattached
|
|
1018
|
+
* hostCurrent (silent delivery into a runtime with no RNFB JS subscribers).
|
|
1019
|
+
*/
|
|
1020
|
+
@Test
|
|
1021
|
+
public void emit_deliversToRestoredAttached_afterPendingCancelWhenHostOnUnrelatedThird()
|
|
1022
|
+
throws Exception {
|
|
1023
|
+
ReactContext thirdContext = mock(ReactContext.class);
|
|
1024
|
+
DeviceEventManagerModule.RCTDeviceEventEmitter thirdJsEmitter =
|
|
1025
|
+
mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
1026
|
+
when(thirdContext.getApplicationContext()).thenReturn(application);
|
|
1027
|
+
when(thirdContext.hasActiveReactInstance()).thenReturn(true);
|
|
1028
|
+
when(thirdContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
1029
|
+
.thenReturn(thirdJsEmitter);
|
|
1030
|
+
|
|
1031
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1032
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1033
|
+
emitter.attachReactContext(staleContext);
|
|
1034
|
+
ShadowLooper.idleMainLooper();
|
|
1035
|
+
|
|
1036
|
+
emitter.notifyJsReady(true);
|
|
1037
|
+
emitter.addListener(EVENT);
|
|
1038
|
+
ShadowLooper.idleMainLooper();
|
|
1039
|
+
|
|
1040
|
+
emitter.attachReactContext(liveContext);
|
|
1041
|
+
ShadowLooper.idleMainLooper();
|
|
1042
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
1043
|
+
|
|
1044
|
+
when(reactHost.getCurrentReactContext()).thenReturn(thirdContext);
|
|
1045
|
+
emitter.detachReactContext(liveContext);
|
|
1046
|
+
ShadowLooper.idleMainLooper();
|
|
1047
|
+
|
|
1048
|
+
assertEquals(null, pendingContext(emitter));
|
|
1049
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
1050
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
1051
|
+
assertTrue(jsReady(emitter));
|
|
1052
|
+
|
|
1053
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1054
|
+
ShadowLooper.idleMainLooper();
|
|
1055
|
+
|
|
1056
|
+
verify(staleJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1057
|
+
verify(thirdJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1058
|
+
verify(liveJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* A subsequent acceptAttachedContext of the host's live generation clears restored-attached emit
|
|
1063
|
+
* affinity so delivery follows the new attached runtime.
|
|
1064
|
+
*/
|
|
1065
|
+
@Test
|
|
1066
|
+
public void emit_prefersNewlyAttachedHost_afterRestoredAffinityClearedByAccept()
|
|
1067
|
+
throws Exception {
|
|
1068
|
+
ReactContext thirdContext = mock(ReactContext.class);
|
|
1069
|
+
DeviceEventManagerModule.RCTDeviceEventEmitter thirdJsEmitter =
|
|
1070
|
+
mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
|
|
1071
|
+
when(thirdContext.getApplicationContext()).thenReturn(application);
|
|
1072
|
+
when(thirdContext.hasActiveReactInstance()).thenReturn(true);
|
|
1073
|
+
when(thirdContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
|
|
1074
|
+
.thenReturn(thirdJsEmitter);
|
|
1075
|
+
|
|
1076
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1077
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1078
|
+
emitter.attachReactContext(staleContext);
|
|
1079
|
+
ShadowLooper.idleMainLooper();
|
|
1080
|
+
|
|
1081
|
+
emitter.notifyJsReady(true);
|
|
1082
|
+
emitter.addListener(EVENT);
|
|
1083
|
+
ShadowLooper.idleMainLooper();
|
|
1084
|
+
|
|
1085
|
+
emitter.attachReactContext(liveContext);
|
|
1086
|
+
ShadowLooper.idleMainLooper();
|
|
1087
|
+
|
|
1088
|
+
when(reactHost.getCurrentReactContext()).thenReturn(thirdContext);
|
|
1089
|
+
emitter.detachReactContext(liveContext);
|
|
1090
|
+
ShadowLooper.idleMainLooper();
|
|
1091
|
+
|
|
1092
|
+
// Host generation attaches for real — affinity must clear.
|
|
1093
|
+
emitter.attachReactContext(thirdContext);
|
|
1094
|
+
ShadowLooper.idleMainLooper();
|
|
1095
|
+
assertEquals(thirdContext, attachedContext(emitter));
|
|
1096
|
+
|
|
1097
|
+
emitter.notifyJsReady(true);
|
|
1098
|
+
emitter.addListener(EVENT);
|
|
1099
|
+
ShadowLooper.idleMainLooper();
|
|
1100
|
+
|
|
1101
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1102
|
+
ShadowLooper.idleMainLooper();
|
|
1103
|
+
|
|
1104
|
+
verify(thirdJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1105
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* With no pending slot and a null host current, emit delivers into the still-attached context.
|
|
1110
|
+
*/
|
|
1111
|
+
@Test
|
|
1112
|
+
public void emit_deliversToAttached_whenHostCurrentNullAndNoPending() {
|
|
1113
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1114
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1115
|
+
emitter.attachReactContext(liveContext);
|
|
1116
|
+
ShadowLooper.idleMainLooper();
|
|
1117
|
+
emitter.notifyJsReady(true);
|
|
1118
|
+
emitter.addListener(EVENT);
|
|
1119
|
+
ShadowLooper.idleMainLooper();
|
|
1120
|
+
|
|
1121
|
+
when(reactHost.getCurrentReactContext()).thenReturn(null);
|
|
1122
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1123
|
+
ShadowLooper.idleMainLooper();
|
|
1124
|
+
|
|
1125
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* A posted tryConverge that runs after the pending slot was cancelled is a no-op (pendingHint
|
|
1130
|
+
* null).
|
|
1131
|
+
*/
|
|
1132
|
+
@Test
|
|
1133
|
+
public void tryConverge_noOps_whenPendingCancelledBeforePostedRunnableRuns() throws Exception {
|
|
1134
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1135
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1136
|
+
emitter.attachReactContext(staleContext);
|
|
1137
|
+
ShadowLooper.idleMainLooper();
|
|
1138
|
+
|
|
1139
|
+
// Enters pending and posts tryConverge; do not idle yet.
|
|
1140
|
+
emitter.attachReactContext(liveContext);
|
|
1141
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
1142
|
+
|
|
1143
|
+
// Cancel pending before the posted converge runs.
|
|
1144
|
+
emitter.detachReactContext(liveContext);
|
|
1145
|
+
assertNull(pendingContext(emitter));
|
|
1146
|
+
|
|
1147
|
+
ShadowLooper.idleMainLooper();
|
|
1148
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Host catch-up between posting tryConverge and running it promotes pending without a second
|
|
1153
|
+
* attach call.
|
|
1154
|
+
*/
|
|
1155
|
+
@Test
|
|
1156
|
+
public void tryConverge_promotesPending_whenHostCatchesUpBeforePostedRunnable() throws Exception {
|
|
1157
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1158
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1159
|
+
emitter.attachReactContext(staleContext);
|
|
1160
|
+
ShadowLooper.idleMainLooper();
|
|
1161
|
+
|
|
1162
|
+
emitter.attachReactContext(liveContext);
|
|
1163
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
1164
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
1165
|
+
|
|
1166
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1167
|
+
ShadowLooper.idleMainLooper();
|
|
1168
|
+
|
|
1169
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
1170
|
+
assertNull(pendingContext(emitter));
|
|
1171
|
+
assertNull(hostLagContext(emitter));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* When attached already equals pending at converge time, clear convergence state without
|
|
1176
|
+
* re-accepting attachment.
|
|
1177
|
+
*/
|
|
1178
|
+
@Test
|
|
1179
|
+
public void tryConverge_clearsStateOnly_whenAttachedAlreadyEqualsPending() throws Exception {
|
|
1180
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1181
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1182
|
+
emitter.attachReactContext(liveContext);
|
|
1183
|
+
ShadowLooper.idleMainLooper();
|
|
1184
|
+
|
|
1185
|
+
setWeakRefField(emitter, "pendingReactContext", liveContext);
|
|
1186
|
+
setWeakRefField(emitter, "hostLagReactContext", staleContext);
|
|
1187
|
+
|
|
1188
|
+
Method converge =
|
|
1189
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1190
|
+
"tryConvergePendingReactContextLocked", ReactContext.class);
|
|
1191
|
+
converge.setAccessible(true);
|
|
1192
|
+
boolean changed = (boolean) converge.invoke(emitter, liveContext);
|
|
1193
|
+
|
|
1194
|
+
assertFalse(changed);
|
|
1195
|
+
assertNull(pendingContext(emitter));
|
|
1196
|
+
assertNull(hostLagContext(emitter));
|
|
1197
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Brownfield hosts that are not {@link ReactApplication} resolve host current as null. */
|
|
1201
|
+
@Test
|
|
1202
|
+
public void hostResolution_returnsNull_whenApplicationContextIsNotReactApplication()
|
|
1203
|
+
throws Exception {
|
|
1204
|
+
Application plainApp = mock(Application.class);
|
|
1205
|
+
when(plainApp.getApplicationContext()).thenReturn(plainApp);
|
|
1206
|
+
ReactContext brownfieldContext = mock(ReactContext.class);
|
|
1207
|
+
when(brownfieldContext.getApplicationContext()).thenReturn(plainApp);
|
|
1208
|
+
|
|
1209
|
+
Method resolve =
|
|
1210
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1211
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1212
|
+
resolve.setAccessible(true);
|
|
1213
|
+
assertNull(resolve.invoke(null, brownfieldContext));
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/** ReactHost getter failures are swallowed; resolution may still fall through. */
|
|
1217
|
+
@Test
|
|
1218
|
+
public void hostResolution_swallowsReactHostRuntimeException() throws Exception {
|
|
1219
|
+
Application reactApp =
|
|
1220
|
+
mock(
|
|
1221
|
+
Application.class,
|
|
1222
|
+
org.mockito.Mockito.withSettings().extraInterfaces(ReactApplication.class));
|
|
1223
|
+
ReactApplication asReactApplication = (ReactApplication) reactApp;
|
|
1224
|
+
when(reactApp.getApplicationContext()).thenReturn(reactApp);
|
|
1225
|
+
when(asReactApplication.getReactHost()).thenThrow(new RuntimeException("reactHost broken"));
|
|
1226
|
+
when(asReactApplication.getReactNativeHost())
|
|
1227
|
+
.thenThrow(new RuntimeException("bridgeless-only test host"));
|
|
1228
|
+
|
|
1229
|
+
ReactContext hint = mock(ReactContext.class);
|
|
1230
|
+
when(hint.getApplicationContext()).thenReturn(reactApp);
|
|
1231
|
+
|
|
1232
|
+
Method resolve =
|
|
1233
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1234
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1235
|
+
resolve.setAccessible(true);
|
|
1236
|
+
assertNull(resolve.invoke(null, hint));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/** When ReactHost is absent, ReactNativeHost failures are logged and null current is returned. */
|
|
1240
|
+
@Test
|
|
1241
|
+
public void hostResolution_swallowsReactNativeHostFailure_whenReactHostAbsent() throws Exception {
|
|
1242
|
+
application.reactHost = null;
|
|
1243
|
+
ReactNativeHost rnHost = mock(ReactNativeHost.class);
|
|
1244
|
+
application.reactNativeHost = rnHost;
|
|
1245
|
+
when(rnHost.hasInstance()).thenThrow(new RuntimeException("native host broken"));
|
|
1246
|
+
|
|
1247
|
+
Method resolve =
|
|
1248
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1249
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1250
|
+
resolve.setAccessible(true);
|
|
1251
|
+
assertNull(resolve.invoke(null, liveContext));
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* When ReactHost is present but reports a null current and ReactNativeHost has no instance,
|
|
1256
|
+
* resolution returns the null ReactHost current (hybrid fallthrough with nothing on the bridge).
|
|
1257
|
+
*/
|
|
1258
|
+
@Test
|
|
1259
|
+
public void hostResolution_returnsNullFromReactHost_whenNativeHostHasNoInstance()
|
|
1260
|
+
throws Exception {
|
|
1261
|
+
when(reactHost.getCurrentReactContext()).thenReturn(null);
|
|
1262
|
+
ReactNativeHost rnHost = mock(ReactNativeHost.class);
|
|
1263
|
+
application.reactNativeHost = rnHost;
|
|
1264
|
+
when(rnHost.hasInstance()).thenReturn(false);
|
|
1265
|
+
|
|
1266
|
+
Method resolve =
|
|
1267
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1268
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1269
|
+
resolve.setAccessible(true);
|
|
1270
|
+
assertNull(resolve.invoke(null, liveContext));
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* When ReactHost is present but reports a null current, a subsequent ReactNativeHost failure must
|
|
1275
|
+
* still return the null ReactHost current.
|
|
1276
|
+
*/
|
|
1277
|
+
@Test
|
|
1278
|
+
public void hostResolution_returnsNullFromReactHost_whenNativeHostFailsAfterNullCurrent()
|
|
1279
|
+
throws Exception {
|
|
1280
|
+
when(reactHost.getCurrentReactContext()).thenReturn(null);
|
|
1281
|
+
ReactNativeHost rnHost = mock(ReactNativeHost.class);
|
|
1282
|
+
application.reactNativeHost = rnHost;
|
|
1283
|
+
when(rnHost.hasInstance()).thenThrow(new RuntimeException("native host broken"));
|
|
1284
|
+
|
|
1285
|
+
Method resolve =
|
|
1286
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1287
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1288
|
+
resolve.setAccessible(true);
|
|
1289
|
+
assertNull(resolve.invoke(null, liveContext));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Fallback {@link ReactNativeFirebaseApp#getApplicationContext()} path when the context hint is
|
|
1294
|
+
* null still participates in host resolution (and brownfield null when unset).
|
|
1295
|
+
*/
|
|
1296
|
+
@Test
|
|
1297
|
+
public void hostResolution_usesReactNativeFirebaseAppFallback_whenContextHintNull()
|
|
1298
|
+
throws Exception {
|
|
1299
|
+
Context previous = ReactNativeFirebaseApp.getApplicationContext();
|
|
1300
|
+
try {
|
|
1301
|
+
ReactNativeFirebaseApp.setApplicationContext(null);
|
|
1302
|
+
Method resolve =
|
|
1303
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1304
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1305
|
+
resolve.setAccessible(true);
|
|
1306
|
+
assertNull(resolve.invoke(null, new Object[] {null}));
|
|
1307
|
+
} finally {
|
|
1308
|
+
ReactNativeFirebaseApp.setApplicationContext(previous);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Same-context re-attach while the host names a different generation must not clear convergence
|
|
1314
|
+
* (dying hostLag with a different pending must keep waiting).
|
|
1315
|
+
*/
|
|
1316
|
+
@Test
|
|
1317
|
+
public void attach_doesNotClearConvergence_whenSameContextReattachAndHostCurrentDiffers()
|
|
1318
|
+
throws Exception {
|
|
1319
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1320
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1321
|
+
emitter.attachReactContext(staleContext);
|
|
1322
|
+
ShadowLooper.idleMainLooper();
|
|
1323
|
+
emitter.attachReactContext(liveContext);
|
|
1324
|
+
ShadowLooper.idleMainLooper();
|
|
1325
|
+
|
|
1326
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
1327
|
+
assertEquals(staleContext, hostLagContext(emitter));
|
|
1328
|
+
|
|
1329
|
+
// Host moved onto live, but re-attach of dying stale must not clear pending.
|
|
1330
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1331
|
+
emitter.attachReactContext(staleContext);
|
|
1332
|
+
ShadowLooper.idleMainLooper();
|
|
1333
|
+
|
|
1334
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
1335
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
1336
|
+
assertEquals(staleContext, hostLagContext(emitter));
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Same-context re-attach of the pending identity while the host has caught up clears residual
|
|
1341
|
+
* convergence (pending == reactContext arm of the compound guard).
|
|
1342
|
+
*/
|
|
1343
|
+
@Test
|
|
1344
|
+
public void attach_clearsConvergence_whenSamePendingReattachAndHostCurrent() throws Exception {
|
|
1345
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1346
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1347
|
+
emitter.attachReactContext(staleContext);
|
|
1348
|
+
ShadowLooper.idleMainLooper();
|
|
1349
|
+
emitter.attachReactContext(liveContext);
|
|
1350
|
+
ShadowLooper.idleMainLooper();
|
|
1351
|
+
|
|
1352
|
+
// Promote pending to attached while leaving pending slot set (host catch-up path normally
|
|
1353
|
+
// clears; force the same-context re-attach compound pending == reactContext).
|
|
1354
|
+
setWeakRefField(emitter, "attachedReactContext", liveContext);
|
|
1355
|
+
setWeakRefField(emitter, "pendingReactContext", liveContext);
|
|
1356
|
+
setWeakRefField(emitter, "hostLagReactContext", staleContext);
|
|
1357
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1358
|
+
|
|
1359
|
+
emitter.attachReactContext(liveContext);
|
|
1360
|
+
ShadowLooper.idleMainLooper();
|
|
1361
|
+
|
|
1362
|
+
assertNull(pendingContext(emitter));
|
|
1363
|
+
assertNull(hostLagContext(emitter));
|
|
1364
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** removeListener is a no-op when the event was never registered. */
|
|
1368
|
+
@Test
|
|
1369
|
+
public void removeListener_isNoOp_whenEventNeverRegistered() throws Exception {
|
|
1370
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1371
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1372
|
+
emitter.attachReactContext(liveContext);
|
|
1373
|
+
ShadowLooper.idleMainLooper();
|
|
1374
|
+
|
|
1375
|
+
emitter.removeListener(EVENT, false);
|
|
1376
|
+
ShadowLooper.idleMainLooper();
|
|
1377
|
+
|
|
1378
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* sendQueuedEvents leaves queued events whose names have no current listener (does not remove
|
|
1383
|
+
* them while draining matching ones).
|
|
1384
|
+
*/
|
|
1385
|
+
@Test
|
|
1386
|
+
public void sendQueuedEvents_leavesUnmatchedQueuedEvents() throws Exception {
|
|
1387
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1388
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1389
|
+
emitter.attachReactContext(liveContext);
|
|
1390
|
+
ShadowLooper.idleMainLooper();
|
|
1391
|
+
emitter.notifyJsReady(true);
|
|
1392
|
+
ShadowLooper.idleMainLooper();
|
|
1393
|
+
|
|
1394
|
+
WritableMap otherBody = mock(WritableMap.class);
|
|
1395
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1396
|
+
emitter.sendEvent(event("auth_id_token_changed", otherBody));
|
|
1397
|
+
ShadowLooper.idleMainLooper();
|
|
1398
|
+
|
|
1399
|
+
emitter.addListener(EVENT);
|
|
1400
|
+
ShadowLooper.idleMainLooper();
|
|
1401
|
+
|
|
1402
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1403
|
+
verify(liveJsEmitter, never()).emit(eq("rnfb_auth_id_token_changed"), eq(otherBody));
|
|
1404
|
+
|
|
1405
|
+
Field queued = ReactNativeFirebaseEventEmitter.class.getDeclaredField("queuedEvents");
|
|
1406
|
+
queued.setAccessible(true);
|
|
1407
|
+
@SuppressWarnings("unchecked")
|
|
1408
|
+
java.util.List<NativeEvent> remaining = (java.util.List<NativeEvent>) queued.get(emitter);
|
|
1409
|
+
assertEquals(1, remaining.size());
|
|
1410
|
+
assertEquals("auth_id_token_changed", remaining.get(0).getEventName());
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* Emit returns false (and re-queues) when resolveEmitContext yields null — no attached, no
|
|
1415
|
+
* pending, and a null host current.
|
|
1416
|
+
*/
|
|
1417
|
+
@Test
|
|
1418
|
+
public void emit_returnsFalse_whenEmitContextNull() throws Exception {
|
|
1419
|
+
when(reactHost.getCurrentReactContext()).thenReturn(null);
|
|
1420
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1421
|
+
emitter.notifyJsReady(true);
|
|
1422
|
+
emitter.addListener(EVENT);
|
|
1423
|
+
ShadowLooper.idleMainLooper();
|
|
1424
|
+
|
|
1425
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1426
|
+
ShadowLooper.idleMainLooper();
|
|
1427
|
+
|
|
1428
|
+
verify(liveJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1429
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1430
|
+
|
|
1431
|
+
Field queued = ReactNativeFirebaseEventEmitter.class.getDeclaredField("queuedEvents");
|
|
1432
|
+
queued.setAccessible(true);
|
|
1433
|
+
@SuppressWarnings("unchecked")
|
|
1434
|
+
java.util.List<NativeEvent> remaining = (java.util.List<NativeEvent>) queued.get(emitter);
|
|
1435
|
+
assertEquals(1, remaining.size());
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** With no pending and a null attached pointer, emit prefers the non-null host current. */
|
|
1439
|
+
@Test
|
|
1440
|
+
public void emit_deliversToHostCurrent_whenAttachedNullAndNoPending() throws Exception {
|
|
1441
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1442
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1443
|
+
emitter.attachReactContext(liveContext);
|
|
1444
|
+
ShadowLooper.idleMainLooper();
|
|
1445
|
+
emitter.notifyJsReady(true);
|
|
1446
|
+
emitter.addListener(EVENT);
|
|
1447
|
+
ShadowLooper.idleMainLooper();
|
|
1448
|
+
|
|
1449
|
+
// Clear attached after host can still resolve via FirebaseApp fallback + ReactHost.
|
|
1450
|
+
Context previous = ReactNativeFirebaseApp.getApplicationContext();
|
|
1451
|
+
try {
|
|
1452
|
+
ReactNativeFirebaseApp.setApplicationContext(application);
|
|
1453
|
+
setWeakRefField(emitter, "attachedReactContext", null);
|
|
1454
|
+
setWeakRefField(emitter, "pendingReactContext", null);
|
|
1455
|
+
|
|
1456
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1457
|
+
ShadowLooper.idleMainLooper();
|
|
1458
|
+
|
|
1459
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1460
|
+
} finally {
|
|
1461
|
+
ReactNativeFirebaseApp.setApplicationContext(previous);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* With no pending, a lagging host current that matches hostLag prefers the live attached
|
|
1467
|
+
* generation over that dying host ref.
|
|
1468
|
+
*/
|
|
1469
|
+
@Test
|
|
1470
|
+
public void emit_prefersAttached_whenHostLagMatchesHostCurrentAndNoPending() throws Exception {
|
|
1471
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1472
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1473
|
+
emitter.attachReactContext(liveContext);
|
|
1474
|
+
ShadowLooper.idleMainLooper();
|
|
1475
|
+
emitter.notifyJsReady(true);
|
|
1476
|
+
emitter.addListener(EVENT);
|
|
1477
|
+
ShadowLooper.idleMainLooper();
|
|
1478
|
+
|
|
1479
|
+
setWeakRefField(emitter, "hostLagReactContext", staleContext);
|
|
1480
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1481
|
+
|
|
1482
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1483
|
+
ShadowLooper.idleMainLooper();
|
|
1484
|
+
|
|
1485
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1486
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Pending-only cancel with a cleared snapshot falls back to resetListenerState (defensive path if
|
|
1491
|
+
* snapshot accounting was discarded while pending still lived).
|
|
1492
|
+
*/
|
|
1493
|
+
@Test
|
|
1494
|
+
public void detach_resetsListeners_whenPendingOnlyCancelledWithoutSnapshot() throws Exception {
|
|
1495
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1496
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1497
|
+
emitter.attachReactContext(staleContext);
|
|
1498
|
+
ShadowLooper.idleMainLooper();
|
|
1499
|
+
emitter.attachReactContext(liveContext);
|
|
1500
|
+
ShadowLooper.idleMainLooper();
|
|
1501
|
+
|
|
1502
|
+
emitter.notifyJsReady(true);
|
|
1503
|
+
emitter.addListener(EVENT);
|
|
1504
|
+
ShadowLooper.idleMainLooper();
|
|
1505
|
+
|
|
1506
|
+
Field snapshot =
|
|
1507
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredField("attachedListenersSnapshot");
|
|
1508
|
+
snapshot.setAccessible(true);
|
|
1509
|
+
snapshot.set(emitter, null);
|
|
1510
|
+
|
|
1511
|
+
emitter.detachReactContext(liveContext);
|
|
1512
|
+
ShadowLooper.idleMainLooper();
|
|
1513
|
+
|
|
1514
|
+
assertNull(pendingContext(emitter));
|
|
1515
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
1516
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
1517
|
+
assertFalse(jsReady(emitter));
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Last-writer-wins pending replace reuses the first attached snapshot so a later pending-only
|
|
1522
|
+
* cancel still restores the original attached generation.
|
|
1523
|
+
*/
|
|
1524
|
+
@Test
|
|
1525
|
+
public void detach_restoresOriginalAttachedSnapshot_afterLastWriterWinsPendingReplace()
|
|
1526
|
+
throws Exception {
|
|
1527
|
+
ReactContext middlePending = mock(ReactContext.class);
|
|
1528
|
+
when(middlePending.getApplicationContext()).thenReturn(application);
|
|
1529
|
+
when(middlePending.hasActiveReactInstance()).thenReturn(true);
|
|
1530
|
+
|
|
1531
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1532
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1533
|
+
emitter.attachReactContext(staleContext);
|
|
1534
|
+
ShadowLooper.idleMainLooper();
|
|
1535
|
+
|
|
1536
|
+
emitter.notifyJsReady(true);
|
|
1537
|
+
emitter.addListener(EVENT);
|
|
1538
|
+
ShadowLooper.idleMainLooper();
|
|
1539
|
+
|
|
1540
|
+
emitter.attachReactContext(middlePending);
|
|
1541
|
+
ShadowLooper.idleMainLooper();
|
|
1542
|
+
emitter.attachReactContext(liveContext);
|
|
1543
|
+
ShadowLooper.idleMainLooper();
|
|
1544
|
+
|
|
1545
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
1546
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
1547
|
+
|
|
1548
|
+
emitter.detachReactContext(liveContext);
|
|
1549
|
+
ShadowLooper.idleMainLooper();
|
|
1550
|
+
|
|
1551
|
+
assertEquals(staleContext, attachedContext(emitter));
|
|
1552
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
1553
|
+
assertTrue(jsReady(emitter));
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/** Detach of attached when pending identity equals attached clears attachment (no promote). */
|
|
1557
|
+
@Test
|
|
1558
|
+
public void detach_clearsAttached_whenPendingIdentityEqualsAttached() throws Exception {
|
|
1559
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1560
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1561
|
+
emitter.attachReactContext(liveContext);
|
|
1562
|
+
ShadowLooper.idleMainLooper();
|
|
1563
|
+
emitter.notifyJsReady(true);
|
|
1564
|
+
emitter.addListener(EVENT);
|
|
1565
|
+
ShadowLooper.idleMainLooper();
|
|
1566
|
+
|
|
1567
|
+
setWeakRefField(emitter, "pendingReactContext", liveContext);
|
|
1568
|
+
|
|
1569
|
+
emitter.detachReactContext(liveContext);
|
|
1570
|
+
ShadowLooper.idleMainLooper();
|
|
1571
|
+
|
|
1572
|
+
assertNull(attachedContext(emitter));
|
|
1573
|
+
assertNull(pendingContext(emitter));
|
|
1574
|
+
assertEquals(0, listenerCount(emitter, EVENT));
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* getListenersMap prefers the pending context as the host-resolution hint while a pending
|
|
1579
|
+
* candidate exists.
|
|
1580
|
+
*/
|
|
1581
|
+
@Test
|
|
1582
|
+
public void getListenersMap_usesPendingHint_whenPendingExists() throws Exception {
|
|
1583
|
+
when(reactHost.getCurrentReactContext()).thenReturn(staleContext);
|
|
1584
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1585
|
+
emitter.attachReactContext(staleContext);
|
|
1586
|
+
ShadowLooper.idleMainLooper();
|
|
1587
|
+
emitter.attachReactContext(liveContext);
|
|
1588
|
+
ShadowLooper.idleMainLooper();
|
|
1589
|
+
|
|
1590
|
+
assertEquals(liveContext, pendingContext(emitter));
|
|
1591
|
+
|
|
1592
|
+
WritableMap writableMap = mock(WritableMap.class);
|
|
1593
|
+
WritableMap eventsMap = mock(WritableMap.class);
|
|
1594
|
+
try (MockedStatic<Arguments> arguments = mockStatic(Arguments.class)) {
|
|
1595
|
+
arguments.when(Arguments::createMap).thenReturn(writableMap, eventsMap);
|
|
1596
|
+
emitter.getListenersMap();
|
|
1597
|
+
verify(writableMap).putInt("attachedContextHash", System.identityHashCode(staleContext));
|
|
1598
|
+
// Host still names dying attached; current hash follows host, not pending hint alone.
|
|
1599
|
+
verify(writableMap).putInt("currentContextHash", System.identityHashCode(staleContext));
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* With no pending, a non-null hostLag that does not match host current does not divert emit away
|
|
1605
|
+
* from the host-preferred path (hostLag != hostCurrent arm).
|
|
1606
|
+
*/
|
|
1607
|
+
@Test
|
|
1608
|
+
public void emit_prefersHostCurrent_whenHostLagSetButDoesNotMatchHost() throws Exception {
|
|
1609
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1610
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1611
|
+
emitter.attachReactContext(staleContext);
|
|
1612
|
+
ShadowLooper.idleMainLooper();
|
|
1613
|
+
emitter.notifyJsReady(true);
|
|
1614
|
+
emitter.addListener(EVENT);
|
|
1615
|
+
ShadowLooper.idleMainLooper();
|
|
1616
|
+
|
|
1617
|
+
setWeakRefField(emitter, "hostLagReactContext", staleContext);
|
|
1618
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1619
|
+
|
|
1620
|
+
emitter.sendEvent(event(EVENT, eventBody));
|
|
1621
|
+
ShadowLooper.idleMainLooper();
|
|
1622
|
+
|
|
1623
|
+
verify(liveJsEmitter).emit(eq("rnfb_" + EVENT), eq(eventBody));
|
|
1624
|
+
verify(staleJsEmitter, never()).emit(anyString(), eq(eventBody));
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Hybrid fallthrough: ReactHost reports null current and getReactNativeHost returns null — the
|
|
1629
|
+
* null ReactHost current stands.
|
|
1630
|
+
*/
|
|
1631
|
+
@Test
|
|
1632
|
+
public void hostResolution_returnsNull_whenReactNativeHostNullAfterNullReactHostCurrent()
|
|
1633
|
+
throws Exception {
|
|
1634
|
+
Application reactApp =
|
|
1635
|
+
mock(
|
|
1636
|
+
Application.class,
|
|
1637
|
+
org.mockito.Mockito.withSettings().extraInterfaces(ReactApplication.class));
|
|
1638
|
+
ReactApplication asReactApplication = (ReactApplication) reactApp;
|
|
1639
|
+
when(reactApp.getApplicationContext()).thenReturn(reactApp);
|
|
1640
|
+
ReactHost host = mock(ReactHost.class);
|
|
1641
|
+
when(asReactApplication.getReactHost()).thenReturn(host);
|
|
1642
|
+
when(host.getCurrentReactContext()).thenReturn(null);
|
|
1643
|
+
when(asReactApplication.getReactNativeHost()).thenReturn(null);
|
|
1644
|
+
|
|
1645
|
+
ReactContext hint = mock(ReactContext.class);
|
|
1646
|
+
when(hint.getApplicationContext()).thenReturn(reactApp);
|
|
1647
|
+
|
|
1648
|
+
Method resolve =
|
|
1649
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1650
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1651
|
+
resolve.setAccessible(true);
|
|
1652
|
+
assertNull(resolve.invoke(null, hint));
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* acceptAttachedContext is a no-op when the candidate is already the attached identity (guards
|
|
1657
|
+
* the inner identity check; production callers only invoke it on a change).
|
|
1658
|
+
*/
|
|
1659
|
+
@Test
|
|
1660
|
+
public void acceptAttachedContext_noOps_whenCandidateAlreadyAttached() throws Exception {
|
|
1661
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1662
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1663
|
+
emitter.attachReactContext(liveContext);
|
|
1664
|
+
ShadowLooper.idleMainLooper();
|
|
1665
|
+
emitter.notifyJsReady(true);
|
|
1666
|
+
emitter.addListener(EVENT);
|
|
1667
|
+
ShadowLooper.idleMainLooper();
|
|
1668
|
+
|
|
1669
|
+
Method accept =
|
|
1670
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1671
|
+
"acceptAttachedContext", ReactContext.class, boolean.class);
|
|
1672
|
+
accept.setAccessible(true);
|
|
1673
|
+
accept.invoke(emitter, liveContext, true);
|
|
1674
|
+
|
|
1675
|
+
assertEquals(liveContext, attachedContext(emitter));
|
|
1676
|
+
assertEquals(1, listenerCount(emitter, EVENT));
|
|
1677
|
+
assertTrue(jsReady(emitter));
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Diagnostic map exposes jsReady and context identity hashes used for on-device diagnosis. {@link
|
|
1682
|
+
* Arguments#createMap()} is mocked so the JVM test does not need native SoLoader.
|
|
1683
|
+
*/
|
|
1684
|
+
@Test
|
|
1685
|
+
public void getListenersMap_exposesJsReadyAndContextIdentityHashes() {
|
|
1686
|
+
when(reactHost.getCurrentReactContext()).thenReturn(liveContext);
|
|
1687
|
+
|
|
1688
|
+
ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
|
|
1689
|
+
emitter.attachReactContext(liveContext);
|
|
1690
|
+
ShadowLooper.idleMainLooper();
|
|
1691
|
+
emitter.notifyJsReady(true);
|
|
1692
|
+
emitter.addListener(EVENT);
|
|
1693
|
+
ShadowLooper.idleMainLooper();
|
|
1694
|
+
|
|
1695
|
+
WritableMap writableMap = mock(WritableMap.class);
|
|
1696
|
+
WritableMap eventsMap = mock(WritableMap.class);
|
|
1697
|
+
|
|
1698
|
+
try (MockedStatic<Arguments> arguments = mockStatic(Arguments.class)) {
|
|
1699
|
+
arguments.when(Arguments::createMap).thenReturn(writableMap, eventsMap);
|
|
1700
|
+
|
|
1701
|
+
WritableMap result = emitter.getListenersMap();
|
|
1702
|
+
assertEquals(writableMap, result);
|
|
1703
|
+
|
|
1704
|
+
verify(writableMap).putInt("listeners", 1);
|
|
1705
|
+
verify(writableMap).putInt(eq("queued"), eq(0));
|
|
1706
|
+
verify(writableMap).putBoolean("jsReady", true);
|
|
1707
|
+
verify(writableMap).putInt("attachedContextHash", System.identityHashCode(liveContext));
|
|
1708
|
+
verify(writableMap).putInt("currentContextHash", System.identityHashCode(liveContext));
|
|
1709
|
+
verify(eventsMap).putInt(EVENT, 1);
|
|
1710
|
+
verify(writableMap).putMap("events", eventsMap);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
private static NativeEvent event(String name, WritableMap body) {
|
|
1715
|
+
return new NativeEvent() {
|
|
1716
|
+
@Override
|
|
1717
|
+
public String getEventName() {
|
|
1718
|
+
return name;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
@Override
|
|
1722
|
+
public WritableMap getEventBody() {
|
|
1723
|
+
return body;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
@Override
|
|
1727
|
+
public String getFirebaseAppName() {
|
|
1728
|
+
return "[DEFAULT]";
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
private static void resetSharedInstance() throws Exception {
|
|
1734
|
+
Constructor<ReactNativeFirebaseEventEmitter> ctor =
|
|
1735
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredConstructor();
|
|
1736
|
+
ctor.setAccessible(true);
|
|
1737
|
+
ReactNativeFirebaseEventEmitter fresh = ctor.newInstance();
|
|
1738
|
+
|
|
1739
|
+
Field shared = ReactNativeFirebaseEventEmitter.class.getDeclaredField("sharedInstance");
|
|
1740
|
+
shared.setAccessible(true);
|
|
1741
|
+
shared.set(null, fresh);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
@SuppressWarnings("unchecked")
|
|
1745
|
+
private static int listenerCount(ReactNativeFirebaseEventEmitter emitter, String eventName)
|
|
1746
|
+
throws Exception {
|
|
1747
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField("jsListeners");
|
|
1748
|
+
field.setAccessible(true);
|
|
1749
|
+
HashMap<String, Integer> listeners = (HashMap<String, Integer>) field.get(emitter);
|
|
1750
|
+
Integer count = listeners.get(eventName);
|
|
1751
|
+
return count == null ? 0 : count;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
@SuppressWarnings("unchecked")
|
|
1755
|
+
private static int queuedCount(ReactNativeFirebaseEventEmitter emitter) throws Exception {
|
|
1756
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField("queuedEvents");
|
|
1757
|
+
field.setAccessible(true);
|
|
1758
|
+
return ((java.util.List<NativeEvent>) field.get(emitter)).size();
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
private static boolean jsReady(ReactNativeFirebaseEventEmitter emitter) throws Exception {
|
|
1762
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField("jsReady");
|
|
1763
|
+
field.setAccessible(true);
|
|
1764
|
+
return field.getBoolean(emitter);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
@SuppressWarnings("unchecked")
|
|
1768
|
+
private static ReactContext attachedContext(ReactNativeFirebaseEventEmitter emitter)
|
|
1769
|
+
throws Exception {
|
|
1770
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField("attachedReactContext");
|
|
1771
|
+
field.setAccessible(true);
|
|
1772
|
+
WeakReference<ReactContext> ref = (WeakReference<ReactContext>) field.get(emitter);
|
|
1773
|
+
return ref.get();
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
@SuppressWarnings("unchecked")
|
|
1777
|
+
private static ReactContext pendingContext(ReactNativeFirebaseEventEmitter emitter)
|
|
1778
|
+
throws Exception {
|
|
1779
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField("pendingReactContext");
|
|
1780
|
+
field.setAccessible(true);
|
|
1781
|
+
WeakReference<ReactContext> ref = (WeakReference<ReactContext>) field.get(emitter);
|
|
1782
|
+
return ref.get();
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
@SuppressWarnings("unchecked")
|
|
1786
|
+
private static ReactContext hostLagContext(ReactNativeFirebaseEventEmitter emitter)
|
|
1787
|
+
throws Exception {
|
|
1788
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField("hostLagReactContext");
|
|
1789
|
+
field.setAccessible(true);
|
|
1790
|
+
WeakReference<ReactContext> ref = (WeakReference<ReactContext>) field.get(emitter);
|
|
1791
|
+
return ref.get();
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
private static void setWeakRefField(
|
|
1795
|
+
ReactNativeFirebaseEventEmitter emitter, String fieldName, @Nullable ReactContext value)
|
|
1796
|
+
throws Exception {
|
|
1797
|
+
Field field = ReactNativeFirebaseEventEmitter.class.getDeclaredField(fieldName);
|
|
1798
|
+
field.setAccessible(true);
|
|
1799
|
+
field.set(emitter, new WeakReference<>(value));
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* Asserts the same diagnostic identities {@link
|
|
1804
|
+
* ReactNativeFirebaseEventEmitter#getListenersMap()} exposes (jsReady / attached / host current
|
|
1805
|
+
* hashes) without calling {@code Arguments.createMap()}, which requires native SoLoader.
|
|
1806
|
+
*/
|
|
1807
|
+
private static void assertDiagnostics(
|
|
1808
|
+
ReactNativeFirebaseEventEmitter emitter,
|
|
1809
|
+
boolean expectedJsReady,
|
|
1810
|
+
@Nullable ReactContext expectedAttached,
|
|
1811
|
+
@Nullable ReactContext expectedCurrent)
|
|
1812
|
+
throws Exception {
|
|
1813
|
+
assertEquals(expectedJsReady, jsReady(emitter));
|
|
1814
|
+
assertEquals(expectedAttached, attachedContext(emitter));
|
|
1815
|
+
|
|
1816
|
+
Method resolve =
|
|
1817
|
+
ReactNativeFirebaseEventEmitter.class.getDeclaredMethod(
|
|
1818
|
+
"getCurrentReactContextFromHost", ReactContext.class);
|
|
1819
|
+
resolve.setAccessible(true);
|
|
1820
|
+
ReactContext current = (ReactContext) resolve.invoke(null, expectedAttached);
|
|
1821
|
+
assertEquals(expectedCurrent, current);
|
|
1822
|
+
}
|
|
1823
|
+
}
|