@samitouri / QOS-React / commits / 5e0f073d50

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