interaction-tracking package (#13234)
Add new interaction-tracking package/bundle
Brian Vaughn committed
Aug 17, 2018 at 10:16 UTC
5e0f073d509a74353533233706fce2e8044a0a15
20 files changed
+1347
packages/interaction-tracking/README.md
packages/interaction-tracking/index.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/InteractionTracking';
packages/interaction-tracking/npm/index.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/interaction-tracking.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/interaction-tracking.development.js');
7
+}
packages/interaction-tracking/package.json
new
+23
@@ -0,0 +1,23 @@
1
+{
2
+ "name": "interaction-tracking",
3
+ "description": "utility for tracking interaction events",
4
+ "version": "0.0.1",
5
+ "license": "MIT",
6
+ "files": [
7
+ "LICENSE",
8
+ "README.md",
9
+ "index.js",
10
+ "cjs/",
11
+ "umd/"
12
+ ],
13
+ "keywords": [
14
+ "interaction",
15
+ "tracking",
16
+ "react"
17
+ ],
18
+ "repository": "https://github.com/facebook/react",
19
+ "bugs": {
20
+ "url": "https://github.com/facebook/react/issues"
21
+ },
22
+ "homepage": "https://reactjs.org/"
23
+}
packages/interaction-tracking/src/InteractionTracking.js
new
+289
@@ -0,0 +1,289 @@
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 {
11
+ enableInteractionTracking,
12
+ enableInteractionTrackingObserver,
13
+} from 'shared/ReactFeatureFlags';
14
+
15
+export type Interaction = {|
16
+ __count: number,
17
+ id: number,
18
+ name: string,
19
+ timestamp: number,
20
+|};
21
+
22
+export type Subscriber = {
23
+ // A new interaction has been created via the track() method.
24
+ onInteractionTracked: (interaction: Interaction) => void,
25
+
26
+ // All scheduled async work for an interaction has finished.
27
+ onInteractionScheduledWorkCompleted: (interaction: Interaction) => void,
28
+
29
+ // New async work has been scheduled for a set of interactions.
30
+ // When this work is later run, onWorkStarted/onWorkStopped will be called.
31
+ // A batch of async/yieldy work may be scheduled multiple times before completing.
32
+ // In that case, onWorkScheduled may be called more than once before onWorkStopped.
33
+ // Work is scheduled by a "thread" which is identified by a unique ID.
34
+ onWorkScheduled: (interactions: Set<Interaction>, threadID: number) => void,
35
+
36
+ // A batch of scheduled work has been canceled.
37
+ // Work is done by a "thread" which is identified by a unique ID.
38
+ onWorkCanceled: (interactions: Set<Interaction>, threadID: number) => void,
39
+
40
+ // A batch of work has started for a set of interactions.
41
+ // When this work is complete, onWorkStopped will be called.
42
+ // Work is not always completed synchronously; yielding may occur in between.
43
+ // A batch of async/yieldy work may also be re-started before completing.
44
+ // In that case, onWorkStarted may be called more than once before onWorkStopped.
45
+ // Work is done by a "thread" which is identified by a unique ID.
46
+ onWorkStarted: (interactions: Set<Interaction>, threadID: number) => void,
47
+
48
+ // A batch of work has completed for a set of interactions.
49
+ // Work is done by a "thread" which is identified by a unique ID.
50
+ onWorkStopped: (interactions: Set<Interaction>, threadID: number) => void,
51
+};
52
+
53
+export type InteractionsRef = {
54
+ current: Set<Interaction>,
55
+};
56
+
57
+export type SubscriberRef = {
58
+ current: Subscriber | null,
59
+};
60
+
61
+const DEFAULT_THREAD_ID = 0;
62
+
63
+// Counters used to generate unique IDs.
64
+let interactionIDCounter: number = 0;
65
+let threadIDCounter: number = 0;
66
+
67
+// Set of currently tracked interactions.
68
+// Interactions "stack"–
69
+// Meaning that newly tracked interactions are appended to the previously active set.
70
+// When an interaction goes out of scope, the previous set (if any) is restored.
71
+let interactionsRef: InteractionsRef = (null: any);
72
+
73
+// Listener(s) to notify when interactions begin and end.
74
+// Note that subscribers are only supported when enableInteractionTrackingObserver is enabled.
75
+let subscriberRef: SubscriberRef = (null: any);
76
+
77
+if (enableInteractionTracking) {
78
+ interactionsRef = {
79
+ current: new Set(),
80
+ };
81
+ if (enableInteractionTrackingObserver) {
82
+ subscriberRef = {
83
+ current: null,
84
+ };
85
+ }
86
+}
87
+
88
+// These values are exported for libraries with advanced use cases (i.e. React).
89
+// They should not typically be accessed directly.
90
+export {interactionsRef as __interactionsRef, subscriberRef as __subscriberRef};
91
+
92
+export function clear(callback: Function): any {
93
+ if (!enableInteractionTracking) {
94
+ return callback();
95
+ }
96
+
97
+ const prevInteractions = interactionsRef.current;
98
+ interactionsRef.current = new Set();
99
+
100
+ try {
101
+ return callback();
102
+ } finally {
103
+ interactionsRef.current = prevInteractions;
104
+ }
105
+}
106
+
107
+export function getCurrent(): Set<Interaction> | null {
108
+ if (!enableInteractionTracking) {
109
+ return null;
110
+ } else {
111
+ return interactionsRef.current;
112
+ }
113
+}
114
+
115
+export function getThreadID(): number {
116
+ return ++threadIDCounter;
117
+}
118
+
119
+export function track(
120
+ name: string,
121
+ timestamp: number,
122
+ callback: Function,
123
+ threadID: number = DEFAULT_THREAD_ID,
124
+): any {
125
+ if (!enableInteractionTracking) {
126
+ return callback();
127
+ }
128
+
129
+ const interaction: Interaction = {
130
+ __count: 0,
131
+ id: interactionIDCounter++,
132
+ name,
133
+ timestamp,
134
+ };
135
+
136
+ const prevInteractions = interactionsRef.current;
137
+
138
+ // Tracked interactions should stack/accumulate.
139
+ // To do that, clone the current interactions.
140
+ // The previous set will be restored upon completion.
141
+ const interactions = new Set(prevInteractions);
142
+ interactions.add(interaction);
143
+ interactionsRef.current = interactions;
144
+
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;
151
+
152
+ try {
153
+ if (subscriber !== null) {
154
+ subscriber.onInteractionTracked(interaction);
155
+ }
156
+ } finally {
157
+ try {
158
+ if (subscriber !== null) {
159
+ subscriber.onWorkStarted(interactions, threadID);
160
+ }
161
+ } finally {
162
+ try {
163
+ returnValue = callback();
164
+ } finally {
165
+ interactionsRef.current = prevInteractions;
166
+
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
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ return returnValue;
185
+ } else {
186
+ try {
187
+ return callback();
188
+ } finally {
189
+ interactionsRef.current = prevInteractions;
190
+ }
191
+ }
192
+}
193
+
194
+export function wrap(
195
+ callback: Function,
196
+ threadID: number = DEFAULT_THREAD_ID,
197
+): Function {
198
+ if (!enableInteractionTracking) {
199
+ return callback;
200
+ }
201
+
202
+ const wrappedInteractions = interactionsRef.current;
203
+
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
+ });
215
+ }
216
+
217
+ const wrapped = () => {
218
+ const prevInteractions = interactionsRef.current;
219
+ interactionsRef.current = wrappedInteractions;
220
+
221
+ if (enableInteractionTrackingObserver) {
222
+ const subscriber = subscriberRef.current;
223
+
224
+ try {
225
+ let returnValue;
226
+
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
+ }
241
+ }
242
+
243
+ return returnValue;
244
+ } 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--;
250
+
251
+ if (subscriber !== null && interaction.__count === 0) {
252
+ subscriber.onInteractionScheduledWorkCompleted(interaction);
253
+ }
254
+ });
255
+ }
256
+ } else {
257
+ try {
258
+ return callback.apply(undefined, arguments);
259
+ } finally {
260
+ interactionsRef.current = prevInteractions;
261
+ }
262
+ }
263
+ };
264
+
265
+ if (enableInteractionTrackingObserver) {
266
+ wrapped.cancel = () => {
267
+ const subscriber = subscriberRef.current;
268
+
269
+ try {
270
+ if (subscriber !== null) {
271
+ subscriber.onWorkCanceled(wrappedInteractions, threadID);
272
+ }
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--;
279
+
280
+ if (subscriber && interaction.__count === 0) {
281
+ subscriber.onInteractionScheduledWorkCompleted(interaction);
282
+ }
283
+ });
284
+ }
285
+ };
286
+ }
287
+
288
+ return wrapped;
289
+}
packages/interaction-tracking/src/__tests__/InteractionTracking-test.internal.js
new
+878
@@ -0,0 +1,878 @@
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 ReactFeatureFlags;
14
+
15
+ let advanceTimeBy;
16
+ let currentTime;
17
+
18
+ function loadModules({
19
+ enableInteractionTracking,
20
+ enableInteractionTrackingObserver,
21
+ }) {
22
+ jest.resetModules();
23
+ jest.useFakeTimers();
24
+
25
+ currentTime = 0;
26
+ Date.now = jest.fn().mockImplementation(() => currentTime);
27
+
28
+ advanceTimeBy = amount => {
29
+ currentTime += amount;
30
+ };
31
+
32
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
33
+ ReactFeatureFlags.enableInteractionTracking = enableInteractionTracking;
34
+ ReactFeatureFlags.enableInteractionTrackingObserver = enableInteractionTrackingObserver;
35
+
36
+ InteractionTracking = require('interaction-tracking');
37
+ }
38
+
39
+ describe('enableInteractionTracking enabled', () => {
40
+ beforeEach(() => loadModules({enableInteractionTracking: true}));
41
+
42
+ it('should return the value of a tracked function', () => {
43
+ expect(
44
+ InteractionTracking.track('arbitrary', currentTime, () => 123),
45
+ ).toBe(123);
46
+ });
47
+
48
+ it('should return the value of a clear function', () => {
49
+ expect(InteractionTracking.clear(() => 123)).toBe(123);
50
+ });
51
+
52
+ it('should return the value of a wrapped function', () => {
53
+ let wrapped;
54
+ InteractionTracking.track('arbitrary', currentTime, () => {
55
+ wrapped = InteractionTracking.wrap(() => 123);
56
+ });
57
+ expect(wrapped()).toBe(123);
58
+ });
59
+
60
+ it('should return an empty set when outside of a tracked event', () => {
61
+ expect(InteractionTracking.getCurrent()).toContainNoInteractions();
62
+ });
63
+
64
+ it('should report the tracked interaction from within the track callback', done => {
65
+ advanceTimeBy(100);
66
+
67
+ InteractionTracking.track('some event', currentTime, () => {
68
+ const interactions = InteractionTracking.getCurrent();
69
+ expect(interactions).toMatchInteractions([
70
+ {name: 'some event', timestamp: 100},
71
+ ]);
72
+
73
+ done();
74
+ });
75
+ });
76
+
77
+ it('should report the tracked interaction from within wrapped callbacks', done => {
78
+ let wrappedIndirection;
79
+
80
+ function indirection() {
81
+ const interactions = InteractionTracking.getCurrent();
82
+ expect(interactions).toMatchInteractions([
83
+ {name: 'some event', timestamp: 100},
84
+ ]);
85
+
86
+ done();
87
+ }
88
+
89
+ advanceTimeBy(100);
90
+
91
+ InteractionTracking.track('some event', currentTime, () => {
92
+ wrappedIndirection = InteractionTracking.wrap(indirection);
93
+ });
94
+
95
+ advanceTimeBy(50);
96
+
97
+ wrappedIndirection();
98
+ });
99
+
100
+ it('should clear the interaction stack for tracked callbacks', () => {
101
+ let innerTestReached = false;
102
+
103
+ InteractionTracking.track('outer event', currentTime, () => {
104
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
105
+ {name: 'outer event'},
106
+ ]);
107
+
108
+ InteractionTracking.clear(() => {
109
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([]);
110
+
111
+ InteractionTracking.track('inner event', currentTime, () => {
112
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
113
+ {name: 'inner event'},
114
+ ]);
115
+
116
+ innerTestReached = true;
117
+ });
118
+ });
119
+
120
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
121
+ {name: 'outer event'},
122
+ ]);
123
+ });
124
+
125
+ expect(innerTestReached).toBe(true);
126
+ });
127
+
128
+ it('should clear the interaction stack for wrapped callbacks', () => {
129
+ let innerTestReached = false;
130
+ let wrappedIndirection;
131
+
132
+ const indirection = jest.fn(() => {
133
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
134
+ {name: 'outer event'},
135
+ ]);
136
+
137
+ InteractionTracking.clear(() => {
138
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([]);
139
+
140
+ InteractionTracking.track('inner event', currentTime, () => {
141
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
142
+ {name: 'inner event'},
143
+ ]);
144
+
145
+ innerTestReached = true;
146
+ });
147
+ });
148
+
149
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
150
+ {name: 'outer event'},
151
+ ]);
152
+ });
153
+
154
+ InteractionTracking.track('outer event', currentTime, () => {
155
+ wrappedIndirection = InteractionTracking.wrap(indirection);
156
+ });
157
+
158
+ wrappedIndirection();
159
+
160
+ expect(innerTestReached).toBe(true);
161
+ });
162
+
163
+ it('should support nested tracked events', done => {
164
+ advanceTimeBy(100);
165
+
166
+ let innerIndirectionTracked = false;
167
+ let outerIndirectionTracked = false;
168
+
169
+ function innerIndirection() {
170
+ const interactions = InteractionTracking.getCurrent();
171
+ expect(interactions).toMatchInteractions([
172
+ {name: 'outer event', timestamp: 100},
173
+ {name: 'inner event', timestamp: 150},
174
+ ]);
175
+
176
+ innerIndirectionTracked = true;
177
+ }
178
+
179
+ function outerIndirection() {
180
+ const interactions = InteractionTracking.getCurrent();
181
+ expect(interactions).toMatchInteractions([
182
+ {name: 'outer event', timestamp: 100},
183
+ ]);
184
+
185
+ outerIndirectionTracked = true;
186
+ }
187
+
188
+ InteractionTracking.track('outer event', currentTime, () => {
189
+ // Verify the current tracked event
190
+ let interactions = InteractionTracking.getCurrent();
191
+ expect(interactions).toMatchInteractions([
192
+ {name: 'outer event', timestamp: 100},
193
+ ]);
194
+
195
+ advanceTimeBy(50);
196
+
197
+ const wrapperOuterIndirection = InteractionTracking.wrap(
198
+ outerIndirection,
199
+ );
200
+
201
+ let wrapperInnerIndirection;
202
+ let innerEventTracked = false;
203
+
204
+ // Verify that a nested event is properly tracked
205
+ InteractionTracking.track('inner event', currentTime, () => {
206
+ interactions = InteractionTracking.getCurrent();
207
+ expect(interactions).toMatchInteractions([
208
+ {name: 'outer event', timestamp: 100},
209
+ {name: 'inner event', timestamp: 150},
210
+ ]);
211
+
212
+ // Verify that a wrapped outer callback is properly tracked
213
+ wrapperOuterIndirection();
214
+ expect(outerIndirectionTracked).toBe(true);
215
+
216
+ wrapperInnerIndirection = InteractionTracking.wrap(innerIndirection);
217
+
218
+ innerEventTracked = true;
219
+ });
220
+
221
+ expect(innerEventTracked).toBe(true);
222
+
223
+ // Verify that the original event is restored
224
+ interactions = InteractionTracking.getCurrent();
225
+ expect(interactions).toMatchInteractions([
226
+ {name: 'outer event', timestamp: 100},
227
+ ]);
228
+
229
+ // Verify that a wrapped nested callback is properly tracked
230
+ wrapperInnerIndirection();
231
+ expect(innerIndirectionTracked).toBe(true);
232
+
233
+ done();
234
+ });
235
+ });
236
+
237
+ describe('error handling', () => {
238
+ it('should reset state appropriately when an error occurs in a track callback', done => {
239
+ advanceTimeBy(100);
240
+
241
+ InteractionTracking.track('outer event', currentTime, () => {
242
+ expect(() => {
243
+ InteractionTracking.track('inner event', currentTime, () => {
244
+ throw Error('intentional');
245
+ });
246
+ }).toThrow();
247
+
248
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
249
+ {name: 'outer event', timestamp: 100},
250
+ ]);
251
+
252
+ done();
253
+ });
254
+ });
255
+
256
+ it('should reset state appropriately when an error occurs in a wrapped callback', done => {
257
+ advanceTimeBy(100);
258
+
259
+ InteractionTracking.track('outer event', currentTime, () => {
260
+ let wrappedCallback;
261
+
262
+ InteractionTracking.track('inner event', currentTime, () => {
263
+ wrappedCallback = InteractionTracking.wrap(() => {
264
+ throw Error('intentional');
265
+ });
266
+ });
267
+
268
+ expect(wrappedCallback).toThrow();
269
+
270
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
271
+ {name: 'outer event', timestamp: 100},
272
+ ]);
273
+
274
+ done();
275
+ });
276
+ });
277
+ });
278
+
279
+ describe('advanced integration', () => {
280
+ it('should expose the current set of interactions to be externally manipulated', () => {
281
+ InteractionTracking.track('outer event', currentTime, () => {
282
+ expect(InteractionTracking.__interactionsRef.current).toBe(
283
+ InteractionTracking.getCurrent(),
284
+ );
285
+
286
+ InteractionTracking.__interactionsRef.current = new Set([
287
+ {name: 'override event'},
288
+ ]);
289
+
290
+ expect(InteractionTracking.getCurrent()).toMatchInteractions([
291
+ {name: 'override event'},
292
+ ]);
293
+ });
294
+ });
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);
565
+
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,
819
+ });
820
+ });
821
+
822
+ it('should not create unnecessary objects', () => {
823
+ expect(InteractionTracking.__subscriberRef).toBe(null);
824
+ });
825
+ });
826
+ });
827
+ });
828
+
829
+ describe('enableInteractionTracking disabled', () => {
830
+ beforeEach(() => loadModules({enableInteractionTracking: false}));
831
+
832
+ it('should return the value of a tracked function', () => {
833
+ expect(
834
+ InteractionTracking.track('arbitrary', currentTime, () => 123),
835
+ ).toBe(123);
836
+ });
837
+
838
+ it('should return the value of a wrapped function', () => {
839
+ let wrapped;
840
+ InteractionTracking.track('arbitrary', currentTime, () => {
841
+ wrapped = InteractionTracking.wrap(() => 123);
842
+ });
843
+ expect(wrapped()).toBe(123);
844
+ });
845
+
846
+ it('should return null for tracked interactions', () => {
847
+ expect(InteractionTracking.getCurrent()).toBe(null);
848
+ });
849
+
850
+ it('should execute tracked callbacks', done => {
851
+ InteractionTracking.track('some event', currentTime, () => {
852
+ expect(InteractionTracking.getCurrent()).toBe(null);
853
+
854
+ done();
855
+ });
856
+ });
857
+
858
+ it('should return the value of a clear function', () => {
859
+ expect(InteractionTracking.clear(() => 123)).toBe(123);
860
+ });
861
+
862
+ it('should execute wrapped callbacks', done => {
863
+ const wrappedCallback = InteractionTracking.wrap(() => {
864
+ expect(InteractionTracking.getCurrent()).toBe(null);
865
+
866
+ done();
867
+ });
868
+
869
+ wrappedCallback();
870
+ });
871
+
872
+ describe('advanced integration', () => {
873
+ it('should not create unnecessary objects', () => {
874
+ expect(InteractionTracking.__interactionsRef).toBe(null);
875
+ });
876
+ });
877
+ });
878
+});
packages/shared/ReactFeatureFlags.js
+6
@@ -39,6 +39,12 @@ export const warnAboutLegacyContextAPI = false;
39
// Gather advanced timing metrics for Profiler subtrees.
40
export const enableProfilerTimer = __PROFILE__;
41
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
+
48
// Only used in www builds.
49
export function addUserTimingListener() {
50
invariant(false, 'Not implemented.');
packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
+2
@@ -21,6 +21,8 @@ export const warnAboutDeprecatedLifecycles = false;
21
export const warnAboutLegacyContextAPI = __DEV__;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
23
export const enableProfilerTimer = __PROFILE__;
24
+export const enableInteractionTracking = false;
25
+export const enableInteractionTrackingObserver = false;
26
27
// Only used in www builds.
28
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
+2
@@ -21,6 +21,8 @@ export const warnAboutDeprecatedLifecycles = false;
21
export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
23
export const enableProfilerTimer = __PROFILE__;
24
+export const enableInteractionTracking = false;
25
+export const enableInteractionTrackingObserver = false;
26
27
// Only used in www builds.
28
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-fb.js
+2
@@ -26,6 +26,8 @@ export const {
26
export const enableUserTimingAPI = __DEV__;
27
export const warnAboutLegacyContextAPI = __DEV__;
28
export const enableProfilerTimer = __PROFILE__;
29
+export const enableInteractionTracking = false;
30
+export const enableInteractionTrackingObserver = false;
31
32
// Only used in www builds.
33
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-oss.js
+2
@@ -21,6 +21,8 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
21
export const warnAboutDeprecatedLifecycles = false;
22
export const warnAboutLegacyContextAPI = false;
23
export const enableProfilerTimer = __PROFILE__;
24
+export const enableInteractionTracking = false;
25
+export const enableInteractionTrackingObserver = false;
26
27
// Only used in www builds.
28
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
+2
@@ -21,6 +21,8 @@ export const warnAboutDeprecatedLifecycles = false;
21
export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
23
export const enableProfilerTimer = __PROFILE__;
24
+export const enableInteractionTracking = false;
25
+export const enableInteractionTrackingObserver = false;
26
27
// Only used in www builds.
28
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+2
@@ -21,6 +21,8 @@ export const warnAboutDeprecatedLifecycles = false;
21
export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23
export const enableProfilerTimer = false;
24
+export const enableInteractionTracking = false;
25
+export const enableInteractionTrackingObserver = false;
26
27
// Only used in www builds.
28
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+2
@@ -21,6 +21,8 @@ export const warnAboutDeprecatedLifecycles = false;
21
export const warnAboutLegacyContextAPI = false;
22
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23
export const enableProfilerTimer = false;
24
+export const enableInteractionTracking = false;
25
+export const enableInteractionTrackingObserver = false;
26
27
// Only used in www builds.
28
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -32,6 +32,8 @@ export const warnAboutLegacyContextAPI = __DEV__;
32
export let enableUserTimingAPI = __DEV__;
33
34
export const enableProfilerTimer = __PROFILE__;
35
+export const enableInteractionTracking = __PROFILE__;
36
+export const enableInteractionTrackingObserver = __PROFILE__;
37
38
let refCount = 0;
39
export function addUserTimingListener() {
scripts/jest/matchers/interactionTracking.js
new
+103
@@ -0,0 +1,103 @@
1
+'use strict';
2
+
3
+const jestDiff = require('jest-diff');
4
+
5
+function toContainNoInteractions(actualSet) {
6
+ return {
7
+ message: () =>
8
+ this.isNot
9
+ ? `Expected interactions but there were none.`
10
+ : `Expected no interactions but there were ${actualSet.size}.`,
11
+ pass: actualSet.size === 0,
12
+ };
13
+}
14
+
15
+function toHaveBeenLastNotifiedOfInteraction(
16
+ mockFunction,
17
+ expectedInteraction
18
+) {
19
+ const calls = mockFunction.mock.calls;
20
+ if (calls.length === 0) {
21
+ return {
22
+ message: () => 'Mock function was not called',
23
+ pass: false,
24
+ };
25
+ }
26
+
27
+ const [actualInteraction] = calls[calls.length - 1];
28
+
29
+ return toMatchInteraction(actualInteraction, expectedInteraction);
30
+}
31
+
32
+function toHaveBeenLastNotifiedOfWork(
33
+ mockFunction,
34
+ expectedInteractions,
35
+ expectedThreadID = undefined
36
+) {
37
+ const calls = mockFunction.mock.calls;
38
+ if (calls.length === 0) {
39
+ return {
40
+ message: () => 'Mock function was not called',
41
+ pass: false,
42
+ };
43
+ }
44
+
45
+ const [actualInteractions, actualThreadID] = calls[calls.length - 1];
46
+
47
+ if (expectedThreadID !== undefined) {
48
+ if (expectedThreadID !== actualThreadID) {
49
+ return {
50
+ message: () => jestDiff(expectedThreadID + '', actualThreadID + ''),
51
+ pass: false,
52
+ };
53
+ }
54
+ }
55
+
56
+ return toMatchInteractions(actualInteractions, expectedInteractions);
57
+}
58
+
59
+function toMatchInteraction(actual, expected) {
60
+ let attribute;
61
+ for (attribute in expected) {
62
+ if (actual[attribute] !== expected[attribute]) {
63
+ return {
64
+ message: () => jestDiff(expected, actual),
65
+ pass: false,
66
+ };
67
+ }
68
+ }
69
+
70
+ return {pass: true};
71
+}
72
+
73
+function toMatchInteractions(actualSetOrArray, expectedSetOrArray) {
74
+ const actualArray = Array.from(actualSetOrArray);
75
+ const expectedArray = Array.from(expectedSetOrArray);
76
+
77
+ if (actualArray.length !== expectedArray.length) {
78
+ return {
79
+ message: () =>
80
+ `Expected ${expectedArray.length} interactions but there were ${
81
+ actualArray.length
82
+ }`,
83
+ pass: false,
84
+ };
85
+ }
86
+
87
+ for (let i = 0; i < actualArray.length; i++) {
88
+ const result = toMatchInteraction(actualArray[i], expectedArray[i]);
89
+ if (result.pass === false) {
90
+ return result;
91
+ }
92
+ }
93
+
94
+ return {pass: true};
95
+}
96
+
97
+module.exports = {
98
+ toContainNoInteractions,
99
+ toHaveBeenLastNotifiedOfInteraction,
100
+ toHaveBeenLastNotifiedOfWork,
101
+ toMatchInteraction,
102
+ toMatchInteractions,
103
+};
scripts/jest/setupTests.js
+1
@@ -44,6 +44,7 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
44
}
45
46
expect.extend({
47
+ ...require('./matchers/interactionTracking'),
48
...require('./matchers/toWarnDev'),
49
...require('./matchers/testRenderer'),
50
});
scripts/jest/spec-equivalence-reporter/setupTests.js
+1
@@ -46,6 +46,7 @@ global.spyOnProd = function(...args) {
46
};
47
48
expect.extend({
49
+ ...require('../matchers/interactionTracking'),
50
...require('../matchers/toWarnDev'),
51
...require('../matchers/testRenderer'),
52
});
scripts/rollup/bundles.js
+10
@@ -394,6 +394,16 @@ const bundles = [
394
global: 'ReactScheduler',
395
externals: [],
396
},
397
+
398
+ /******* interaction-tracking (experimental) *******/
399
+ {
400
+ label: 'interaction-tracking',
401
+ bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
402
+ moduleType: ISOMORPHIC,
403
+ entry: 'interaction-tracking',
404
+ global: 'InteractionTracking',
405
+ externals: [],
406
+ },
407
];
408
409
// Based on deep-freeze by substack (public domain)
scripts/rollup/modules.js
+1
@@ -20,6 +20,7 @@ const importSideEffects = Object.freeze({
20
const knownGlobals = Object.freeze({
21
react: 'React',
22
'react-dom': 'ReactDOM',
23
+ 'interaction-tracking': 'InteractionTracking',
24
});
25
26
// Given ['react'] in bundle externals, returns { 'react': 'React' }.