react-native-notify-kit 10.4.7 → 10.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +4 -0
  2. package/android/src/main/kotlin/io/invertase/notifee/NotifeeReactUtils.kt +679 -7
  3. package/android/src/test/java/io/invertase/notifee/NotifeeReactUtilsPendingEventsTest.java +1025 -0
  4. package/dist/NotifeeApiModule.d.ts +7 -2
  5. package/dist/NotifeeApiModule.js +34 -15
  6. package/dist/NotifeeApiModule.js.map +1 -1
  7. package/dist/fcm/reconstructNotification.js +3 -2
  8. package/dist/fcm/reconstructNotification.js.map +1 -1
  9. package/dist/fcm/types.d.ts +5 -4
  10. package/dist/types/Module.d.ts +11 -2
  11. package/dist/version.d.ts +1 -1
  12. package/dist/version.js +1 -1
  13. package/package.json +1 -1
  14. package/src/NotifeeApiModule.ts +44 -17
  15. package/src/fcm/reconstructNotification.ts +5 -3
  16. package/src/fcm/types.ts +5 -4
  17. package/src/types/Module.ts +12 -2
  18. package/src/version.ts +1 -1
  19. package/server/dist/android.d.ts +0 -8
  20. package/server/dist/android.d.ts.map +0 -1
  21. package/server/dist/android.js +0 -20
  22. package/server/dist/android.js.map +0 -1
  23. package/server/dist/buildPayload.d.ts +0 -3
  24. package/server/dist/buildPayload.d.ts.map +0 -1
  25. package/server/dist/buildPayload.js +0 -82
  26. package/server/dist/buildPayload.js.map +0 -1
  27. package/server/dist/index.d.ts +0 -6
  28. package/server/dist/index.d.ts.map +0 -1
  29. package/server/dist/index.js +0 -12
  30. package/server/dist/index.js.map +0 -1
  31. package/server/dist/ios.d.ts +0 -11
  32. package/server/dist/ios.d.ts.map +0 -1
  33. package/server/dist/ios.js +0 -61
  34. package/server/dist/ios.js.map +0 -1
  35. package/server/dist/serialize.d.ts +0 -11
  36. package/server/dist/serialize.d.ts.map +0 -1
  37. package/server/dist/serialize.js +0 -32
  38. package/server/dist/serialize.js.map +0 -1
  39. package/server/dist/types.d.ts +0 -2
  40. package/server/dist/types.d.ts.map +0 -1
  41. package/server/dist/types.js +0 -3
  42. package/server/dist/types.js.map +0 -1
  43. package/server/dist/validation.d.ts +0 -3
  44. package/server/dist/validation.d.ts.map +0 -1
  45. package/server/dist/validation.js +0 -101
  46. package/server/dist/validation.js.map +0 -1
@@ -0,0 +1,1025 @@
1
+ package io.invertase.notifee;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertFalse;
5
+ import static org.junit.Assert.assertNotNull;
6
+ import static org.junit.Assert.assertNull;
7
+ import static org.junit.Assert.assertSame;
8
+ import static org.junit.Assert.assertTrue;
9
+ import static org.mockito.ArgumentMatchers.any;
10
+ import static org.mockito.ArgumentMatchers.eq;
11
+ import static org.mockito.ArgumentMatchers.same;
12
+ import static org.mockito.Mockito.doThrow;
13
+ import static org.mockito.Mockito.mock;
14
+ import static org.mockito.Mockito.never;
15
+ import static org.mockito.Mockito.times;
16
+ import static org.mockito.Mockito.verify;
17
+ import static org.mockito.Mockito.when;
18
+
19
+ import android.app.Application;
20
+ import android.content.Context;
21
+ import android.content.pm.ApplicationInfo;
22
+ import android.content.pm.PackageManager;
23
+ import android.os.Bundle;
24
+ import app.notifee.core.ContextHolder;
25
+ import app.notifee.core.event.NotificationEvent;
26
+ import com.facebook.react.bridge.JavaOnlyMap;
27
+ import com.facebook.react.bridge.ReactContext;
28
+ import com.facebook.react.bridge.ReadableMap;
29
+ import com.facebook.react.bridge.WritableMap;
30
+ import com.facebook.react.modules.core.DeviceEventManagerModule;
31
+ import java.lang.reflect.Field;
32
+ import java.lang.reflect.Method;
33
+ import java.util.ArrayList;
34
+ import java.util.Arrays;
35
+ import java.util.List;
36
+ import java.util.concurrent.atomic.AtomicLong;
37
+ import kotlin.Pair;
38
+ import org.junit.After;
39
+ import org.junit.Before;
40
+ import org.junit.Test;
41
+ import org.junit.runner.RunWith;
42
+ import org.robolectric.RobolectricTestRunner;
43
+ import org.robolectric.RuntimeEnvironment;
44
+ import org.robolectric.annotation.Config;
45
+ import org.robolectric.shadows.ShadowLog;
46
+
47
+ @RunWith(RobolectricTestRunner.class)
48
+ @Config(application = NotifeeReactUtilsPendingEventsTest.TestApplication.class, sdk = 34)
49
+ public class NotifeeReactUtilsPendingEventsTest {
50
+
51
+ private static final String DIAGNOSTICS_TAG = "NOTIFEE_PENDING_DIAG";
52
+ private static final String NOTIFICATION_EVENT_NAME = "app.notifee.notification-event";
53
+ private static final String DIAGNOSTIC_SCENARIO_ID_KEY = "__notifeePendingDiagScenarioId";
54
+ private static final String DIAGNOSTIC_EVENT_ID_KEY = "__notifeePendingDiagEventId";
55
+
56
+ private TestApplication application;
57
+
58
+ @Before
59
+ public void setUp() throws Exception {
60
+ application = (TestApplication) RuntimeEnvironment.getApplication();
61
+ application.reactHost.reset();
62
+ ContextHolder.setApplicationContext(application);
63
+ resetState();
64
+ ShadowLog.clear();
65
+ }
66
+
67
+ @After
68
+ public void tearDown() throws Exception {
69
+ resetState();
70
+ ShadowLog.clear();
71
+ }
72
+
73
+ @Test
74
+ public void sendEvent_whenContextUnavailable_enqueuesInFifoOrder() throws Exception {
75
+ WritableMap eventA = mock(WritableMap.class);
76
+ WritableMap eventB = mock(WritableMap.class);
77
+ WritableMap eventC = mock(WritableMap.class);
78
+
79
+ setReactContexts(null, null, null);
80
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
81
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
82
+ NotifeeReactUtils.INSTANCE.sendEvent("C", eventC);
83
+
84
+ List<?> queued = pendingEvents();
85
+ assertEquals(Arrays.asList("A", "B", "C"), pendingEventNames(queued));
86
+ assertSame(eventA, pendingEventBody(queued.get(0)));
87
+ assertSame(eventB, pendingEventBody(queued.get(1)));
88
+ assertSame(eventC, pendingEventBody(queued.get(2)));
89
+ assertEquals(Pair.class, queued.get(0).getClass());
90
+ assertNull(pendingDiagnosticsState());
91
+ }
92
+
93
+ @Test
94
+ public void sendEvent_whenElevenEventsPending_dropsOldestAndKeepsTen() throws Exception {
95
+ List<String> names = Arrays.asList("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K");
96
+
97
+ setReactContexts((ReactContext) null);
98
+ for (String name : names) {
99
+ NotifeeReactUtils.INSTANCE.sendEvent(name, mock(WritableMap.class));
100
+ }
101
+
102
+ List<?> queued = pendingEvents();
103
+ assertEquals(10, queued.size());
104
+ assertEquals(names.subList(1, names.size()), pendingEventNames(queued));
105
+ }
106
+
107
+ @Test
108
+ public void snapshotAndClear_separatesBatchAndClearsCurrentQueue() throws Exception {
109
+ WritableMap eventA = mock(WritableMap.class);
110
+ WritableMap eventB = mock(WritableMap.class);
111
+ WritableMap eventC = mock(WritableMap.class);
112
+ ReactContext activeContext = mock(ReactContext.class);
113
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
114
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
115
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
116
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
117
+ .thenReturn(emitter);
118
+
119
+ setReactContexts(null, null, null, activeContext);
120
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
121
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
122
+ NotifeeReactUtils.INSTANCE.sendEvent("C", eventC);
123
+
124
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
125
+
126
+ assertEquals(0, pendingEvents().size());
127
+ org.mockito.InOrder emissionOrder = org.mockito.Mockito.inOrder(emitter);
128
+ emissionOrder.verify(emitter).emit("A", eventA);
129
+ emissionOrder.verify(emitter).emit("B", eventB);
130
+ emissionOrder.verify(emitter).emit("C", eventC);
131
+ }
132
+
133
+ @Test
134
+ public void sendEvent_afterSnapshot_staysInCurrentQueueAndOutsidePreviousBatch()
135
+ throws Exception {
136
+ WritableMap eventA = mock(WritableMap.class);
137
+ WritableMap eventB = mock(WritableMap.class);
138
+ ReactContext activeContext = mock(ReactContext.class);
139
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
140
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
141
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
142
+ .thenReturn(emitter);
143
+ setReactContexts((ReactContext) null);
144
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
145
+
146
+ setReactContexts(activeContext, null);
147
+ when(activeContext.hasActiveReactInstance())
148
+ .thenAnswer(
149
+ invocation -> {
150
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
151
+ return true;
152
+ });
153
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
154
+
155
+ verify(emitter).emit("A", eventA);
156
+ assertEquals(Arrays.asList("B"), pendingEventNames(pendingEvents()));
157
+ assertSame(eventB, pendingEventBody(pendingEvents().get(0)));
158
+ }
159
+
160
+ @Test
161
+ public void flushPendingEvents_whenContextNull_requeuesEvent() throws Exception {
162
+ WritableMap eventA = mock(WritableMap.class);
163
+ setReactContexts(null, null);
164
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
165
+ assertEquals(1, pendingEvents().size());
166
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
167
+
168
+ assertEquals(Arrays.asList("A"), pendingEventNames(pendingEvents()));
169
+ assertSame(eventA, pendingEventBody(pendingEvents().get(0)));
170
+ }
171
+
172
+ @Test
173
+ public void flushPendingEvents_whenContextInactive_requeuesEvent() throws Exception {
174
+ WritableMap eventA = mock(WritableMap.class);
175
+ ReactContext inactiveContext = mock(ReactContext.class);
176
+ when(inactiveContext.hasActiveReactInstance()).thenReturn(false);
177
+
178
+ setReactContexts(null, inactiveContext);
179
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
180
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
181
+
182
+ assertEquals(Arrays.asList("A"), pendingEventNames(pendingEvents()));
183
+ assertSame(eventA, pendingEventBody(pendingEvents().get(0)));
184
+ verify(inactiveContext).hasActiveReactInstance();
185
+ verify(inactiveContext, never()).getJSModule(any());
186
+ }
187
+
188
+ @Test
189
+ public void flushPendingEvents_requeuedEventRecoversExactlyOnceOnNextFlush() throws Exception {
190
+ WritableMap eventA = mock(WritableMap.class);
191
+ ReactContext activeContext = mock(ReactContext.class);
192
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
193
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
194
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
195
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
196
+ .thenReturn(emitter);
197
+
198
+ setReactContexts(null, null);
199
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
200
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
201
+ assertEquals(Arrays.asList("A"), pendingEventNames(pendingEvents()));
202
+
203
+ setReactContexts(activeContext);
204
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
205
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
206
+
207
+ assertTrue(pendingEvents().isEmpty());
208
+ verify(emitter, times(1)).emit("A", eventA);
209
+ }
210
+
211
+ @Test
212
+ public void flushPendingEvents_whenAllContextsUnavailable_preservesSnapshotFifo()
213
+ throws Exception {
214
+ WritableMap eventA = mock(WritableMap.class);
215
+ WritableMap eventB = mock(WritableMap.class);
216
+ WritableMap eventC = mock(WritableMap.class);
217
+
218
+ setReactContexts(null, null, null);
219
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
220
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
221
+ NotifeeReactUtils.INSTANCE.sendEvent("C", eventC);
222
+ setReactContexts((ReactContext) null);
223
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
224
+
225
+ assertEquals(Arrays.asList("A", "B", "C"), pendingEventNames(pendingEvents()));
226
+ assertSame(eventA, pendingEventBody(pendingEvents().get(0)));
227
+ assertSame(eventB, pendingEventBody(pendingEvents().get(1)));
228
+ assertSame(eventC, pendingEventBody(pendingEvents().get(2)));
229
+ }
230
+
231
+ @Test
232
+ public void flushPendingEvents_newArrivalDuringFlush_isMergedAfterRequeuedSnapshot()
233
+ throws Exception {
234
+ WritableMap eventA = mock(WritableMap.class);
235
+ WritableMap eventB = mock(WritableMap.class);
236
+ WritableMap eventC = mock(WritableMap.class);
237
+ ReactContext inactiveContext = mock(ReactContext.class);
238
+
239
+ setReactContexts(null, null);
240
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
241
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
242
+ setReactContexts(inactiveContext, null);
243
+ when(inactiveContext.hasActiveReactInstance())
244
+ .thenAnswer(
245
+ invocation -> {
246
+ NotifeeReactUtils.INSTANCE.sendEvent("C", eventC);
247
+ return false;
248
+ });
249
+
250
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
251
+
252
+ assertEquals(Arrays.asList("A", "B", "C"), pendingEventNames(pendingEvents()));
253
+ assertSame(eventA, pendingEventBody(pendingEvents().get(0)));
254
+ assertSame(eventB, pendingEventBody(pendingEvents().get(1)));
255
+ assertSame(eventC, pendingEventBody(pendingEvents().get(2)));
256
+ }
257
+
258
+ @Test
259
+ public void flushPendingEvents_partialDelivery_requeuesOnlyUndeliveredEvents() throws Exception {
260
+ WritableMap eventA = mock(WritableMap.class);
261
+ WritableMap eventB = mock(WritableMap.class);
262
+ WritableMap eventC = mock(WritableMap.class);
263
+ ReactContext activeContext = mock(ReactContext.class);
264
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
265
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
266
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
267
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
268
+ .thenReturn(emitter);
269
+
270
+ setReactContexts(null, null, null);
271
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
272
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
273
+ NotifeeReactUtils.INSTANCE.sendEvent("C", eventC);
274
+ setReactContexts(activeContext, null);
275
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
276
+
277
+ verify(emitter, times(1)).emit("A", eventA);
278
+ verify(emitter, never()).emit("B", eventB);
279
+ verify(emitter, never()).emit("C", eventC);
280
+ assertEquals(Arrays.asList("B", "C"), pendingEventNames(pendingEvents()));
281
+ }
282
+
283
+ @Test
284
+ public void flushPendingEvents_requeueMergeOverCapacity_dropsGlobalOldestAndKeepsFifo()
285
+ throws Exception {
286
+ List<String> snapshotNames = Arrays.asList("A", "B", "C");
287
+ List<String> newNames = Arrays.asList("D", "E", "F", "G", "H", "I", "J", "K", "L");
288
+ ReactContext inactiveContext = mock(ReactContext.class);
289
+
290
+ setReactContexts((ReactContext) null);
291
+ for (String name : snapshotNames) {
292
+ NotifeeReactUtils.INSTANCE.sendEvent(name, mock(WritableMap.class));
293
+ }
294
+ setReactContexts(inactiveContext, null);
295
+ when(inactiveContext.hasActiveReactInstance())
296
+ .thenAnswer(
297
+ invocation -> {
298
+ for (String name : newNames) {
299
+ NotifeeReactUtils.INSTANCE.sendEvent(name, mock(WritableMap.class));
300
+ }
301
+ return false;
302
+ });
303
+
304
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
305
+
306
+ assertEquals(10, pendingEvents().size());
307
+ assertEquals(
308
+ Arrays.asList("C", "D", "E", "F", "G", "H", "I", "J", "K", "L"),
309
+ pendingEventNames(pendingEvents()));
310
+ }
311
+
312
+ @Test
313
+ public void sendEvent_directDuringFlush_bypassesRequeueAndCanPrecedeRecoveredEvent()
314
+ throws Exception {
315
+ WritableMap eventA = mock(WritableMap.class);
316
+ WritableMap eventB = mock(WritableMap.class);
317
+ ReactContext inactiveContext = mock(ReactContext.class);
318
+ ReactContext activeContext = mock(ReactContext.class);
319
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
320
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
321
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
322
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
323
+ .thenReturn(emitter);
324
+
325
+ setReactContexts((ReactContext) null);
326
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
327
+ setReactContexts(inactiveContext, activeContext);
328
+ when(inactiveContext.hasActiveReactInstance())
329
+ .thenAnswer(
330
+ invocation -> {
331
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
332
+ return false;
333
+ });
334
+
335
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
336
+ assertEquals(Arrays.asList("A"), pendingEventNames(pendingEvents()));
337
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
338
+
339
+ assertTrue(pendingEvents().isEmpty());
340
+ org.mockito.InOrder emissionOrder = org.mockito.Mockito.inOrder(emitter);
341
+ emissionOrder.verify(emitter).emit("B", eventB);
342
+ emissionOrder.verify(emitter).emit("A", eventA);
343
+ verify(emitter, times(1)).emit("B", eventB);
344
+ verify(emitter, times(1)).emit("A", eventA);
345
+ }
346
+
347
+ @Test
348
+ public void flushPendingEvents_whenFirstEmitThrows_doesNotRequeueAndAttemptsNext()
349
+ throws Exception {
350
+ WritableMap eventA = mock(WritableMap.class);
351
+ WritableMap eventB = mock(WritableMap.class);
352
+ ReactContext activeContext = mock(ReactContext.class);
353
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
354
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
355
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
356
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
357
+ .thenReturn(emitter);
358
+ doThrow(new IllegalStateException("characterization emit failure"))
359
+ .when(emitter)
360
+ .emit(eq("A"), same(eventA));
361
+
362
+ setReactContexts(null, null, activeContext, activeContext);
363
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
364
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
365
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
366
+
367
+ assertEquals(0, pendingEvents().size());
368
+ verify(emitter).emit("A", eventA);
369
+ verify(emitter).emit("B", eventB);
370
+ }
371
+
372
+ @Test
373
+ public void diagnostics_withoutMetadata_areDisabledWithZeroDelayAndNoMarkers() throws Exception {
374
+ Object config = loadPendingDiagnosticsConfig(application);
375
+ assertFalse((boolean) privateField(config, "enabled"));
376
+ assertEquals(0L, ((Number) privateField(config, "delayAfterSnapshotMs")).longValue());
377
+
378
+ WritableMap eventA = mock(WritableMap.class);
379
+ setReactContexts(null, null);
380
+ int packageManagerRequestsBeforeEvent = application.packageManagerRequests;
381
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
382
+ List<?> queued = pendingEvents();
383
+ assertEquals(1, queued.size());
384
+ assertEquals(Pair.class, queued.get(0).getClass());
385
+ assertSame(eventA, pendingEventBody(queued.get(0)));
386
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
387
+
388
+ assertEquals(1, pendingEvents().size());
389
+ assertSame(eventA, pendingEventBody(pendingEvents().get(0)));
390
+ assertEquals(packageManagerRequestsBeforeEvent, application.packageManagerRequests);
391
+ assertNull(pendingDiagnosticsState());
392
+ assertTrue(ShadowLog.getLogsForTag(DIAGNOSTICS_TAG).isEmpty());
393
+ }
394
+
395
+ @Test
396
+ public void diagnosticIdentity_validFixturePayload_extractsControlledIdentity() throws Exception {
397
+ JavaOnlyMap eventMap = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A");
398
+
399
+ Object identity = extractPendingDiagnosticIdentity(eventMap);
400
+
401
+ assertNotNull(identity);
402
+ assertEquals("scenario-1", privateField(identity, "scenarioId"));
403
+ assertEquals("pending-diag:scenario-1:A", privateField(identity, "eventId"));
404
+ assertEquals(NotificationEvent.TYPE_DELIVERED, privateField(identity, "eventType"));
405
+ }
406
+
407
+ @Test
408
+ public void diagnosticIdentity_missingPayload_returnsUnavailableWithoutException()
409
+ throws Exception {
410
+ assertNull(extractPendingDiagnosticIdentity(new JavaOnlyMap()));
411
+
412
+ JavaOnlyMap eventMap = JavaOnlyMap.of("detail", JavaOnlyMap.of("notification", null));
413
+ assertNull(extractPendingDiagnosticIdentity(eventMap));
414
+ }
415
+
416
+ @Test
417
+ public void diagnosticIdentity_wrongFieldType_returnsUnavailableWithoutException()
418
+ throws Exception {
419
+ JavaOnlyMap eventMap = diagnosticEventMap("scenario-1", 7);
420
+
421
+ assertNull(extractPendingDiagnosticIdentity(eventMap));
422
+ }
423
+
424
+ @Test
425
+ public void diagnosticIdentity_eventTypeMustBePresentNumericAndIntegerValued() throws Exception {
426
+ JavaOnlyMap missingType =
427
+ diagnosticEventMapWithoutType("scenario-1", "pending-diag:scenario-1:A");
428
+ JavaOnlyMap wrongType = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A", "3");
429
+ JavaOnlyMap fractionalType =
430
+ diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A", 3.5d);
431
+ JavaOnlyMap outOfRangeType =
432
+ diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A", 2147483648d);
433
+ JavaOnlyMap integerValuedNumber =
434
+ diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A", 3.0d);
435
+
436
+ Object missingTypeIdentity = extractPendingDiagnosticIdentity(missingType);
437
+ Object wrongTypeIdentity = extractPendingDiagnosticIdentity(wrongType);
438
+ Object fractionalTypeIdentity = extractPendingDiagnosticIdentity(fractionalType);
439
+ Object outOfRangeTypeIdentity = extractPendingDiagnosticIdentity(outOfRangeType);
440
+ Object integerValuedIdentity = extractPendingDiagnosticIdentity(integerValuedNumber);
441
+ assertNotNull(missingTypeIdentity);
442
+ assertNotNull(wrongTypeIdentity);
443
+ assertNotNull(fractionalTypeIdentity);
444
+ assertNotNull(outOfRangeTypeIdentity);
445
+ assertNotNull(integerValuedIdentity);
446
+ assertNull(privateField(missingTypeIdentity, "eventType"));
447
+ assertNull(privateField(wrongTypeIdentity, "eventType"));
448
+ assertNull(privateField(fractionalTypeIdentity, "eventType"));
449
+ assertNull(privateField(outOfRangeTypeIdentity, "eventType"));
450
+ assertEquals(
451
+ NotificationEvent.TYPE_DELIVERED, privateField(integerValuedIdentity, "eventType"));
452
+
453
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
454
+ setReactContexts((ReactContext) null);
455
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, missingType);
456
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, wrongType);
457
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, fractionalType);
458
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, outOfRangeType);
459
+
460
+ assertEquals(
461
+ Arrays.asList("unavailable", "unavailable", "unavailable", "unavailable"),
462
+ markerFieldValues("EVENT_RECEIVED", "eventType"));
463
+ assertEquals(
464
+ Arrays.asList(
465
+ "pending-diag:scenario-1:A",
466
+ "pending-diag:scenario-1:A",
467
+ "pending-diag:scenario-1:A",
468
+ "pending-diag:scenario-1:A"),
469
+ markerFieldValues("EVENT_RECEIVED", "diagEventId"));
470
+ }
471
+
472
+ @Test
473
+ public void diagnosticIdentity_nonConformingValue_isUnavailableAndNeverLogged() throws Exception {
474
+ JavaOnlyMap missingPrefix = diagnosticEventMap("scenario-1", "consumer-value");
475
+ JavaOnlyMap forbiddenCharacters =
476
+ diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A secret");
477
+ JavaOnlyMap wrongScenario = diagnosticEventMap("scenario-1", "pending-diag:scenario-2:A");
478
+
479
+ assertNull(extractPendingDiagnosticIdentity(missingPrefix));
480
+ assertNull(extractPendingDiagnosticIdentity(forbiddenCharacters));
481
+ assertNull(extractPendingDiagnosticIdentity(wrongScenario));
482
+
483
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
484
+ setReactContexts((ReactContext) null);
485
+ NotifeeReactUtils.INSTANCE.sendEvent("A", forbiddenCharacters);
486
+
487
+ String received = markerMessage("EVENT_RECEIVED");
488
+ assertTrue(received.contains("diagScenarioId=unavailable"));
489
+ assertTrue(received.contains("diagEventId=unavailable"));
490
+ assertTrue(received.contains("eventType=unavailable"));
491
+ assertFalse(received.contains("A secret"));
492
+ }
493
+
494
+ @Test
495
+ public void diagnostics_sameIdDifferentTypes_keepSeparateNativeReceipts() throws Exception {
496
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
497
+ String eventId = "pending-diag:scenario-1:A";
498
+ JavaOnlyMap triggerCreated =
499
+ diagnosticEventMap(
500
+ "scenario-1", eventId, NotificationEvent.TYPE_TRIGGER_NOTIFICATION_CREATED);
501
+ JavaOnlyMap delivered =
502
+ diagnosticEventMap("scenario-1", eventId, NotificationEvent.TYPE_DELIVERED);
503
+ ReactContext activeContext = mock(ReactContext.class);
504
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
505
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
506
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
507
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
508
+ .thenReturn(emitter);
509
+
510
+ setReactContexts(activeContext, null);
511
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, triggerCreated);
512
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, delivered);
513
+
514
+ verify(emitter).emit(NOTIFICATION_EVENT_NAME, triggerCreated);
515
+ assertEquals(
516
+ Arrays.asList(eventId, eventId), markerFieldValues("EVENT_RECEIVED", "diagEventId"));
517
+ assertEquals(Arrays.asList("1", "2"), markerFieldValues("EVENT_RECEIVED", "eventSeq"));
518
+ assertEquals(Arrays.asList("7", "3"), markerFieldValues("EVENT_RECEIVED", "eventType"));
519
+ assertEquals(Arrays.asList("7"), markerFieldValues("EVENT_EMIT_DIRECT_BEGIN", "eventType"));
520
+ assertEquals(Arrays.asList("3"), markerFieldValues("EVENT_ENQUEUED", "eventType"));
521
+ }
522
+
523
+ @Test
524
+ public void diagnostics_sameIdAndType_keepDistinctReceiptSequences() throws Exception {
525
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
526
+ String eventId = "pending-diag:scenario-1:A";
527
+ JavaOnlyMap deliveredOne =
528
+ diagnosticEventMap("scenario-1", eventId, NotificationEvent.TYPE_DELIVERED);
529
+ JavaOnlyMap deliveredTwo =
530
+ diagnosticEventMap("scenario-1", eventId, NotificationEvent.TYPE_DELIVERED);
531
+
532
+ setReactContexts((ReactContext) null);
533
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, deliveredOne);
534
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, deliveredTwo);
535
+
536
+ assertEquals(
537
+ Arrays.asList(eventId, eventId), markerFieldValues("EVENT_RECEIVED", "diagEventId"));
538
+ assertEquals(Arrays.asList("3", "3"), markerFieldValues("EVENT_RECEIVED", "eventType"));
539
+ assertEquals(Arrays.asList("1", "2"), markerFieldValues("EVENT_RECEIVED", "eventSeq"));
540
+ assertEquals(Arrays.asList("1", "2"), markerFieldValues("EVENT_ENQUEUED", "eventSeq"));
541
+ }
542
+
543
+ @Test
544
+ public void diagnostics_enabled_emitMarkersAndDoNotModifyPublicPayload() throws Exception {
545
+ DiagnosticsContext diagnostics = diagnosticsContext(true, true, 1);
546
+ assertTrue(initializePendingDiagnostics(diagnostics.context));
547
+
548
+ JavaOnlyMap eventA = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A");
549
+ ReactContext activeContext = mock(ReactContext.class);
550
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
551
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
552
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
553
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
554
+ .thenReturn(emitter);
555
+
556
+ setReactContexts(null, activeContext);
557
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
558
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
559
+
560
+ verify(emitter).emit(eq("A"), same(eventA));
561
+ ReadableMap detail = eventA.getMap("detail");
562
+ assertNotNull(detail);
563
+ ReadableMap notification = detail.getMap("notification");
564
+ assertNotNull(notification);
565
+ ReadableMap data = notification.getMap("data");
566
+ assertNotNull(data);
567
+ assertEquals("consumer-value", data.getString("consumerField"));
568
+ assertEquals("scenario-1", data.getString(DIAGNOSTIC_SCENARIO_ID_KEY));
569
+ assertEquals("pending-diag:scenario-1:A", data.getString(DIAGNOSTIC_EVENT_ID_KEY));
570
+ assertEquals(NotificationEvent.TYPE_DELIVERED, eventA.getInt("type"));
571
+ assertFalse(eventA.hasKey("eventSeq"));
572
+ assertFalse(data.hasKey("eventSeq"));
573
+ assertTrue(hasMarker("EVENT_RECEIVED"));
574
+ assertTrue(hasMarker("EVENT_ENQUEUED"));
575
+ assertTrue(hasMarker("FLUSH_REQUESTED"));
576
+ assertTrue(hasMarker("FLUSH_SNAPSHOT"));
577
+ assertTrue(hasMarker("FLUSH_SNAPSHOT_ITEM"));
578
+ assertTrue(hasMarker("FLUSH_QUEUE_CLEARED"));
579
+ assertTrue(hasMarker("FLUSH_DIAGNOSTIC_DELAY_BEGIN"));
580
+ assertTrue(hasMarker("FLUSH_DIAGNOSTIC_DELAY_END"));
581
+ assertTrue(hasMarker("FLUSH_EVENT_CONTEXT_CHECK"));
582
+ assertTrue(hasMarker("FLUSH_EVENT_EMIT_BEGIN"));
583
+ assertTrue(hasMarker("FLUSH_EVENT_EMIT_RETURN"));
584
+ assertTrue(hasMarker("FLUSH_COMPLETED"));
585
+ assertTrue(markerMessage("EVENT_RECEIVED").contains("eventSeq=1"));
586
+ assertTrue(markerMessage("EVENT_RECEIVED").contains("diagScenarioId=scenario-1"));
587
+ assertTrue(markerMessage("EVENT_RECEIVED").contains("diagEventId=pending-diag:scenario-1:A"));
588
+ assertEquals(Arrays.asList("3"), markerFieldValues("EVENT_RECEIVED", "eventType"));
589
+ assertEquals(Arrays.asList("3"), markerFieldValues("EVENT_ENQUEUED", "eventType"));
590
+ assertEquals(Arrays.asList("3"), markerFieldValues("FLUSH_SNAPSHOT_ITEM", "eventType"));
591
+ assertEquals(Arrays.asList("3"), markerFieldValues("FLUSH_EVENT_CONTEXT_CHECK", "eventType"));
592
+ assertEquals(Arrays.asList("3"), markerFieldValues("FLUSH_EVENT_EMIT_RETURN", "eventType"));
593
+ assertTrue(markerMessage("FLUSH_REQUESTED").contains("flushId=1"));
594
+
595
+ Object state = pendingDiagnosticsState();
596
+ assertNotNull(state);
597
+ assertEquals(1L, atomicLongField(state, "eventSequence").get());
598
+ assertEquals(1L, atomicLongField(state, "flushSequence").get());
599
+ assertTrue(((java.util.Map<?, ?>) privateField(state, "queuedEventDiagnostics")).isEmpty());
600
+ verify(diagnostics.packageManager, times(1))
601
+ .getApplicationInfo("com.notifeeexample", PackageManager.GET_META_DATA);
602
+ }
603
+
604
+ @Test
605
+ public void diagnostics_requeuedEvent_keepsIdentityAndRecoversExactlyOnce() throws Exception {
606
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
607
+ JavaOnlyMap eventA = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A");
608
+ ReactContext activeContext = mock(ReactContext.class);
609
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
610
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
611
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
612
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
613
+ .thenReturn(emitter);
614
+
615
+ setReactContexts(null, null);
616
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, eventA);
617
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
618
+
619
+ assertEquals(Arrays.asList(NOTIFICATION_EVENT_NAME), pendingEventNames(pendingEvents()));
620
+ assertSame(eventA, pendingEventBody(pendingEvents().get(0)));
621
+ assertEquals(
622
+ 1,
623
+ ((java.util.Map<?, ?>) privateField(pendingDiagnosticsState(), "queuedEventDiagnostics"))
624
+ .size());
625
+ assertEquals(Arrays.asList("1"), markerFieldValues("FLUSH_EVENT_REQUEUED", "eventSeq"));
626
+ assertEquals(Arrays.asList("1"), markerFieldValues("FLUSH_EVENT_REQUEUED", "flushId"));
627
+ assertEquals(Arrays.asList("3"), markerFieldValues("FLUSH_EVENT_REQUEUED", "eventType"));
628
+ assertEquals(
629
+ Arrays.asList("pending-diag:scenario-1:A"),
630
+ markerFieldValues("FLUSH_EVENT_REQUEUED", "diagEventId"));
631
+ assertEquals(
632
+ Arrays.asList("context_null"), markerFieldValues("FLUSH_EVENT_REQUEUED", "reason"));
633
+ assertEquals(Arrays.asList("1"), markerFieldValues("FLUSH_REQUEUE_MERGE", "requeuedCount"));
634
+ assertEquals(Arrays.asList("0"), markerFieldValues("FLUSH_REQUEUE_MERGE", "newArrivalsCount"));
635
+ assertEquals(Arrays.asList("0"), markerFieldValues("FLUSH_REQUEUE_MERGE", "droppedCount"));
636
+
637
+ setReactContexts(activeContext);
638
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
639
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
640
+
641
+ verify(emitter, times(1)).emit(NOTIFICATION_EVENT_NAME, eventA);
642
+ assertTrue(pendingEvents().isEmpty());
643
+ assertTrue(
644
+ ((java.util.Map<?, ?>) privateField(pendingDiagnosticsState(), "queuedEventDiagnostics"))
645
+ .isEmpty());
646
+ assertEquals(Arrays.asList("1", "1"), markerFieldValues("FLUSH_SNAPSHOT_ITEM", "eventSeq"));
647
+ assertEquals(Arrays.asList("1", "2"), markerFieldValues("FLUSH_SNAPSHOT_ITEM", "flushId"));
648
+ assertEquals(Arrays.asList("1"), markerFieldValues("FLUSH_EVENT_EMIT_RETURN", "eventSeq"));
649
+ assertEquals(Arrays.asList("2"), markerFieldValues("FLUSH_EVENT_EMIT_RETURN", "flushId"));
650
+ }
651
+
652
+ @Test
653
+ public void diagnostics_emitException_isNotRequeuedAndNextEventIsAttempted() throws Exception {
654
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
655
+ JavaOnlyMap eventA = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A");
656
+ JavaOnlyMap eventB = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:B");
657
+ ReactContext activeContext = mock(ReactContext.class);
658
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
659
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
660
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
661
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
662
+ .thenReturn(emitter);
663
+ doThrow(new IllegalStateException("diagnostic emit failure"))
664
+ .when(emitter)
665
+ .emit(eq(NOTIFICATION_EVENT_NAME), same(eventA));
666
+
667
+ setReactContexts(null, null, activeContext, activeContext);
668
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, eventA);
669
+ NotifeeReactUtils.INSTANCE.sendEvent(NOTIFICATION_EVENT_NAME, eventB);
670
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
671
+
672
+ assertTrue(pendingEvents().isEmpty());
673
+ verify(emitter).emit(NOTIFICATION_EVENT_NAME, eventA);
674
+ verify(emitter).emit(NOTIFICATION_EVENT_NAME, eventB);
675
+ assertEquals(
676
+ Arrays.asList("pending-diag:scenario-1:A"),
677
+ markerFieldValues("FLUSH_EVENT_EMIT_EXCEPTION", "diagEventId"));
678
+ assertFalse(hasMarker("FLUSH_EVENT_REQUEUED"));
679
+ }
680
+
681
+ @Test
682
+ public void diagnostics_requeueMergeOverCapacity_preservesSideMapAndGlobalFifo()
683
+ throws Exception {
684
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
685
+ List<String> expectedEventIds = new ArrayList<>();
686
+ for (int index = 1; index <= 11; index += 1) {
687
+ expectedEventIds.add(String.format("pending-diag:scenario-1:overflow:%02d", index));
688
+ }
689
+ ReactContext inactiveContext = mock(ReactContext.class);
690
+ ReactContext activeContext = mock(ReactContext.class);
691
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
692
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
693
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
694
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
695
+ .thenReturn(emitter);
696
+
697
+ setReactContexts((ReactContext) null);
698
+ for (int index = 0; index < 2; index += 1) {
699
+ NotifeeReactUtils.INSTANCE.sendEvent(
700
+ "overflow", diagnosticEventMap("scenario-1", expectedEventIds.get(index)));
701
+ }
702
+ setReactContexts(inactiveContext, null);
703
+ when(inactiveContext.hasActiveReactInstance())
704
+ .thenAnswer(
705
+ invocation -> {
706
+ for (int index = 2; index < expectedEventIds.size(); index += 1) {
707
+ NotifeeReactUtils.INSTANCE.sendEvent(
708
+ "overflow", diagnosticEventMap("scenario-1", expectedEventIds.get(index)));
709
+ }
710
+ return false;
711
+ });
712
+
713
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
714
+
715
+ assertEquals(10, pendingEvents().size());
716
+ assertEquals(
717
+ 10,
718
+ ((java.util.Map<?, ?>) privateField(pendingDiagnosticsState(), "queuedEventDiagnostics"))
719
+ .size());
720
+ assertEquals(
721
+ Arrays.asList(expectedEventIds.get(0)),
722
+ markerFieldValues("FLUSH_REQUEUE_DROP", "diagEventId"));
723
+ assertEquals(Arrays.asList("2"), markerFieldValues("FLUSH_REQUEUE_MERGE", "requeuedCount"));
724
+ assertEquals(Arrays.asList("9"), markerFieldValues("FLUSH_REQUEUE_MERGE", "newArrivalsCount"));
725
+ assertEquals(Arrays.asList("1"), markerFieldValues("FLUSH_REQUEUE_MERGE", "droppedCount"));
726
+
727
+ setReactContexts(activeContext);
728
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
729
+
730
+ List<String> allSnapshotIds = markerFieldValues("FLUSH_SNAPSHOT_ITEM", "diagEventId");
731
+ assertEquals(
732
+ expectedEventIds.subList(1, expectedEventIds.size()), allSnapshotIds.subList(2, 12));
733
+ assertTrue(pendingEvents().isEmpty());
734
+ assertTrue(
735
+ ((java.util.Map<?, ?>) privateField(pendingDiagnosticsState(), "queuedEventDiagnostics"))
736
+ .isEmpty());
737
+ verify(emitter, times(10)).emit(eq("overflow"), any(WritableMap.class));
738
+ }
739
+
740
+ @Test
741
+ public void diagnostics_similarEvents_keepDistinctControlledIdentities() throws Exception {
742
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
743
+ JavaOnlyMap eventA = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:A");
744
+ JavaOnlyMap eventB = diagnosticEventMap("scenario-1", "pending-diag:scenario-1:B");
745
+ ReactContext activeContext = mock(ReactContext.class);
746
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
747
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
748
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
749
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
750
+ .thenReturn(emitter);
751
+
752
+ setReactContexts(null, null, activeContext, activeContext);
753
+ NotifeeReactUtils.INSTANCE.sendEvent("A", eventA);
754
+ NotifeeReactUtils.INSTANCE.sendEvent("B", eventB);
755
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
756
+
757
+ verify(emitter).emit("A", eventA);
758
+ verify(emitter).emit("B", eventB);
759
+ assertEquals(
760
+ Arrays.asList("pending-diag:scenario-1:A", "pending-diag:scenario-1:B"),
761
+ markerFieldValues("EVENT_ENQUEUED", "diagEventId"));
762
+ assertEquals(
763
+ Arrays.asList("pending-diag:scenario-1:A", "pending-diag:scenario-1:B"),
764
+ markerFieldValues("FLUSH_SNAPSHOT_ITEM", "diagEventId"));
765
+ assertTrue(
766
+ ((java.util.Map<?, ?>) privateField(pendingDiagnosticsState(), "queuedEventDiagnostics"))
767
+ .isEmpty());
768
+ }
769
+
770
+ @Test
771
+ public void diagnostics_overflow_logsDroppedIdentityAndCleansSideMap() throws Exception {
772
+ assertTrue(initializePendingDiagnostics(diagnosticsContext(true, true, 0).context));
773
+ setReactContexts((ReactContext) null);
774
+ List<String> expectedEventIds = new ArrayList<>();
775
+
776
+ for (int index = 1; index <= 11; index += 1) {
777
+ String eventId = String.format("pending-diag:scenario-1:overflow:%02d", index);
778
+ expectedEventIds.add(eventId);
779
+ NotifeeReactUtils.INSTANCE.sendEvent("overflow", diagnosticEventMap("scenario-1", eventId));
780
+ }
781
+
782
+ Object state = pendingDiagnosticsState();
783
+ assertEquals(10, ((java.util.Map<?, ?>) privateField(state, "queuedEventDiagnostics")).size());
784
+ assertTrue(
785
+ markerMessage("EVENT_OVERFLOW_DROPPED")
786
+ .contains("diagEventId=pending-diag:scenario-1:overflow:01"));
787
+
788
+ ReactContext activeContext = mock(ReactContext.class);
789
+ DeviceEventManagerModule.RCTDeviceEventEmitter emitter =
790
+ mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class);
791
+ when(activeContext.hasActiveReactInstance()).thenReturn(true);
792
+ when(activeContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class))
793
+ .thenReturn(emitter);
794
+ setReactContexts(activeContext);
795
+ NotifeeReactUtils.INSTANCE.flushPendingEvents();
796
+
797
+ assertEquals(
798
+ expectedEventIds.subList(1, expectedEventIds.size()),
799
+ markerFieldValues("FLUSH_SNAPSHOT_ITEM", "diagEventId"));
800
+ assertEquals(
801
+ expectedEventIds.subList(1, expectedEventIds.size()),
802
+ markerFieldValues("FLUSH_EVENT_CONTEXT_CHECK", "diagEventId"));
803
+ assertTrue(((java.util.Map<?, ?>) privateField(state, "queuedEventDiagnostics")).isEmpty());
804
+ }
805
+
806
+ @Test
807
+ public void diagnostics_configuration_isDebugGatedClampedAndInitializedOnce() throws Exception {
808
+ DiagnosticsContext releaseContext = diagnosticsContext(false, true, 3000);
809
+ assertFalse(initializePendingDiagnostics(releaseContext.context));
810
+ assertNull(pendingDiagnosticsState());
811
+
812
+ Object negativeDelayConfig =
813
+ loadPendingDiagnosticsConfig(diagnosticsContext(true, true, -1).context);
814
+ assertEquals(
815
+ 0L, ((Number) privateField(negativeDelayConfig, "delayAfterSnapshotMs")).longValue());
816
+
817
+ DiagnosticsContext clampedContext = diagnosticsContext(true, true, 15000);
818
+ assertTrue(initializePendingDiagnostics(clampedContext.context));
819
+ assertEquals(
820
+ 10000L,
821
+ ((Number) privateField(pendingDiagnosticsState(), "delayAfterSnapshotMs")).longValue());
822
+ assertTrue(initializePendingDiagnostics(clampedContext.context));
823
+ verify(clampedContext.packageManager, times(1))
824
+ .getApplicationInfo("com.notifeeexample", PackageManager.GET_META_DATA);
825
+ }
826
+
827
+ private void setReactContexts(ReactContext... contexts) {
828
+ if (contexts.length == 0) {
829
+ throw new IllegalArgumentException("At least one context result is required");
830
+ }
831
+ application.reactHost.setResults(contexts);
832
+ }
833
+
834
+ @SuppressWarnings("unchecked")
835
+ private static List<?> pendingEvents() throws Exception {
836
+ Field field = NotifeeReactUtils.class.getDeclaredField("pendingEvents");
837
+ field.setAccessible(true);
838
+ return (List<?>) field.get(NotifeeReactUtils.INSTANCE);
839
+ }
840
+
841
+ private static Object loadPendingDiagnosticsConfig(Context context) throws Exception {
842
+ Method method =
843
+ NotifeeReactUtils.class.getDeclaredMethod("loadPendingDiagnosticsConfig", Context.class);
844
+ method.setAccessible(true);
845
+ return method.invoke(NotifeeReactUtils.INSTANCE, context);
846
+ }
847
+
848
+ private static boolean initializePendingDiagnostics(Context context) throws Exception {
849
+ Method method =
850
+ NotifeeReactUtils.class.getDeclaredMethod("initializePendingDiagnostics", Context.class);
851
+ method.setAccessible(true);
852
+ return (boolean) method.invoke(NotifeeReactUtils.INSTANCE, context);
853
+ }
854
+
855
+ private static Object extractPendingDiagnosticIdentity(WritableMap eventMap) throws Exception {
856
+ Method method =
857
+ NotifeeReactUtils.class.getDeclaredMethod(
858
+ "extractPendingDiagnosticIdentity", WritableMap.class);
859
+ method.setAccessible(true);
860
+ return method.invoke(NotifeeReactUtils.INSTANCE, eventMap);
861
+ }
862
+
863
+ private static JavaOnlyMap diagnosticEventMap(String scenarioId, Object eventId) {
864
+ return diagnosticEventMap(scenarioId, eventId, NotificationEvent.TYPE_DELIVERED);
865
+ }
866
+
867
+ private static JavaOnlyMap diagnosticEventMap(
868
+ String scenarioId, Object eventId, Object eventType) {
869
+ JavaOnlyMap data =
870
+ JavaOnlyMap.of(
871
+ DIAGNOSTIC_SCENARIO_ID_KEY,
872
+ scenarioId,
873
+ DIAGNOSTIC_EVENT_ID_KEY,
874
+ eventId,
875
+ "consumerField",
876
+ "consumer-value");
877
+ JavaOnlyMap notification = JavaOnlyMap.of("id", "controlled-fixture", "data", data);
878
+ JavaOnlyMap detail = JavaOnlyMap.of("notification", notification);
879
+ return JavaOnlyMap.of("type", eventType, "detail", detail, "headless", false);
880
+ }
881
+
882
+ private static JavaOnlyMap diagnosticEventMapWithoutType(String scenarioId, Object eventId) {
883
+ JavaOnlyMap data =
884
+ JavaOnlyMap.of(
885
+ DIAGNOSTIC_SCENARIO_ID_KEY,
886
+ scenarioId,
887
+ DIAGNOSTIC_EVENT_ID_KEY,
888
+ eventId,
889
+ "consumerField",
890
+ "consumer-value");
891
+ JavaOnlyMap notification = JavaOnlyMap.of("id", "controlled-fixture", "data", data);
892
+ JavaOnlyMap detail = JavaOnlyMap.of("notification", notification);
893
+ return JavaOnlyMap.of("detail", detail, "headless", false);
894
+ }
895
+
896
+ private static List<String> pendingEventNames(List<?> pendingEvents) throws Exception {
897
+ List<String> names = new ArrayList<>();
898
+ for (Object pendingEvent : pendingEvents) {
899
+ names.add((String) ((Pair<?, ?>) pendingEvent).getFirst());
900
+ }
901
+ return names;
902
+ }
903
+
904
+ private static WritableMap pendingEventBody(Object pendingEvent) throws Exception {
905
+ return (WritableMap) ((Pair<?, ?>) pendingEvent).getSecond();
906
+ }
907
+
908
+ private static Object pendingDiagnosticsState() throws Exception {
909
+ Field field = NotifeeReactUtils.class.getDeclaredField("pendingDiagnosticsState");
910
+ field.setAccessible(true);
911
+ return field.get(NotifeeReactUtils.INSTANCE);
912
+ }
913
+
914
+ private static AtomicLong atomicLongField(Object target, String name) throws Exception {
915
+ return (AtomicLong) privateField(target, name);
916
+ }
917
+
918
+ private static boolean hasMarker(String marker) {
919
+ return ShadowLog.getLogsForTag(DIAGNOSTICS_TAG).stream()
920
+ .anyMatch(item -> item.msg.contains("marker=" + marker + " "));
921
+ }
922
+
923
+ private static String markerMessage(String marker) {
924
+ return ShadowLog.getLogsForTag(DIAGNOSTICS_TAG).stream()
925
+ .map(item -> item.msg)
926
+ .filter(message -> message.contains("marker=" + marker + " "))
927
+ .findFirst()
928
+ .orElse("");
929
+ }
930
+
931
+ private static List<String> markerFieldValues(String marker, String field) {
932
+ List<String> values = new ArrayList<>();
933
+ String fieldPrefix = field + "=";
934
+ for (ShadowLog.LogItem item : ShadowLog.getLogsForTag(DIAGNOSTICS_TAG)) {
935
+ if (!item.msg.contains("marker=" + marker + " ")) continue;
936
+ for (String token : item.msg.split(" ")) {
937
+ if (token.startsWith(fieldPrefix)) {
938
+ values.add(token.substring(fieldPrefix.length()));
939
+ break;
940
+ }
941
+ }
942
+ }
943
+ return values;
944
+ }
945
+
946
+ private static DiagnosticsContext diagnosticsContext(
947
+ boolean debuggable, boolean enabled, int delayMs) throws Exception {
948
+ Context context = mock(Context.class);
949
+ PackageManager packageManager = mock(PackageManager.class);
950
+ ApplicationInfo applicationInfo = new ApplicationInfo();
951
+ applicationInfo.flags = debuggable ? ApplicationInfo.FLAG_DEBUGGABLE : 0;
952
+ applicationInfo.metaData = new Bundle();
953
+ applicationInfo.metaData.putBoolean("notifee_pending_events_diagnostics_enabled", enabled);
954
+ applicationInfo.metaData.putInt("notifee_pending_events_delay_after_snapshot_ms", delayMs);
955
+ when(context.getPackageManager()).thenReturn(packageManager);
956
+ when(context.getPackageName()).thenReturn("com.notifeeexample");
957
+ when(packageManager.getApplicationInfo("com.notifeeexample", PackageManager.GET_META_DATA))
958
+ .thenReturn(applicationInfo);
959
+ return new DiagnosticsContext(context, packageManager);
960
+ }
961
+
962
+ private static Object privateField(Object target, String name) throws Exception {
963
+ Field field = target.getClass().getDeclaredField(name);
964
+ field.setAccessible(true);
965
+ return field.get(target);
966
+ }
967
+
968
+ private static void resetState() throws Exception {
969
+ List<?> events = pendingEvents();
970
+ synchronized (events) {
971
+ events.clear();
972
+ }
973
+ Field stateField = NotifeeReactUtils.class.getDeclaredField("pendingDiagnosticsState");
974
+ stateField.setAccessible(true);
975
+ stateField.set(NotifeeReactUtils.INSTANCE, null);
976
+ }
977
+
978
+ public static class TestApplication extends Application {
979
+ final TestReactHost reactHost = new TestReactHost();
980
+ int packageManagerRequests;
981
+
982
+ @Override
983
+ public PackageManager getPackageManager() {
984
+ packageManagerRequests += 1;
985
+ return super.getPackageManager();
986
+ }
987
+
988
+ public TestReactHost getReactHost() {
989
+ return reactHost;
990
+ }
991
+ }
992
+
993
+ public static class TestReactHost {
994
+ private final List<ReactContext> results = new ArrayList<>();
995
+ private int invocation;
996
+
997
+ public ReactContext getCurrentReactContext() {
998
+ if (results.isEmpty()) return null;
999
+ ReactContext result = results.get(Math.min(invocation, results.size() - 1));
1000
+ invocation += 1;
1001
+ return result;
1002
+ }
1003
+
1004
+ void setResults(ReactContext... contexts) {
1005
+ results.clear();
1006
+ results.addAll(Arrays.asList(contexts));
1007
+ invocation = 0;
1008
+ }
1009
+
1010
+ void reset() {
1011
+ results.clear();
1012
+ invocation = 0;
1013
+ }
1014
+ }
1015
+
1016
+ private static final class DiagnosticsContext {
1017
+ final Context context;
1018
+ final PackageManager packageManager;
1019
+
1020
+ DiagnosticsContext(Context context, PackageManager packageManager) {
1021
+ this.context = context;
1022
+ this.packageManager = packageManager;
1023
+ }
1024
+ }
1025
+ }