Add interaction-tracking/subscriptions (#13426)
* Removed enableInteractionTrackingObserver as a separate flag; only enableInteractionTracking is used now * Added interaction-tracking/subscriptions bundle and split tests * Added multi-subscriber support * Moved subscriptions behind feature flag * Fixed bug with wrap() parameters and added test * Replaced wrap arrow function
Brian Vaughn committed
Aug 17, 2018 at 14:45 UTC
0da5102cf02890a4e006f4d949ae4fbf2bfd85af
17 files changed
+856
-653
packages/interaction-tracking/package.json
+1
@@ -7,6 +7,7 @@
7
"LICENSE",
8
"README.md",
9
"index.js",
10
+ "subscriptions.js",
11
"cjs/",
12
"umd/"
13
],
packages/interaction-tracking/src/InteractionTracking.js
+81
-110
@@ -7,10 +7,7 @@
7
* @flow
8
*/
9
10
-import {
11
- enableInteractionTracking,
12
- enableInteractionTrackingObserver,
13
-} from 'shared/ReactFeatureFlags';
10
+import {enableInteractionTracking} from 'shared/ReactFeatureFlags';
11
12
export type Interaction = {|
13
__count: number,
@@ -71,18 +68,15 @@ let threadIDCounter: number = 0;
68
let interactionsRef: InteractionsRef = (null: any);
69
70
// Listener(s) to notify when interactions begin and end.
74
-// Note that subscribers are only supported when enableInteractionTrackingObserver is enabled.
71
let subscriberRef: SubscriberRef = (null: any);
72
73
if (enableInteractionTracking) {
74
interactionsRef = {
75
current: new Set(),
76
};
81
- if (enableInteractionTrackingObserver) {
82
- subscriberRef = {
83
- current: null,
84
- };
85
- }
77
+ subscriberRef = {
78
+ current: null,
79
+ };
80
}
81
82
// These values are exported for libraries with advanced use cases (i.e. React).
@@ -127,7 +121,7 @@ export function track(
121
}
122
123
const interaction: Interaction = {
130
- __count: 0,
124
+ __count: 1,
125
id: interactionIDCounter++,
126
name,
127
timestamp,
@@ -142,53 +136,42 @@ export function track(
136
interactions.add(interaction);
137
interactionsRef.current = interactions;
138
145
- if (enableInteractionTrackingObserver) {
146
- // Update before calling callback in case it schedules follow-up work.
147
- interaction.__count = 1;
148
-
149
- let returnValue;
150
- const subscriber = subscriberRef.current;
139
+ const subscriber = subscriberRef.current;
140
+ let returnValue;
141
142
+ try {
143
+ if (subscriber !== null) {
144
+ subscriber.onInteractionTracked(interaction);
145
+ }
146
+ } finally {
147
try {
148
if (subscriber !== null) {
154
- subscriber.onInteractionTracked(interaction);
149
+ subscriber.onWorkStarted(interactions, threadID);
150
}
151
} finally {
152
try {
158
- if (subscriber !== null) {
159
- subscriber.onWorkStarted(interactions, threadID);
160
- }
153
+ returnValue = callback();
154
} finally {
155
+ interactionsRef.current = prevInteractions;
156
+
157
try {
163
- returnValue = callback();
158
+ if (subscriber !== null) {
159
+ subscriber.onWorkStopped(interactions, threadID);
160
+ }
161
} finally {
165
- interactionsRef.current = prevInteractions;
162
+ interaction.__count--;
163
167
- try {
168
- if (subscriber !== null) {
169
- subscriber.onWorkStopped(interactions, threadID);
170
- }
171
- } finally {
172
- interaction.__count--;
173
-
174
- // If no async work was scheduled for this interaction,
175
- // Notify subscribers that it's completed.
176
- if (subscriber !== null && interaction.__count === 0) {
177
- subscriber.onInteractionScheduledWorkCompleted(interaction);
178
- }
164
+ // If no async work was scheduled for this interaction,
165
+ // Notify subscribers that it's completed.
166
+ if (subscriber !== null && interaction.__count === 0) {
167
+ subscriber.onInteractionScheduledWorkCompleted(interaction);
168
}
169
}
170
}
171
}
183
-
184
- return returnValue;
185
- } else {
186
- try {
187
- return callback();
188
- } finally {
189
- interactionsRef.current = prevInteractions;
190
- }
172
}
173
+
174
+ return returnValue;
175
}
176
177
export function wrap(
@@ -201,89 +184,77 @@ export function wrap(
184
185
const wrappedInteractions = interactionsRef.current;
186
204
- if (enableInteractionTrackingObserver) {
205
- const subscriber = subscriberRef.current;
206
- if (subscriber !== null) {
207
- subscriber.onWorkScheduled(wrappedInteractions, threadID);
208
- }
209
-
210
- // Update the pending async work count for the current interactions.
211
- // Update after calling subscribers in case of error.
212
- wrappedInteractions.forEach(interaction => {
213
- interaction.__count++;
214
- });
187
+ let subscriber = subscriberRef.current;
188
+ if (subscriber !== null) {
189
+ subscriber.onWorkScheduled(wrappedInteractions, threadID);
190
}
191
217
- const wrapped = () => {
192
+ // Update the pending async work count for the current interactions.
193
+ // Update after calling subscribers in case of error.
194
+ wrappedInteractions.forEach(interaction => {
195
+ interaction.__count++;
196
+ });
197
+
198
+ function wrapped() {
199
const prevInteractions = interactionsRef.current;
200
interactionsRef.current = wrappedInteractions;
201
221
- if (enableInteractionTrackingObserver) {
222
- const subscriber = subscriberRef.current;
202
+ subscriber = subscriberRef.current;
203
224
- try {
225
- let returnValue;
204
+ try {
205
+ let returnValue;
206
227
- try {
228
- if (subscriber !== null) {
229
- subscriber.onWorkStarted(wrappedInteractions, threadID);
230
- }
231
- } finally {
232
- try {
233
- returnValue = callback.apply(undefined, arguments);
234
- } finally {
235
- interactionsRef.current = prevInteractions;
236
-
237
- if (subscriber !== null) {
238
- subscriber.onWorkStopped(wrappedInteractions, threadID);
239
- }
240
- }
207
+ try {
208
+ if (subscriber !== null) {
209
+ subscriber.onWorkStarted(wrappedInteractions, threadID);
210
}
242
-
243
- return returnValue;
211
} finally {
245
- // Update pending async counts for all wrapped interactions.
246
- // If this was the last scheduled async work for any of them,
247
- // Mark them as completed.
248
- wrappedInteractions.forEach(interaction => {
249
- interaction.__count--;
212
+ try {
213
+ returnValue = callback.apply(undefined, arguments);
214
+ } finally {
215
+ interactionsRef.current = prevInteractions;
216
251
- if (subscriber !== null && interaction.__count === 0) {
252
- subscriber.onInteractionScheduledWorkCompleted(interaction);
217
+ if (subscriber !== null) {
218
+ subscriber.onWorkStopped(wrappedInteractions, threadID);
219
}
254
- });
255
- }
256
- } else {
257
- try {
258
- return callback.apply(undefined, arguments);
259
- } finally {
260
- interactionsRef.current = prevInteractions;
220
+ }
221
}
262
- }
263
- };
264
-
265
- if (enableInteractionTrackingObserver) {
266
- wrapped.cancel = () => {
267
- const subscriber = subscriberRef.current;
222
269
- try {
270
- if (subscriber !== null) {
271
- subscriber.onWorkCanceled(wrappedInteractions, threadID);
223
+ return returnValue;
224
+ } finally {
225
+ // Update pending async counts for all wrapped interactions.
226
+ // If this was the last scheduled async work for any of them,
227
+ // Mark them as completed.
228
+ wrappedInteractions.forEach(interaction => {
229
+ interaction.__count--;
230
+
231
+ if (subscriber !== null && interaction.__count === 0) {
232
+ subscriber.onInteractionScheduledWorkCompleted(interaction);
233
}
273
- } finally {
274
- // Update pending async counts for all wrapped interactions.
275
- // If this was the last scheduled async work for any of them,
276
- // Mark them as completed.
277
- wrappedInteractions.forEach(interaction => {
278
- interaction.__count--;
234
+ });
235
+ }
236
+ }
237
280
- if (subscriber && interaction.__count === 0) {
281
- subscriber.onInteractionScheduledWorkCompleted(interaction);
282
- }
283
- });
238
+ wrapped.cancel = function cancel() {
239
+ subscriber = subscriberRef.current;
240
+
241
+ try {
242
+ if (subscriber !== null) {
243
+ subscriber.onWorkCanceled(wrappedInteractions, threadID);
244
}
285
- };
286
- }
245
+ } finally {
246
+ // Update pending async counts for all wrapped interactions.
247
+ // If this was the last scheduled async work for any of them,
248
+ // Mark them as completed.
249
+ wrappedInteractions.forEach(interaction => {
250
+ interaction.__count--;
251
+
252
+ if (subscriber && interaction.__count === 0) {
253
+ subscriber.onInteractionScheduledWorkCompleted(interaction);
254
+ }
255
+ });
256
+ }
257
+ };
258
259
return wrapped;
260
}
packages/interaction-tracking/src/InteractionTrackingSubscriptions.js
new
+167
@@ -0,0 +1,167 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Interaction, Subscriber} from './InteractionTracking';
11
+
12
+import {enableInteractionTracking} from 'shared/ReactFeatureFlags';
13
+import {__subscriberRef} from 'interaction-tracking';
14
+
15
+let subscribers: Set<Subscriber> = (null: any);
16
+if (enableInteractionTracking) {
17
+ subscribers = new Set();
18
+}
19
+
20
+export function subscribe(subscriber: Subscriber): void {
21
+ if (enableInteractionTracking) {
22
+ subscribers.add(subscriber);
23
+ }
24
+}
25
+
26
+export function unsubscribe(subscriber: Subscriber): void {
27
+ if (enableInteractionTracking) {
28
+ subscribers.delete(subscriber);
29
+ }
30
+}
31
+
32
+function onInteractionTracked(interaction: Interaction): void {
33
+ let didCatchError = false;
34
+ let caughtError = null;
35
+
36
+ subscribers.forEach(subscriber => {
37
+ try {
38
+ subscriber.onInteractionTracked(interaction);
39
+ } catch (error) {
40
+ if (!didCatchError) {
41
+ didCatchError = true;
42
+ caughtError = error;
43
+ }
44
+ }
45
+ });
46
+
47
+ if (didCatchError) {
48
+ throw caughtError;
49
+ }
50
+}
51
+
52
+function onInteractionScheduledWorkCompleted(interaction: Interaction): void {
53
+ let didCatchError = false;
54
+ let caughtError = null;
55
+
56
+ subscribers.forEach(subscriber => {
57
+ try {
58
+ subscriber.onInteractionScheduledWorkCompleted(interaction);
59
+ } catch (error) {
60
+ if (!didCatchError) {
61
+ didCatchError = true;
62
+ caughtError = error;
63
+ }
64
+ }
65
+ });
66
+
67
+ if (didCatchError) {
68
+ throw caughtError;
69
+ }
70
+}
71
+
72
+function onWorkScheduled(
73
+ interactions: Set<Interaction>,
74
+ threadID: number,
75
+): void {
76
+ let didCatchError = false;
77
+ let caughtError = null;
78
+
79
+ subscribers.forEach(subscriber => {
80
+ try {
81
+ subscriber.onWorkScheduled(interactions, threadID);
82
+ } catch (error) {
83
+ if (!didCatchError) {
84
+ didCatchError = true;
85
+ caughtError = error;
86
+ }
87
+ }
88
+ });
89
+
90
+ if (didCatchError) {
91
+ throw caughtError;
92
+ }
93
+}
94
+
95
+function onWorkStarted(interactions: Set<Interaction>, threadID: number): void {
96
+ let didCatchError = false;
97
+ let caughtError = null;
98
+
99
+ subscribers.forEach(subscriber => {
100
+ try {
101
+ subscriber.onWorkStarted(interactions, threadID);
102
+ } catch (error) {
103
+ if (!didCatchError) {
104
+ didCatchError = true;
105
+ caughtError = error;
106
+ }
107
+ }
108
+ });
109
+
110
+ if (didCatchError) {
111
+ throw caughtError;
112
+ }
113
+}
114
+
115
+function onWorkStopped(interactions: Set<Interaction>, threadID: number): void {
116
+ let didCatchError = false;
117
+ let caughtError = null;
118
+
119
+ subscribers.forEach(subscriber => {
120
+ try {
121
+ subscriber.onWorkStopped(interactions, threadID);
122
+ } catch (error) {
123
+ if (!didCatchError) {
124
+ didCatchError = true;
125
+ caughtError = error;
126
+ }
127
+ }
128
+ });
129
+
130
+ if (didCatchError) {
131
+ throw caughtError;
132
+ }
133
+}
134
+
135
+function onWorkCanceled(
136
+ interactions: Set<Interaction>,
137
+ threadID: number,
138
+): void {
139
+ let didCatchError = false;
140
+ let caughtError = null;
141
+
142
+ subscribers.forEach(subscriber => {
143
+ try {
144
+ subscriber.onWorkCanceled(interactions, threadID);
145
+ } catch (error) {
146
+ if (!didCatchError) {
147
+ didCatchError = true;
148
+ caughtError = error;
149
+ }
150
+ }
151
+ });
152
+
153
+ if (didCatchError) {
154
+ throw caughtError;
155
+ }
156
+}
157
+
158
+if (enableInteractionTracking) {
159
+ __subscriberRef.current = {
160
+ onInteractionScheduledWorkCompleted,
161
+ onInteractionTracked,
162
+ onWorkCanceled,
163
+ onWorkScheduled,
164
+ onWorkStarted,
165
+ onWorkStopped,
166
+ };
167
+}
packages/interaction-tracking/src/__tests__/InteractionTracking-test.internal.js
+23
-532
@@ -15,10 +15,7 @@ describe('InteractionTracking', () => {
15
let advanceTimeBy;
16
let currentTime;
17
18
- function loadModules({
19
- enableInteractionTracking,
20
- enableInteractionTrackingObserver,
21
- }) {
18
+ function loadModules({enableInteractionTracking}) {
19
jest.resetModules();
20
jest.useFakeTimers();
21
@@ -31,7 +28,6 @@ describe('InteractionTracking', () => {
28
29
ReactFeatureFlags = require('shared/ReactFeatureFlags');
30
ReactFeatureFlags.enableInteractionTracking = enableInteractionTracking;
34
- ReactFeatureFlags.enableInteractionTrackingObserver = enableInteractionTrackingObserver;
31
32
InteractionTracking = require('interaction-tracking');
33
}
@@ -57,6 +53,18 @@ describe('InteractionTracking', () => {
53
expect(wrapped()).toBe(123);
54
});
55
56
+ it('should pass arguments through to a wrapped function', done => {
57
+ let wrapped;
58
+ InteractionTracking.track('arbitrary', currentTime, () => {
59
+ wrapped = InteractionTracking.wrap((param1, param2) => {
60
+ expect(param1).toBe('foo');
61
+ expect(param2).toBe('bar');
62
+ done();
63
+ });
64
+ });
65
+ wrapped('foo', 'bar');
66
+ });
67
+
68
it('should return an empty set when outside of a tracked event', () => {
69
expect(InteractionTracking.getCurrent()).toContainNoInteractions();
70
});
@@ -277,6 +285,12 @@ describe('InteractionTracking', () => {
285
});
286
287
describe('advanced integration', () => {
288
+ it('should return a unique threadID per request', () => {
289
+ expect(InteractionTracking.getThreadID()).not.toBe(
290
+ InteractionTracking.getThreadID(),
291
+ );
292
+ });
293
+
294
it('should expose the current set of interactions to be externally manipulated', () => {
295
InteractionTracking.track('outer event', currentTime, () => {
296
expect(InteractionTracking.__interactionsRef.current).toBe(
@@ -292,536 +306,13 @@ describe('InteractionTracking', () => {
306
]);
307
});
308
});
295
- });
296
-
297
- describe('interaction subscribers enabled', () => {
298
- let onInteractionScheduledWorkCompleted;
299
- let onInteractionTracked;
300
- let onWorkCanceled;
301
- let onWorkScheduled;
302
- let onWorkStarted;
303
- let onWorkStopped;
304
- let subscriber;
305
- let throwInOnInteractionScheduledWorkCompleted;
306
- let throwInOnInteractionTracked;
307
- let throwInOnWorkCanceled;
308
- let throwInOnWorkScheduled;
309
- let throwInOnWorkStarted;
310
- let throwInOnWorkStopped;
311
-
312
- const firstEvent = {id: 0, name: 'first', timestamp: 0};
313
- const secondEvent = {id: 1, name: 'second', timestamp: 0};
314
- const threadID = 123;
315
-
316
- beforeEach(() => {
317
- throwInOnInteractionScheduledWorkCompleted = false;
318
- throwInOnInteractionTracked = false;
319
- throwInOnWorkCanceled = false;
320
- throwInOnWorkScheduled = false;
321
- throwInOnWorkStarted = false;
322
- throwInOnWorkStopped = false;
323
-
324
- onInteractionScheduledWorkCompleted = jest.fn(() => {
325
- if (throwInOnInteractionScheduledWorkCompleted) {
326
- throw Error('Expected error onInteractionScheduledWorkCompleted');
327
- }
328
- });
329
- onInteractionTracked = jest.fn(() => {
330
- if (throwInOnInteractionTracked) {
331
- throw Error('Expected error onInteractionTracked');
332
- }
333
- });
334
- onWorkCanceled = jest.fn(() => {
335
- if (throwInOnWorkCanceled) {
336
- throw Error('Expected error onWorkCanceled');
337
- }
338
- });
339
- onWorkScheduled = jest.fn(() => {
340
- if (throwInOnWorkScheduled) {
341
- throw Error('Expected error onWorkScheduled');
342
- }
343
- });
344
- onWorkStarted = jest.fn(() => {
345
- if (throwInOnWorkStarted) {
346
- throw Error('Expected error onWorkStarted');
347
- }
348
- });
349
- onWorkStopped = jest.fn(() => {
350
- if (throwInOnWorkStopped) {
351
- throw Error('Expected error onWorkStopped');
352
- }
353
- });
354
- });
355
-
356
- describe('enableInteractionTrackingObserver enabled', () => {
357
- beforeEach(() => {
358
- loadModules({
359
- enableInteractionTracking: true,
360
- enableInteractionTrackingObserver: true,
361
- });
362
-
363
- subscriber = {
364
- onInteractionScheduledWorkCompleted,
365
- onInteractionTracked,
366
- onWorkCanceled,
367
- onWorkScheduled,
368
- onWorkStarted,
369
- onWorkStopped,
370
- };
371
-
372
- InteractionTracking.__subscriberRef.current = subscriber;
373
- });
374
-
375
- it('should return the value of a tracked function', () => {
376
- expect(
377
- InteractionTracking.track('arbitrary', currentTime, () => 123),
378
- ).toBe(123);
379
- });
380
-
381
- it('should return the value of a wrapped function', () => {
382
- let wrapped;
383
- InteractionTracking.track('arbitrary', currentTime, () => {
384
- wrapped = InteractionTracking.wrap(() => 123);
385
- });
386
- expect(wrapped()).toBe(123);
387
- });
388
-
389
- describe('error handling', () => {
390
- it('should cover onInteractionTracked/onWorkStarted within', done => {
391
- InteractionTracking.track(firstEvent.name, currentTime, () => {
392
- const mock = jest.fn();
393
-
394
- // It should call the callback before re-throwing
395
- throwInOnInteractionTracked = true;
396
- expect(() =>
397
- InteractionTracking.track(
398
- secondEvent.name,
399
- currentTime,
400
- mock,
401
- threadID,
402
- ),
403
- ).toThrow('Expected error onInteractionTracked');
404
- throwInOnInteractionTracked = false;
405
- expect(mock).toHaveBeenCalledTimes(1);
406
-
407
- throwInOnWorkStarted = true;
408
- expect(() =>
409
- InteractionTracking.track(
410
- secondEvent.name,
411
- currentTime,
412
- mock,
413
- threadID,
414
- ),
415
- ).toThrow('Expected error onWorkStarted');
416
- expect(mock).toHaveBeenCalledTimes(2);
417
-
418
- // It should restore the previous/outer interactions
419
- expect(InteractionTracking.getCurrent()).toMatchInteractions([
420
- firstEvent,
421
- ]);
422
-
423
- done();
424
- });
425
- });
426
-
427
- it('should cover onWorkStopped within track', done => {
428
- InteractionTracking.track(firstEvent.name, currentTime, () => {
429
- let innerInteraction;
430
- const mock = jest.fn(() => {
431
- innerInteraction = Array.from(
432
- InteractionTracking.getCurrent(),
433
- )[1];
434
- });
435
-
436
- throwInOnWorkStopped = true;
437
- expect(() =>
438
- InteractionTracking.track(secondEvent.name, currentTime, mock),
439
- ).toThrow('Expected error onWorkStopped');
440
- throwInOnWorkStopped = false;
441
-
442
- // It should restore the previous/outer interactions
443
- expect(InteractionTracking.getCurrent()).toMatchInteractions([
444
- firstEvent,
445
- ]);
446
-
447
- // It should update the interaction count so as not to interfere with subsequent calls
448
- expect(innerInteraction.__count).toBe(0);
449
-
450
- done();
451
- });
452
- });
453
-
454
- it('should cover the callback within track', done => {
455
- expect(onWorkStarted).not.toHaveBeenCalled();
456
- expect(onWorkStopped).not.toHaveBeenCalled();
457
-
458
- expect(() => {
459
- InteractionTracking.track(firstEvent.name, currentTime, () => {
460
- throw Error('Expected error callback');
461
- });
462
- }).toThrow('Expected error callback');
463
-
464
- expect(onWorkStarted).toHaveBeenCalledTimes(1);
465
- expect(onWorkStopped).toHaveBeenCalledTimes(1);
466
-
467
- done();
468
- });
469
-
470
- it('should cover onWorkScheduled within wrap', done => {
471
- InteractionTracking.track(firstEvent.name, currentTime, () => {
472
- const interaction = Array.from(
473
- InteractionTracking.getCurrent(),
474
- )[0];
475
- const beforeCount = interaction.__count;
476
-
477
- throwInOnWorkScheduled = true;
478
- expect(() => InteractionTracking.wrap(() => {})).toThrow(
479
- 'Expected error onWorkScheduled',
480
- );
481
-
482
- // It should not update the interaction count so as not to interfere with subsequent calls
483
- expect(interaction.__count).toBe(beforeCount);
484
-
485
- done();
486
- });
487
- });
488
-
489
- it('should cover onWorkStarted within wrap', () => {
490
- const mock = jest.fn();
491
- let interaction, wrapped;
492
- InteractionTracking.track(firstEvent.name, currentTime, () => {
493
- interaction = Array.from(InteractionTracking.getCurrent())[0];
494
- wrapped = InteractionTracking.wrap(mock);
495
- });
496
- expect(interaction.__count).toBe(1);
497
-
498
- throwInOnWorkStarted = true;
499
- expect(wrapped).toThrow('Expected error onWorkStarted');
500
-
501
- // It should call the callback before re-throwing
502
- expect(mock).toHaveBeenCalledTimes(1);
503
-
504
- // It should update the interaction count so as not to interfere with subsequent calls
505
- expect(interaction.__count).toBe(0);
506
- });
507
-
508
- it('should cover onWorkStopped within wrap', done => {
509
- InteractionTracking.track(firstEvent.name, currentTime, () => {
510
- const outerInteraction = Array.from(
511
- InteractionTracking.getCurrent(),
512
- )[0];
513
- expect(outerInteraction.__count).toBe(1);
514
-
515
- let wrapped;
516
- let innerInteraction;
517
-
518
- InteractionTracking.track(secondEvent.name, currentTime, () => {
519
- innerInteraction = Array.from(
520
- InteractionTracking.getCurrent(),
521
- )[1];
522
- expect(outerInteraction.__count).toBe(1);
523
- expect(innerInteraction.__count).toBe(1);
524
-
525
- wrapped = InteractionTracking.wrap(jest.fn());
526
- expect(outerInteraction.__count).toBe(2);
527
- expect(innerInteraction.__count).toBe(2);
528
- });
529
-
530
- expect(outerInteraction.__count).toBe(2);
531
- expect(innerInteraction.__count).toBe(1);
532
-
533
- throwInOnWorkStopped = true;
534
- expect(wrapped).toThrow('Expected error onWorkStopped');
535
- throwInOnWorkStopped = false;
536
-
537
- // It should restore the previous interactions
538
- expect(InteractionTracking.getCurrent()).toMatchInteractions([
539
- outerInteraction,
540
- ]);
541
-
542
- // It should update the interaction count so as not to interfere with subsequent calls
543
- expect(outerInteraction.__count).toBe(1);
544
- expect(innerInteraction.__count).toBe(0);
545
-
546
- done();
547
- });
548
- });
549
-
550
- it('should cover the callback within wrap', done => {
551
- expect(onWorkStarted).not.toHaveBeenCalled();
552
- expect(onWorkStopped).not.toHaveBeenCalled();
553
-
554
- let wrapped;
555
- let interaction;
556
- InteractionTracking.track(firstEvent.name, currentTime, () => {
557
- interaction = Array.from(InteractionTracking.getCurrent())[0];
558
- wrapped = InteractionTracking.wrap(() => {
559
- throw Error('Expected error wrap');
560
- });
561
- });
562
-
563
- expect(onWorkStarted).toHaveBeenCalledTimes(1);
564
- expect(onWorkStopped).toHaveBeenCalledTimes(1);
309
566
- expect(wrapped).toThrow('Expected error wrap');
567
-
568
- expect(onWorkStarted).toHaveBeenCalledTimes(2);
569
- expect(onWorkStopped).toHaveBeenCalledTimes(2);
570
- expect(onWorkStopped).toHaveBeenLastNotifiedOfWork([interaction]);
571
-
572
- done();
573
- });
574
-
575
- it('should cover onWorkCanceled within wrap', () => {
576
- let interaction, wrapped;
577
- InteractionTracking.track(firstEvent.name, currentTime, () => {
578
- interaction = Array.from(InteractionTracking.getCurrent())[0];
579
- wrapped = InteractionTracking.wrap(jest.fn());
580
- });
581
- expect(interaction.__count).toBe(1);
582
-
583
- throwInOnWorkCanceled = true;
584
- expect(wrapped.cancel).toThrow('Expected error onWorkCanceled');
585
-
586
- expect(onWorkCanceled).toHaveBeenCalledTimes(1);
587
-
588
- // It should update the interaction count so as not to interfere with subsequent calls
589
- expect(interaction.__count).toBe(0);
590
- expect(
591
- onInteractionScheduledWorkCompleted,
592
- ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
593
- });
594
- });
595
-
596
- it('calls lifecycle methods for track', () => {
597
- expect(onInteractionTracked).not.toHaveBeenCalled();
598
- expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
599
-
600
- InteractionTracking.track(
601
- firstEvent.name,
602
- currentTime,
603
- () => {
604
- expect(onInteractionTracked).toHaveBeenCalledTimes(1);
605
- expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
606
- firstEvent,
607
- );
608
- expect(
609
- onInteractionScheduledWorkCompleted,
610
- ).not.toHaveBeenCalled();
611
- expect(onWorkStarted).toHaveBeenCalledTimes(1);
612
- expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
613
- new Set([firstEvent]),
614
- threadID,
615
- );
616
- expect(onWorkStopped).not.toHaveBeenCalled();
617
-
618
- InteractionTracking.track(
619
- secondEvent.name,
620
- currentTime,
621
- () => {
622
- expect(onInteractionTracked).toHaveBeenCalledTimes(2);
623
- expect(
624
- onInteractionTracked,
625
- ).toHaveBeenLastNotifiedOfInteraction(secondEvent);
626
- expect(
627
- onInteractionScheduledWorkCompleted,
628
- ).not.toHaveBeenCalled();
629
- expect(onWorkStarted).toHaveBeenCalledTimes(2);
630
- expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
631
- new Set([firstEvent, secondEvent]),
632
- threadID,
633
- );
634
- expect(onWorkStopped).not.toHaveBeenCalled();
635
- },
636
- threadID,
637
- );
638
-
639
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(
640
- 1,
641
- );
642
- expect(
643
- onInteractionScheduledWorkCompleted,
644
- ).toHaveBeenLastNotifiedOfInteraction(secondEvent);
645
- expect(onWorkStopped).toHaveBeenCalledTimes(1);
646
- expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
647
- new Set([firstEvent, secondEvent]),
648
- threadID,
649
- );
650
- },
651
- threadID,
652
- );
653
-
654
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
655
- expect(
656
- onInteractionScheduledWorkCompleted,
657
- ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
658
- expect(onWorkScheduled).not.toHaveBeenCalled();
659
- expect(onWorkCanceled).not.toHaveBeenCalled();
660
- expect(onWorkStarted).toHaveBeenCalledTimes(2);
661
- expect(onWorkStopped).toHaveBeenCalledTimes(2);
662
- expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
663
- new Set([firstEvent]),
664
- threadID,
665
- );
666
- });
667
-
668
- it('calls lifecycle methods for wrap', () => {
669
- const unwrapped = jest.fn();
670
- let wrapped;
671
-
672
- InteractionTracking.track(firstEvent.name, currentTime, () => {
673
- expect(onInteractionTracked).toHaveBeenCalledTimes(1);
674
- expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
675
- firstEvent,
676
- );
677
-
678
- InteractionTracking.track(secondEvent.name, currentTime, () => {
679
- expect(onInteractionTracked).toHaveBeenCalledTimes(2);
680
- expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
681
- secondEvent,
682
- );
683
-
684
- wrapped = InteractionTracking.wrap(unwrapped, threadID);
685
- expect(onWorkScheduled).toHaveBeenCalledTimes(1);
686
- expect(onWorkScheduled).toHaveBeenLastNotifiedOfWork(
687
- new Set([firstEvent, secondEvent]),
688
- threadID,
689
- );
690
- });
691
- });
692
-
693
- expect(onInteractionTracked).toHaveBeenCalledTimes(2);
694
- expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
695
-
696
- wrapped();
697
- expect(unwrapped).toHaveBeenCalled();
698
-
699
- expect(onWorkScheduled).toHaveBeenCalledTimes(1);
700
- expect(onWorkCanceled).not.toHaveBeenCalled();
701
- expect(onWorkStarted).toHaveBeenCalledTimes(3);
702
- expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
703
- new Set([firstEvent, secondEvent]),
704
- threadID,
705
- );
706
- expect(onWorkStopped).toHaveBeenCalledTimes(3);
707
- expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
708
- new Set([firstEvent, secondEvent]),
709
- threadID,
710
- );
711
-
712
- expect(
713
- onInteractionScheduledWorkCompleted.mock.calls[0][0],
714
- ).toMatchInteraction(firstEvent);
715
- expect(
716
- onInteractionScheduledWorkCompleted.mock.calls[1][0],
717
- ).toMatchInteraction(secondEvent);
718
- });
719
-
720
- it('should call the correct interaction subscriber methods when a wrapped callback is canceled', () => {
721
- const fnOne = jest.fn();
722
- const fnTwo = jest.fn();
723
- let wrappedOne, wrappedTwo;
724
- InteractionTracking.track(firstEvent.name, currentTime, () => {
725
- wrappedOne = InteractionTracking.wrap(fnOne, threadID);
726
- InteractionTracking.track(secondEvent.name, currentTime, () => {
727
- wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
728
- });
729
- });
730
-
731
- expect(onInteractionTracked).toHaveBeenCalledTimes(2);
732
- expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
733
- expect(onWorkCanceled).not.toHaveBeenCalled();
734
- expect(onWorkStarted).toHaveBeenCalledTimes(2);
735
- expect(onWorkStopped).toHaveBeenCalledTimes(2);
736
-
737
- wrappedTwo.cancel();
738
-
739
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
740
- expect(
741
- onInteractionScheduledWorkCompleted,
742
- ).toHaveBeenLastNotifiedOfInteraction(secondEvent);
743
- expect(onWorkCanceled).toHaveBeenCalledTimes(1);
744
- expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
745
- new Set([firstEvent, secondEvent]),
746
- threadID,
747
- );
748
-
749
- wrappedOne.cancel();
750
-
751
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
752
- expect(
753
- onInteractionScheduledWorkCompleted,
754
- ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
755
- expect(onWorkCanceled).toHaveBeenCalledTimes(2);
756
- expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
757
- new Set([firstEvent]),
758
- threadID,
759
- );
760
-
761
- expect(fnOne).not.toHaveBeenCalled();
762
- expect(fnTwo).not.toHaveBeenCalled();
763
- });
764
-
765
- it('should not end an interaction twice if wrap is used to schedule follow up work within another wrap', () => {
766
- const fnOne = jest.fn(() => {
767
- wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
768
- });
769
- const fnTwo = jest.fn();
770
- let wrappedOne, wrappedTwo;
771
- InteractionTracking.track(firstEvent.name, currentTime, () => {
772
- wrappedOne = InteractionTracking.wrap(fnOne, threadID);
773
- });
774
-
775
- expect(onInteractionTracked).toHaveBeenCalledTimes(1);
776
- expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
777
-
778
- wrappedOne();
779
-
780
- expect(onInteractionTracked).toHaveBeenCalledTimes(1);
781
- expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
782
-
783
- wrappedTwo();
784
-
785
- expect(onInteractionTracked).toHaveBeenCalledTimes(1);
786
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
787
- expect(
788
- onInteractionScheduledWorkCompleted,
789
- ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
790
- });
791
-
792
- it('should unsubscribe', () => {
793
- InteractionTracking.__subscriberRef.current = null;
794
- InteractionTracking.track(firstEvent.name, currentTime, () => {});
795
-
796
- expect(onInteractionTracked).not.toHaveBeenCalled();
797
- });
798
-
799
- describe('advanced integration', () => {
800
- it('should return a unique threadID per request', () => {
801
- expect(InteractionTracking.getThreadID()).not.toBe(
802
- InteractionTracking.getThreadID(),
803
- );
804
- });
805
-
806
- it('should expose the current set of interaction subscribers to be called externally', () => {
807
- expect(
808
- InteractionTracking.__subscriberRef.current.onInteractionTracked,
809
- ).toBe(onInteractionTracked);
810
- });
811
- });
812
- });
813
-
814
- describe('enableInteractionTrackingObserver disabled', () => {
815
- beforeEach(() => {
816
- loadModules({
817
- enableInteractionTracking: true,
818
- enableInteractionTrackingObserver: false,
310
+ it('should expose a subscriber ref to be externally manipulated', () => {
311
+ InteractionTracking.track('outer event', currentTime, () => {
312
+ expect(InteractionTracking.__subscriberRef).toEqual({
313
+ current: null,
314
});
315
});
821
-
822
- it('should not create unnecessary objects', () => {
823
- expect(InteractionTracking.__subscriberRef).toBe(null);
824
- });
316
});
317
});
318
});
packages/interaction-tracking/src/__tests__/InteractionTrackingSubscriptions-test.internal.js
new
+562
@@ -0,0 +1,562 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @jest-environment node
8
+ */
9
+'use strict';
10
+
11
+describe('InteractionTracking', () => {
12
+ let InteractionTracking;
13
+ let InteractionTrackingSubscriptions;
14
+ let ReactFeatureFlags;
15
+
16
+ let currentTime;
17
+
18
+ let onInteractionScheduledWorkCompleted;
19
+ let onInteractionTracked;
20
+ let onWorkCanceled;
21
+ let onWorkScheduled;
22
+ let onWorkStarted;
23
+ let onWorkStopped;
24
+ let throwInOnInteractionScheduledWorkCompleted;
25
+ let throwInOnInteractionTracked;
26
+ let throwInOnWorkCanceled;
27
+ let throwInOnWorkScheduled;
28
+ let throwInOnWorkStarted;
29
+ let throwInOnWorkStopped;
30
+ let firstSubscriber;
31
+ let secondSubscriber;
32
+
33
+ const firstEvent = {id: 0, name: 'first', timestamp: 0};
34
+ const secondEvent = {id: 1, name: 'second', timestamp: 0};
35
+ const threadID = 123;
36
+
37
+ function loadModules({enableInteractionTracking}) {
38
+ jest.resetModules();
39
+ jest.useFakeTimers();
40
+
41
+ currentTime = 0;
42
+
43
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
44
+ ReactFeatureFlags.enableInteractionTracking = enableInteractionTracking;
45
+
46
+ InteractionTracking = require('interaction-tracking');
47
+ InteractionTrackingSubscriptions = require('interaction-tracking/subscriptions');
48
+
49
+ throwInOnInteractionScheduledWorkCompleted = false;
50
+ throwInOnInteractionTracked = false;
51
+ throwInOnWorkCanceled = false;
52
+ throwInOnWorkScheduled = false;
53
+ throwInOnWorkStarted = false;
54
+ throwInOnWorkStopped = false;
55
+
56
+ onInteractionScheduledWorkCompleted = jest.fn(() => {
57
+ if (throwInOnInteractionScheduledWorkCompleted) {
58
+ throw Error('Expected error onInteractionScheduledWorkCompleted');
59
+ }
60
+ });
61
+ onInteractionTracked = jest.fn(() => {
62
+ if (throwInOnInteractionTracked) {
63
+ throw Error('Expected error onInteractionTracked');
64
+ }
65
+ });
66
+ onWorkCanceled = jest.fn(() => {
67
+ if (throwInOnWorkCanceled) {
68
+ throw Error('Expected error onWorkCanceled');
69
+ }
70
+ });
71
+ onWorkScheduled = jest.fn(() => {
72
+ if (throwInOnWorkScheduled) {
73
+ throw Error('Expected error onWorkScheduled');
74
+ }
75
+ });
76
+ onWorkStarted = jest.fn(() => {
77
+ if (throwInOnWorkStarted) {
78
+ throw Error('Expected error onWorkStarted');
79
+ }
80
+ });
81
+ onWorkStopped = jest.fn(() => {
82
+ if (throwInOnWorkStopped) {
83
+ throw Error('Expected error onWorkStopped');
84
+ }
85
+ });
86
+
87
+ firstSubscriber = {
88
+ onInteractionScheduledWorkCompleted,
89
+ onInteractionTracked,
90
+ onWorkCanceled,
91
+ onWorkScheduled,
92
+ onWorkStarted,
93
+ onWorkStopped,
94
+ };
95
+
96
+ secondSubscriber = {
97
+ onInteractionScheduledWorkCompleted: jest.fn(),
98
+ onInteractionTracked: jest.fn(),
99
+ onWorkCanceled: jest.fn(),
100
+ onWorkScheduled: jest.fn(),
101
+ onWorkStarted: jest.fn(),
102
+ onWorkStopped: jest.fn(),
103
+ };
104
+
105
+ InteractionTrackingSubscriptions.subscribe(firstSubscriber);
106
+ InteractionTrackingSubscriptions.subscribe(secondSubscriber);
107
+ }
108
+
109
+ describe('enabled', () => {
110
+ beforeEach(() => loadModules({enableInteractionTracking: true}));
111
+
112
+ describe('error handling', () => {
113
+ it('should cover onInteractionTracked/onWorkStarted within', done => {
114
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
115
+ const mock = jest.fn();
116
+
117
+ // It should call the callback before re-throwing
118
+ throwInOnInteractionTracked = true;
119
+ expect(() =>
120
+ InteractionTracking.track(
121
+ secondEvent.name,
122
+ currentTime,
123
+ mock,
124
+ threadID,
125
+ ),
126
+ ).toThrow('Expected error onInteractionTracked');
127
+ throwInOnInteractionTracked = false;
128
+ expect(mock).toHaveBeenCalledTimes(1);
129
+
130
+ throwInOnWorkStarted = true;
131
+ expect(() =>
132
+ InteractionTracking.track(
133
+ secondEvent.name,
134
+ currentTime,
135
+ mock,
136
+ threadID,
137
+ ),
138
+ ).toThrow('Expected error onWorkStarted');
139
+ expect(mock).toHaveBeenCalledTimes(2);
140
+
141
+ // It should restore the previous/outer interactions
142
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
143
+ firstEvent,
144
+ ]);
145
+
146
+ // It should call other subscribers despite the earlier error
147
+ expect(secondSubscriber.onInteractionTracked).toHaveBeenCalledTimes(
148
+ 3,
149
+ );
150
+ expect(secondSubscriber.onWorkStarted).toHaveBeenCalledTimes(3);
151
+
152
+ done();
153
+ });
154
+ });
155
+
156
+ it('should cover onWorkStopped within track', done => {
157
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
158
+ let innerInteraction;
159
+ const mock = jest.fn(() => {
160
+ innerInteraction = Array.from(InteractionTracking.getCurrent())[1];
161
+ });
162
+
163
+ throwInOnWorkStopped = true;
164
+ expect(() =>
165
+ InteractionTracking.track(secondEvent.name, currentTime, mock),
166
+ ).toThrow('Expected error onWorkStopped');
167
+ throwInOnWorkStopped = false;
168
+
169
+ // It should restore the previous/outer interactions
170
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
171
+ firstEvent,
172
+ ]);
173
+
174
+ // It should update the interaction count so as not to interfere with subsequent calls
175
+ expect(innerInteraction.__count).toBe(0);
176
+
177
+ // It should call other subscribers despite the earlier error
178
+ expect(secondSubscriber.onWorkStopped).toHaveBeenCalledTimes(1);
179
+
180
+ done();
181
+ });
182
+ });
183
+
184
+ it('should cover onInteractionScheduledWorkCompleted within track', done => {
185
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
186
+ const mock = jest.fn();
187
+
188
+ throwInOnInteractionScheduledWorkCompleted = true;
189
+ expect(() =>
190
+ InteractionTracking.track(secondEvent.name, currentTime, mock),
191
+ ).toThrow('Expected error onInteractionScheduledWorkCompleted');
192
+ throwInOnInteractionScheduledWorkCompleted = false;
193
+
194
+ // It should restore the previous/outer interactions
195
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
196
+ firstEvent,
197
+ ]);
198
+
199
+ // It should call other subscribers despite the earlier error
200
+ expect(
201
+ secondSubscriber.onInteractionScheduledWorkCompleted,
202
+ ).toHaveBeenCalledTimes(1);
203
+
204
+ done();
205
+ });
206
+ });
207
+
208
+ it('should cover the callback within track', done => {
209
+ expect(onWorkStarted).not.toHaveBeenCalled();
210
+ expect(onWorkStopped).not.toHaveBeenCalled();
211
+
212
+ expect(() => {
213
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
214
+ throw Error('Expected error callback');
215
+ });
216
+ }).toThrow('Expected error callback');
217
+
218
+ expect(onWorkStarted).toHaveBeenCalledTimes(1);
219
+ expect(onWorkStopped).toHaveBeenCalledTimes(1);
220
+
221
+ done();
222
+ });
223
+
224
+ it('should cover onWorkScheduled within wrap', done => {
225
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
226
+ const interaction = Array.from(InteractionTracking.getCurrent())[0];
227
+ const beforeCount = interaction.__count;
228
+
229
+ throwInOnWorkScheduled = true;
230
+ expect(() => InteractionTracking.wrap(() => {})).toThrow(
231
+ 'Expected error onWorkScheduled',
232
+ );
233
+
234
+ // It should not update the interaction count so as not to interfere with subsequent calls
235
+ expect(interaction.__count).toBe(beforeCount);
236
+
237
+ // It should call other subscribers despite the earlier error
238
+ expect(secondSubscriber.onWorkScheduled).toHaveBeenCalledTimes(1);
239
+
240
+ done();
241
+ });
242
+ });
243
+
244
+ it('should cover onWorkStarted within wrap', () => {
245
+ const mock = jest.fn();
246
+ let interaction, wrapped;
247
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
248
+ interaction = Array.from(InteractionTracking.getCurrent())[0];
249
+ wrapped = InteractionTracking.wrap(mock);
250
+ });
251
+ expect(interaction.__count).toBe(1);
252
+
253
+ throwInOnWorkStarted = true;
254
+ expect(wrapped).toThrow('Expected error onWorkStarted');
255
+
256
+ // It should call the callback before re-throwing
257
+ expect(mock).toHaveBeenCalledTimes(1);
258
+
259
+ // It should update the interaction count so as not to interfere with subsequent calls
260
+ expect(interaction.__count).toBe(0);
261
+
262
+ // It should call other subscribers despite the earlier error
263
+ expect(secondSubscriber.onWorkStarted).toHaveBeenCalledTimes(2);
264
+ });
265
+
266
+ it('should cover onWorkStopped within wrap', done => {
267
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
268
+ const outerInteraction = Array.from(
269
+ InteractionTracking.getCurrent(),
270
+ )[0];
271
+ expect(outerInteraction.__count).toBe(1);
272
+
273
+ let wrapped;
274
+ let innerInteraction;
275
+
276
+ InteractionTracking.track(secondEvent.name, currentTime, () => {
277
+ innerInteraction = Array.from(InteractionTracking.getCurrent())[1];
278
+ expect(outerInteraction.__count).toBe(1);
279
+ expect(innerInteraction.__count).toBe(1);
280
+
281
+ wrapped = InteractionTracking.wrap(jest.fn());
282
+ expect(outerInteraction.__count).toBe(2);
283
+ expect(innerInteraction.__count).toBe(2);
284
+ });
285
+
286
+ expect(outerInteraction.__count).toBe(2);
287
+ expect(innerInteraction.__count).toBe(1);
288
+
289
+ throwInOnWorkStopped = true;
290
+ expect(wrapped).toThrow('Expected error onWorkStopped');
291
+ throwInOnWorkStopped = false;
292
+
293
+ // It should restore the previous interactions
294
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
295
+ outerInteraction,
296
+ ]);
297
+
298
+ // It should update the interaction count so as not to interfere with subsequent calls
299
+ expect(outerInteraction.__count).toBe(1);
300
+ expect(innerInteraction.__count).toBe(0);
301
+
302
+ expect(secondSubscriber.onWorkStopped).toHaveBeenCalledTimes(2);
303
+
304
+ done();
305
+ });
306
+ });
307
+
308
+ it('should cover the callback within wrap', done => {
309
+ expect(onWorkStarted).not.toHaveBeenCalled();
310
+ expect(onWorkStopped).not.toHaveBeenCalled();
311
+
312
+ let wrapped;
313
+ let interaction;
314
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
315
+ interaction = Array.from(InteractionTracking.getCurrent())[0];
316
+ wrapped = InteractionTracking.wrap(() => {
317
+ throw Error('Expected error wrap');
318
+ });
319
+ });
320
+
321
+ expect(onWorkStarted).toHaveBeenCalledTimes(1);
322
+ expect(onWorkStopped).toHaveBeenCalledTimes(1);
323
+
324
+ expect(wrapped).toThrow('Expected error wrap');
325
+
326
+ expect(onWorkStarted).toHaveBeenCalledTimes(2);
327
+ expect(onWorkStopped).toHaveBeenCalledTimes(2);
328
+ expect(onWorkStopped).toHaveBeenLastNotifiedOfWork([interaction]);
329
+
330
+ done();
331
+ });
332
+
333
+ it('should cover onWorkCanceled within wrap', () => {
334
+ let interaction, wrapped;
335
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
336
+ interaction = Array.from(InteractionTracking.getCurrent())[0];
337
+ wrapped = InteractionTracking.wrap(jest.fn());
338
+ });
339
+ expect(interaction.__count).toBe(1);
340
+
341
+ throwInOnWorkCanceled = true;
342
+ expect(wrapped.cancel).toThrow('Expected error onWorkCanceled');
343
+
344
+ expect(onWorkCanceled).toHaveBeenCalledTimes(1);
345
+
346
+ // It should update the interaction count so as not to interfere with subsequent calls
347
+ expect(interaction.__count).toBe(0);
348
+ expect(
349
+ onInteractionScheduledWorkCompleted,
350
+ ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
351
+
352
+ // It should call other subscribers despite the earlier error
353
+ expect(secondSubscriber.onWorkCanceled).toHaveBeenCalledTimes(1);
354
+ });
355
+ });
356
+
357
+ it('calls lifecycle methods for track', () => {
358
+ expect(onInteractionTracked).not.toHaveBeenCalled();
359
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
360
+
361
+ InteractionTracking.track(
362
+ firstEvent.name,
363
+ currentTime,
364
+ () => {
365
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
366
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
367
+ firstEvent,
368
+ );
369
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
370
+ expect(onWorkStarted).toHaveBeenCalledTimes(1);
371
+ expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
372
+ new Set([firstEvent]),
373
+ threadID,
374
+ );
375
+ expect(onWorkStopped).not.toHaveBeenCalled();
376
+
377
+ InteractionTracking.track(
378
+ secondEvent.name,
379
+ currentTime,
380
+ () => {
381
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
382
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
383
+ secondEvent,
384
+ );
385
+ expect(
386
+ onInteractionScheduledWorkCompleted,
387
+ ).not.toHaveBeenCalled();
388
+ expect(onWorkStarted).toHaveBeenCalledTimes(2);
389
+ expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
390
+ new Set([firstEvent, secondEvent]),
391
+ threadID,
392
+ );
393
+ expect(onWorkStopped).not.toHaveBeenCalled();
394
+ },
395
+ threadID,
396
+ );
397
+
398
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
399
+ expect(
400
+ onInteractionScheduledWorkCompleted,
401
+ ).toHaveBeenLastNotifiedOfInteraction(secondEvent);
402
+ expect(onWorkStopped).toHaveBeenCalledTimes(1);
403
+ expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
404
+ new Set([firstEvent, secondEvent]),
405
+ threadID,
406
+ );
407
+ },
408
+ threadID,
409
+ );
410
+
411
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
412
+ expect(
413
+ onInteractionScheduledWorkCompleted,
414
+ ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
415
+ expect(onWorkScheduled).not.toHaveBeenCalled();
416
+ expect(onWorkCanceled).not.toHaveBeenCalled();
417
+ expect(onWorkStarted).toHaveBeenCalledTimes(2);
418
+ expect(onWorkStopped).toHaveBeenCalledTimes(2);
419
+ expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
420
+ new Set([firstEvent]),
421
+ threadID,
422
+ );
423
+ });
424
+
425
+ it('calls lifecycle methods for wrap', () => {
426
+ const unwrapped = jest.fn();
427
+ let wrapped;
428
+
429
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
430
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
431
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
432
+ firstEvent,
433
+ );
434
+
435
+ InteractionTracking.track(secondEvent.name, currentTime, () => {
436
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
437
+ expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
438
+ secondEvent,
439
+ );
440
+
441
+ wrapped = InteractionTracking.wrap(unwrapped, threadID);
442
+ expect(onWorkScheduled).toHaveBeenCalledTimes(1);
443
+ expect(onWorkScheduled).toHaveBeenLastNotifiedOfWork(
444
+ new Set([firstEvent, secondEvent]),
445
+ threadID,
446
+ );
447
+ });
448
+ });
449
+
450
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
451
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
452
+
453
+ wrapped();
454
+ expect(unwrapped).toHaveBeenCalled();
455
+
456
+ expect(onWorkScheduled).toHaveBeenCalledTimes(1);
457
+ expect(onWorkCanceled).not.toHaveBeenCalled();
458
+ expect(onWorkStarted).toHaveBeenCalledTimes(3);
459
+ expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
460
+ new Set([firstEvent, secondEvent]),
461
+ threadID,
462
+ );
463
+ expect(onWorkStopped).toHaveBeenCalledTimes(3);
464
+ expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
465
+ new Set([firstEvent, secondEvent]),
466
+ threadID,
467
+ );
468
+
469
+ expect(
470
+ onInteractionScheduledWorkCompleted.mock.calls[0][0],
471
+ ).toMatchInteraction(firstEvent);
472
+ expect(
473
+ onInteractionScheduledWorkCompleted.mock.calls[1][0],
474
+ ).toMatchInteraction(secondEvent);
475
+ });
476
+
477
+ it('should call the correct interaction subscriber methods when a wrapped callback is canceled', () => {
478
+ const fnOne = jest.fn();
479
+ const fnTwo = jest.fn();
480
+ let wrappedOne, wrappedTwo;
481
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
482
+ wrappedOne = InteractionTracking.wrap(fnOne, threadID);
483
+ InteractionTracking.track(secondEvent.name, currentTime, () => {
484
+ wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
485
+ });
486
+ });
487
+
488
+ expect(onInteractionTracked).toHaveBeenCalledTimes(2);
489
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
490
+ expect(onWorkCanceled).not.toHaveBeenCalled();
491
+ expect(onWorkStarted).toHaveBeenCalledTimes(2);
492
+ expect(onWorkStopped).toHaveBeenCalledTimes(2);
493
+
494
+ wrappedTwo.cancel();
495
+
496
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
497
+ expect(
498
+ onInteractionScheduledWorkCompleted,
499
+ ).toHaveBeenLastNotifiedOfInteraction(secondEvent);
500
+ expect(onWorkCanceled).toHaveBeenCalledTimes(1);
501
+ expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
502
+ new Set([firstEvent, secondEvent]),
503
+ threadID,
504
+ );
505
+
506
+ wrappedOne.cancel();
507
+
508
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
509
+ expect(
510
+ onInteractionScheduledWorkCompleted,
511
+ ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
512
+ expect(onWorkCanceled).toHaveBeenCalledTimes(2);
513
+ expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
514
+ new Set([firstEvent]),
515
+ threadID,
516
+ );
517
+
518
+ expect(fnOne).not.toHaveBeenCalled();
519
+ expect(fnTwo).not.toHaveBeenCalled();
520
+ });
521
+
522
+ it('should not end an interaction twice if wrap is used to schedule follow up work within another wrap', () => {
523
+ const fnOne = jest.fn(() => {
524
+ wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
525
+ });
526
+ const fnTwo = jest.fn();
527
+ let wrappedOne, wrappedTwo;
528
+ InteractionTracking.track(firstEvent.name, currentTime, () => {
529
+ wrappedOne = InteractionTracking.wrap(fnOne, threadID);
530
+ });
531
+
532
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
533
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
534
+
535
+ wrappedOne();
536
+
537
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
538
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
539
+
540
+ wrappedTwo();
541
+
542
+ expect(onInteractionTracked).toHaveBeenCalledTimes(1);
543
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
544
+ expect(
545
+ onInteractionScheduledWorkCompleted,
546
+ ).toHaveBeenLastNotifiedOfInteraction(firstEvent);
547
+ });
548
+
549
+ it('should unsubscribe', () => {
550
+ InteractionTrackingSubscriptions.unsubscribe(firstSubscriber);
551
+ InteractionTracking.track(firstEvent.name, currentTime, () => {});
552
+
553
+ expect(onInteractionTracked).not.toHaveBeenCalled();
554
+ });
555
+ });
556
+
557
+ describe('disabled', () => {
558
+ beforeEach(() => loadModules({enableInteractionTracking: false}));
559
+
560
+ // TODO
561
+ });
562
+});
packages/interaction-tracking/subscriptions.js
new
+12
@@ -0,0 +1,12 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+'use strict';
11
+
12
+export * from './src/InteractionTrackingSubscriptions';
packages/shared/ReactFeatureFlags.js
-3
@@ -42,9 +42,6 @@ export const enableProfilerTimer = __PROFILE__;
42
// Track which interactions trigger each commit.
43
export const enableInteractionTracking = false;
44
45
-// Track which interactions trigger each commit.
46
-export const enableInteractionTrackingObserver = false;
47
-
45
// Only used in www builds.
46
export function addUserTimingListener() {
47
invariant(false, 'Not implemented.');
packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
-1
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = __DEV__;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
23
export const enableProfilerTimer = __PROFILE__;
24
export const enableInteractionTracking = false;
25
-export const enableInteractionTrackingObserver = false;
25
26
// Only used in www builds.
27
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
-1
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
23
export const enableProfilerTimer = __PROFILE__;
24
export const enableInteractionTracking = false;
25
-export const enableInteractionTrackingObserver = false;
25
26
// Only used in www builds.
27
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-fb.js
-1
@@ -27,7 +27,6 @@ export const enableUserTimingAPI = __DEV__;
27
export const warnAboutLegacyContextAPI = __DEV__;
28
export const enableProfilerTimer = __PROFILE__;
29
export const enableInteractionTracking = false;
30
-export const enableInteractionTrackingObserver = false;
30
31
// Only used in www builds.
32
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-oss.js
-1
@@ -22,7 +22,6 @@ export const warnAboutDeprecatedLifecycles = false;
22
export const warnAboutLegacyContextAPI = false;
23
export const enableProfilerTimer = __PROFILE__;
24
export const enableInteractionTracking = false;
25
-export const enableInteractionTrackingObserver = false;
25
26
// Only used in www builds.
27
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
-1
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
23
export const enableProfilerTimer = __PROFILE__;
24
export const enableInteractionTracking = false;
25
-export const enableInteractionTrackingObserver = false;
25
26
// Only used in www builds.
27
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.js
-1
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23
export const enableProfilerTimer = false;
24
export const enableInteractionTracking = false;
25
-export const enableInteractionTrackingObserver = false;
25
26
// Only used in www builds.
27
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
-1
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23
export const enableProfilerTimer = false;
24
export const enableInteractionTracking = false;
25
-export const enableInteractionTrackingObserver = false;
25
26
// Only used in www builds.
27
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
-1
@@ -33,7 +33,6 @@ export let enableUserTimingAPI = __DEV__;
33
34
export const enableProfilerTimer = __PROFILE__;
35
export const enableInteractionTracking = __PROFILE__;
36
-export const enableInteractionTrackingObserver = __PROFILE__;
36
37
let refCount = 0;
38
export function addUserTimingListener() {
scripts/rollup/bundles.js
+9
@@ -404,6 +404,15 @@ const bundles = [
404
global: 'InteractionTracking',
405
externals: [],
406
},
407
+
408
+ {
409
+ label: 'interaction-tracking-subscriptions',
410
+ bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
411
+ moduleType: ISOMORPHIC,
412
+ entry: 'interaction-tracking/subscriptions',
413
+ global: 'InteractionTrackingSubscriptions',
414
+ externals: ['interaction-tracking'],
415
+ },
416
];
417
418
// Based on deep-freeze by substack (public domain)
scripts/rollup/modules.js
+1
@@ -21,6 +21,7 @@ const knownGlobals = Object.freeze({
21
react: 'React',
22
'react-dom': 'ReactDOM',
23
'interaction-tracking': 'InteractionTracking',
24
+ 'interaction-tracking/subscriptions': 'InteractionTrackingSubscriptions',
25
});
26
27
// Given ['react'] in bundle externals, returns { 'react': 'React' }.