@samitouri / QOS-React-2 / commits / 0da5102cf0

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' }.