mirror of
https://github.com/facebook/react.git
synced 2026-02-23 20:23:02 +00:00
Add interaction-tracking/subscriptions (#13426)
* Removed enableInteractionTrackingObserver as a separate flag; only enableInteractionTracking is used now * Added interaction-tracking/subscriptions bundle and split tests * Added multi-subscriber support * Moved subscriptions behind feature flag * Fixed bug with wrap() parameters and added test * Replaced wrap arrow function
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"index.js",
|
||||
"subscriptions.js",
|
||||
"cjs/",
|
||||
"umd/"
|
||||
],
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
enableInteractionTracking,
|
||||
enableInteractionTrackingObserver,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {enableInteractionTracking} from 'shared/ReactFeatureFlags';
|
||||
|
||||
export type Interaction = {|
|
||||
__count: number,
|
||||
@@ -71,18 +68,15 @@ let threadIDCounter: number = 0;
|
||||
let interactionsRef: InteractionsRef = (null: any);
|
||||
|
||||
// Listener(s) to notify when interactions begin and end.
|
||||
// Note that subscribers are only supported when enableInteractionTrackingObserver is enabled.
|
||||
let subscriberRef: SubscriberRef = (null: any);
|
||||
|
||||
if (enableInteractionTracking) {
|
||||
interactionsRef = {
|
||||
current: new Set(),
|
||||
};
|
||||
if (enableInteractionTrackingObserver) {
|
||||
subscriberRef = {
|
||||
current: null,
|
||||
};
|
||||
}
|
||||
subscriberRef = {
|
||||
current: null,
|
||||
};
|
||||
}
|
||||
|
||||
// These values are exported for libraries with advanced use cases (i.e. React).
|
||||
@@ -127,7 +121,7 @@ export function track(
|
||||
}
|
||||
|
||||
const interaction: Interaction = {
|
||||
__count: 0,
|
||||
__count: 1,
|
||||
id: interactionIDCounter++,
|
||||
name,
|
||||
timestamp,
|
||||
@@ -142,53 +136,42 @@ export function track(
|
||||
interactions.add(interaction);
|
||||
interactionsRef.current = interactions;
|
||||
|
||||
if (enableInteractionTrackingObserver) {
|
||||
// Update before calling callback in case it schedules follow-up work.
|
||||
interaction.__count = 1;
|
||||
|
||||
let returnValue;
|
||||
const subscriber = subscriberRef.current;
|
||||
const subscriber = subscriberRef.current;
|
||||
let returnValue;
|
||||
|
||||
try {
|
||||
if (subscriber !== null) {
|
||||
subscriber.onInteractionTracked(interaction);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (subscriber !== null) {
|
||||
subscriber.onInteractionTracked(interaction);
|
||||
subscriber.onWorkStarted(interactions, threadID);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkStarted(interactions, threadID);
|
||||
}
|
||||
returnValue = callback();
|
||||
} finally {
|
||||
interactionsRef.current = prevInteractions;
|
||||
|
||||
try {
|
||||
returnValue = callback();
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkStopped(interactions, threadID);
|
||||
}
|
||||
} finally {
|
||||
interactionsRef.current = prevInteractions;
|
||||
interaction.__count--;
|
||||
|
||||
try {
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkStopped(interactions, threadID);
|
||||
}
|
||||
} finally {
|
||||
interaction.__count--;
|
||||
|
||||
// If no async work was scheduled for this interaction,
|
||||
// Notify subscribers that it's completed.
|
||||
if (subscriber !== null && interaction.__count === 0) {
|
||||
subscriber.onInteractionScheduledWorkCompleted(interaction);
|
||||
}
|
||||
// If no async work was scheduled for this interaction,
|
||||
// Notify subscribers that it's completed.
|
||||
if (subscriber !== null && interaction.__count === 0) {
|
||||
subscriber.onInteractionScheduledWorkCompleted(interaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
} else {
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
interactionsRef.current = prevInteractions;
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
export function wrap(
|
||||
@@ -201,89 +184,77 @@ export function wrap(
|
||||
|
||||
const wrappedInteractions = interactionsRef.current;
|
||||
|
||||
if (enableInteractionTrackingObserver) {
|
||||
const subscriber = subscriberRef.current;
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkScheduled(wrappedInteractions, threadID);
|
||||
}
|
||||
|
||||
// Update the pending async work count for the current interactions.
|
||||
// Update after calling subscribers in case of error.
|
||||
wrappedInteractions.forEach(interaction => {
|
||||
interaction.__count++;
|
||||
});
|
||||
let subscriber = subscriberRef.current;
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkScheduled(wrappedInteractions, threadID);
|
||||
}
|
||||
|
||||
const wrapped = () => {
|
||||
// Update the pending async work count for the current interactions.
|
||||
// Update after calling subscribers in case of error.
|
||||
wrappedInteractions.forEach(interaction => {
|
||||
interaction.__count++;
|
||||
});
|
||||
|
||||
function wrapped() {
|
||||
const prevInteractions = interactionsRef.current;
|
||||
interactionsRef.current = wrappedInteractions;
|
||||
|
||||
if (enableInteractionTrackingObserver) {
|
||||
const subscriber = subscriberRef.current;
|
||||
subscriber = subscriberRef.current;
|
||||
|
||||
try {
|
||||
let returnValue;
|
||||
|
||||
try {
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkStarted(wrappedInteractions, threadID);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
returnValue = callback.apply(undefined, arguments);
|
||||
} finally {
|
||||
interactionsRef.current = prevInteractions;
|
||||
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkStopped(wrappedInteractions, threadID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
} finally {
|
||||
// Update pending async counts for all wrapped interactions.
|
||||
// If this was the last scheduled async work for any of them,
|
||||
// Mark them as completed.
|
||||
wrappedInteractions.forEach(interaction => {
|
||||
interaction.__count--;
|
||||
|
||||
if (subscriber !== null && interaction.__count === 0) {
|
||||
subscriber.onInteractionScheduledWorkCompleted(interaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return callback.apply(undefined, arguments);
|
||||
} finally {
|
||||
interactionsRef.current = prevInteractions;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (enableInteractionTrackingObserver) {
|
||||
wrapped.cancel = () => {
|
||||
const subscriber = subscriberRef.current;
|
||||
try {
|
||||
let returnValue;
|
||||
|
||||
try {
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkCanceled(wrappedInteractions, threadID);
|
||||
subscriber.onWorkStarted(wrappedInteractions, threadID);
|
||||
}
|
||||
} finally {
|
||||
// Update pending async counts for all wrapped interactions.
|
||||
// If this was the last scheduled async work for any of them,
|
||||
// Mark them as completed.
|
||||
wrappedInteractions.forEach(interaction => {
|
||||
interaction.__count--;
|
||||
try {
|
||||
returnValue = callback.apply(undefined, arguments);
|
||||
} finally {
|
||||
interactionsRef.current = prevInteractions;
|
||||
|
||||
if (subscriber && interaction.__count === 0) {
|
||||
subscriber.onInteractionScheduledWorkCompleted(interaction);
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkStopped(wrappedInteractions, threadID);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return returnValue;
|
||||
} finally {
|
||||
// Update pending async counts for all wrapped interactions.
|
||||
// If this was the last scheduled async work for any of them,
|
||||
// Mark them as completed.
|
||||
wrappedInteractions.forEach(interaction => {
|
||||
interaction.__count--;
|
||||
|
||||
if (subscriber !== null && interaction.__count === 0) {
|
||||
subscriber.onInteractionScheduledWorkCompleted(interaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
wrapped.cancel = function cancel() {
|
||||
subscriber = subscriberRef.current;
|
||||
|
||||
try {
|
||||
if (subscriber !== null) {
|
||||
subscriber.onWorkCanceled(wrappedInteractions, threadID);
|
||||
}
|
||||
} finally {
|
||||
// Update pending async counts for all wrapped interactions.
|
||||
// If this was the last scheduled async work for any of them,
|
||||
// Mark them as completed.
|
||||
wrappedInteractions.forEach(interaction => {
|
||||
interaction.__count--;
|
||||
|
||||
if (subscriber && interaction.__count === 0) {
|
||||
subscriber.onInteractionScheduledWorkCompleted(interaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {Interaction, Subscriber} from './InteractionTracking';
|
||||
|
||||
import {enableInteractionTracking} from 'shared/ReactFeatureFlags';
|
||||
import {__subscriberRef} from 'interaction-tracking';
|
||||
|
||||
let subscribers: Set<Subscriber> = (null: any);
|
||||
if (enableInteractionTracking) {
|
||||
subscribers = new Set();
|
||||
}
|
||||
|
||||
export function subscribe(subscriber: Subscriber): void {
|
||||
if (enableInteractionTracking) {
|
||||
subscribers.add(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
export function unsubscribe(subscriber: Subscriber): void {
|
||||
if (enableInteractionTracking) {
|
||||
subscribers.delete(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
function onInteractionTracked(interaction: Interaction): void {
|
||||
let didCatchError = false;
|
||||
let caughtError = null;
|
||||
|
||||
subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber.onInteractionTracked(interaction);
|
||||
} catch (error) {
|
||||
if (!didCatchError) {
|
||||
didCatchError = true;
|
||||
caughtError = error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (didCatchError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
function onInteractionScheduledWorkCompleted(interaction: Interaction): void {
|
||||
let didCatchError = false;
|
||||
let caughtError = null;
|
||||
|
||||
subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber.onInteractionScheduledWorkCompleted(interaction);
|
||||
} catch (error) {
|
||||
if (!didCatchError) {
|
||||
didCatchError = true;
|
||||
caughtError = error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (didCatchError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
function onWorkScheduled(
|
||||
interactions: Set<Interaction>,
|
||||
threadID: number,
|
||||
): void {
|
||||
let didCatchError = false;
|
||||
let caughtError = null;
|
||||
|
||||
subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber.onWorkScheduled(interactions, threadID);
|
||||
} catch (error) {
|
||||
if (!didCatchError) {
|
||||
didCatchError = true;
|
||||
caughtError = error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (didCatchError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
function onWorkStarted(interactions: Set<Interaction>, threadID: number): void {
|
||||
let didCatchError = false;
|
||||
let caughtError = null;
|
||||
|
||||
subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber.onWorkStarted(interactions, threadID);
|
||||
} catch (error) {
|
||||
if (!didCatchError) {
|
||||
didCatchError = true;
|
||||
caughtError = error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (didCatchError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
function onWorkStopped(interactions: Set<Interaction>, threadID: number): void {
|
||||
let didCatchError = false;
|
||||
let caughtError = null;
|
||||
|
||||
subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber.onWorkStopped(interactions, threadID);
|
||||
} catch (error) {
|
||||
if (!didCatchError) {
|
||||
didCatchError = true;
|
||||
caughtError = error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (didCatchError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
function onWorkCanceled(
|
||||
interactions: Set<Interaction>,
|
||||
threadID: number,
|
||||
): void {
|
||||
let didCatchError = false;
|
||||
let caughtError = null;
|
||||
|
||||
subscribers.forEach(subscriber => {
|
||||
try {
|
||||
subscriber.onWorkCanceled(interactions, threadID);
|
||||
} catch (error) {
|
||||
if (!didCatchError) {
|
||||
didCatchError = true;
|
||||
caughtError = error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (didCatchError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
if (enableInteractionTracking) {
|
||||
__subscriberRef.current = {
|
||||
onInteractionScheduledWorkCompleted,
|
||||
onInteractionTracked,
|
||||
onWorkCanceled,
|
||||
onWorkScheduled,
|
||||
onWorkStarted,
|
||||
onWorkStopped,
|
||||
};
|
||||
}
|
||||
@@ -15,10 +15,7 @@ describe('InteractionTracking', () => {
|
||||
let advanceTimeBy;
|
||||
let currentTime;
|
||||
|
||||
function loadModules({
|
||||
enableInteractionTracking,
|
||||
enableInteractionTrackingObserver,
|
||||
}) {
|
||||
function loadModules({enableInteractionTracking}) {
|
||||
jest.resetModules();
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -31,7 +28,6 @@ describe('InteractionTracking', () => {
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableInteractionTracking = enableInteractionTracking;
|
||||
ReactFeatureFlags.enableInteractionTrackingObserver = enableInteractionTrackingObserver;
|
||||
|
||||
InteractionTracking = require('interaction-tracking');
|
||||
}
|
||||
@@ -57,6 +53,18 @@ describe('InteractionTracking', () => {
|
||||
expect(wrapped()).toBe(123);
|
||||
});
|
||||
|
||||
it('should pass arguments through to a wrapped function', done => {
|
||||
let wrapped;
|
||||
InteractionTracking.track('arbitrary', currentTime, () => {
|
||||
wrapped = InteractionTracking.wrap((param1, param2) => {
|
||||
expect(param1).toBe('foo');
|
||||
expect(param2).toBe('bar');
|
||||
done();
|
||||
});
|
||||
});
|
||||
wrapped('foo', 'bar');
|
||||
});
|
||||
|
||||
it('should return an empty set when outside of a tracked event', () => {
|
||||
expect(InteractionTracking.getCurrent()).toContainNoInteractions();
|
||||
});
|
||||
@@ -277,6 +285,12 @@ describe('InteractionTracking', () => {
|
||||
});
|
||||
|
||||
describe('advanced integration', () => {
|
||||
it('should return a unique threadID per request', () => {
|
||||
expect(InteractionTracking.getThreadID()).not.toBe(
|
||||
InteractionTracking.getThreadID(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should expose the current set of interactions to be externally manipulated', () => {
|
||||
InteractionTracking.track('outer event', currentTime, () => {
|
||||
expect(InteractionTracking.__interactionsRef.current).toBe(
|
||||
@@ -292,535 +306,12 @@ describe('InteractionTracking', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('interaction subscribers enabled', () => {
|
||||
let onInteractionScheduledWorkCompleted;
|
||||
let onInteractionTracked;
|
||||
let onWorkCanceled;
|
||||
let onWorkScheduled;
|
||||
let onWorkStarted;
|
||||
let onWorkStopped;
|
||||
let subscriber;
|
||||
let throwInOnInteractionScheduledWorkCompleted;
|
||||
let throwInOnInteractionTracked;
|
||||
let throwInOnWorkCanceled;
|
||||
let throwInOnWorkScheduled;
|
||||
let throwInOnWorkStarted;
|
||||
let throwInOnWorkStopped;
|
||||
|
||||
const firstEvent = {id: 0, name: 'first', timestamp: 0};
|
||||
const secondEvent = {id: 1, name: 'second', timestamp: 0};
|
||||
const threadID = 123;
|
||||
|
||||
beforeEach(() => {
|
||||
throwInOnInteractionScheduledWorkCompleted = false;
|
||||
throwInOnInteractionTracked = false;
|
||||
throwInOnWorkCanceled = false;
|
||||
throwInOnWorkScheduled = false;
|
||||
throwInOnWorkStarted = false;
|
||||
throwInOnWorkStopped = false;
|
||||
|
||||
onInteractionScheduledWorkCompleted = jest.fn(() => {
|
||||
if (throwInOnInteractionScheduledWorkCompleted) {
|
||||
throw Error('Expected error onInteractionScheduledWorkCompleted');
|
||||
}
|
||||
});
|
||||
onInteractionTracked = jest.fn(() => {
|
||||
if (throwInOnInteractionTracked) {
|
||||
throw Error('Expected error onInteractionTracked');
|
||||
}
|
||||
});
|
||||
onWorkCanceled = jest.fn(() => {
|
||||
if (throwInOnWorkCanceled) {
|
||||
throw Error('Expected error onWorkCanceled');
|
||||
}
|
||||
});
|
||||
onWorkScheduled = jest.fn(() => {
|
||||
if (throwInOnWorkScheduled) {
|
||||
throw Error('Expected error onWorkScheduled');
|
||||
}
|
||||
});
|
||||
onWorkStarted = jest.fn(() => {
|
||||
if (throwInOnWorkStarted) {
|
||||
throw Error('Expected error onWorkStarted');
|
||||
}
|
||||
});
|
||||
onWorkStopped = jest.fn(() => {
|
||||
if (throwInOnWorkStopped) {
|
||||
throw Error('Expected error onWorkStopped');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableInteractionTrackingObserver enabled', () => {
|
||||
beforeEach(() => {
|
||||
loadModules({
|
||||
enableInteractionTracking: true,
|
||||
enableInteractionTrackingObserver: true,
|
||||
it('should expose a subscriber ref to be externally manipulated', () => {
|
||||
InteractionTracking.track('outer event', currentTime, () => {
|
||||
expect(InteractionTracking.__subscriberRef).toEqual({
|
||||
current: null,
|
||||
});
|
||||
|
||||
subscriber = {
|
||||
onInteractionScheduledWorkCompleted,
|
||||
onInteractionTracked,
|
||||
onWorkCanceled,
|
||||
onWorkScheduled,
|
||||
onWorkStarted,
|
||||
onWorkStopped,
|
||||
};
|
||||
|
||||
InteractionTracking.__subscriberRef.current = subscriber;
|
||||
});
|
||||
|
||||
it('should return the value of a tracked function', () => {
|
||||
expect(
|
||||
InteractionTracking.track('arbitrary', currentTime, () => 123),
|
||||
).toBe(123);
|
||||
});
|
||||
|
||||
it('should return the value of a wrapped function', () => {
|
||||
let wrapped;
|
||||
InteractionTracking.track('arbitrary', currentTime, () => {
|
||||
wrapped = InteractionTracking.wrap(() => 123);
|
||||
});
|
||||
expect(wrapped()).toBe(123);
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should cover onInteractionTracked/onWorkStarted within', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
const mock = jest.fn();
|
||||
|
||||
// It should call the callback before re-throwing
|
||||
throwInOnInteractionTracked = true;
|
||||
expect(() =>
|
||||
InteractionTracking.track(
|
||||
secondEvent.name,
|
||||
currentTime,
|
||||
mock,
|
||||
threadID,
|
||||
),
|
||||
).toThrow('Expected error onInteractionTracked');
|
||||
throwInOnInteractionTracked = false;
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
|
||||
throwInOnWorkStarted = true;
|
||||
expect(() =>
|
||||
InteractionTracking.track(
|
||||
secondEvent.name,
|
||||
currentTime,
|
||||
mock,
|
||||
threadID,
|
||||
),
|
||||
).toThrow('Expected error onWorkStarted');
|
||||
expect(mock).toHaveBeenCalledTimes(2);
|
||||
|
||||
// It should restore the previous/outer interactions
|
||||
expect(InteractionTracking.getCurrent()).toMatchInteractions([
|
||||
firstEvent,
|
||||
]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover onWorkStopped within track', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
let innerInteraction;
|
||||
const mock = jest.fn(() => {
|
||||
innerInteraction = Array.from(
|
||||
InteractionTracking.getCurrent(),
|
||||
)[1];
|
||||
});
|
||||
|
||||
throwInOnWorkStopped = true;
|
||||
expect(() =>
|
||||
InteractionTracking.track(secondEvent.name, currentTime, mock),
|
||||
).toThrow('Expected error onWorkStopped');
|
||||
throwInOnWorkStopped = false;
|
||||
|
||||
// It should restore the previous/outer interactions
|
||||
expect(InteractionTracking.getCurrent()).toMatchInteractions([
|
||||
firstEvent,
|
||||
]);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(innerInteraction.__count).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover the callback within track', done => {
|
||||
expect(onWorkStarted).not.toHaveBeenCalled();
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
|
||||
expect(() => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
throw Error('Expected error callback');
|
||||
});
|
||||
}).toThrow('Expected error callback');
|
||||
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should cover onWorkScheduled within wrap', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
const interaction = Array.from(
|
||||
InteractionTracking.getCurrent(),
|
||||
)[0];
|
||||
const beforeCount = interaction.__count;
|
||||
|
||||
throwInOnWorkScheduled = true;
|
||||
expect(() => InteractionTracking.wrap(() => {})).toThrow(
|
||||
'Expected error onWorkScheduled',
|
||||
);
|
||||
|
||||
// It should not update the interaction count so as not to interfere with subsequent calls
|
||||
expect(interaction.__count).toBe(beforeCount);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover onWorkStarted within wrap', () => {
|
||||
const mock = jest.fn();
|
||||
let interaction, wrapped;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
interaction = Array.from(InteractionTracking.getCurrent())[0];
|
||||
wrapped = InteractionTracking.wrap(mock);
|
||||
});
|
||||
expect(interaction.__count).toBe(1);
|
||||
|
||||
throwInOnWorkStarted = true;
|
||||
expect(wrapped).toThrow('Expected error onWorkStarted');
|
||||
|
||||
// It should call the callback before re-throwing
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(interaction.__count).toBe(0);
|
||||
});
|
||||
|
||||
it('should cover onWorkStopped within wrap', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
const outerInteraction = Array.from(
|
||||
InteractionTracking.getCurrent(),
|
||||
)[0];
|
||||
expect(outerInteraction.__count).toBe(1);
|
||||
|
||||
let wrapped;
|
||||
let innerInteraction;
|
||||
|
||||
InteractionTracking.track(secondEvent.name, currentTime, () => {
|
||||
innerInteraction = Array.from(
|
||||
InteractionTracking.getCurrent(),
|
||||
)[1];
|
||||
expect(outerInteraction.__count).toBe(1);
|
||||
expect(innerInteraction.__count).toBe(1);
|
||||
|
||||
wrapped = InteractionTracking.wrap(jest.fn());
|
||||
expect(outerInteraction.__count).toBe(2);
|
||||
expect(innerInteraction.__count).toBe(2);
|
||||
});
|
||||
|
||||
expect(outerInteraction.__count).toBe(2);
|
||||
expect(innerInteraction.__count).toBe(1);
|
||||
|
||||
throwInOnWorkStopped = true;
|
||||
expect(wrapped).toThrow('Expected error onWorkStopped');
|
||||
throwInOnWorkStopped = false;
|
||||
|
||||
// It should restore the previous interactions
|
||||
expect(InteractionTracking.getCurrent()).toMatchInteractions([
|
||||
outerInteraction,
|
||||
]);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(outerInteraction.__count).toBe(1);
|
||||
expect(innerInteraction.__count).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover the callback within wrap', done => {
|
||||
expect(onWorkStarted).not.toHaveBeenCalled();
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
|
||||
let wrapped;
|
||||
let interaction;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
interaction = Array.from(InteractionTracking.getCurrent())[0];
|
||||
wrapped = InteractionTracking.wrap(() => {
|
||||
throw Error('Expected error wrap');
|
||||
});
|
||||
});
|
||||
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(wrapped).toThrow('Expected error wrap');
|
||||
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork([interaction]);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should cover onWorkCanceled within wrap', () => {
|
||||
let interaction, wrapped;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
interaction = Array.from(InteractionTracking.getCurrent())[0];
|
||||
wrapped = InteractionTracking.wrap(jest.fn());
|
||||
});
|
||||
expect(interaction.__count).toBe(1);
|
||||
|
||||
throwInOnWorkCanceled = true;
|
||||
expect(wrapped.cancel).toThrow('Expected error onWorkCanceled');
|
||||
|
||||
expect(onWorkCanceled).toHaveBeenCalledTimes(1);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(interaction.__count).toBe(0);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls lifecycle methods for track', () => {
|
||||
expect(onInteractionTracked).not.toHaveBeenCalled();
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
InteractionTracking.track(
|
||||
firstEvent.name,
|
||||
currentTime,
|
||||
() => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
|
||||
firstEvent,
|
||||
);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent]),
|
||||
threadID,
|
||||
);
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
|
||||
InteractionTracking.track(
|
||||
secondEvent.name,
|
||||
currentTime,
|
||||
() => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
onInteractionTracked,
|
||||
).toHaveBeenLastNotifiedOfInteraction(secondEvent);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
},
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(secondEvent);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
},
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
expect(onWorkScheduled).not.toHaveBeenCalled();
|
||||
expect(onWorkCanceled).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent]),
|
||||
threadID,
|
||||
);
|
||||
});
|
||||
|
||||
it('calls lifecycle methods for wrap', () => {
|
||||
const unwrapped = jest.fn();
|
||||
let wrapped;
|
||||
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
|
||||
firstEvent,
|
||||
);
|
||||
|
||||
InteractionTracking.track(secondEvent.name, currentTime, () => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
|
||||
secondEvent,
|
||||
);
|
||||
|
||||
wrapped = InteractionTracking.wrap(unwrapped, threadID);
|
||||
expect(onWorkScheduled).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkScheduled).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
wrapped();
|
||||
expect(unwrapped).toHaveBeenCalled();
|
||||
|
||||
expect(onWorkScheduled).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkCanceled).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(3);
|
||||
expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(3);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted.mock.calls[0][0],
|
||||
).toMatchInteraction(firstEvent);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted.mock.calls[1][0],
|
||||
).toMatchInteraction(secondEvent);
|
||||
});
|
||||
|
||||
it('should call the correct interaction subscriber methods when a wrapped callback is canceled', () => {
|
||||
const fnOne = jest.fn();
|
||||
const fnTwo = jest.fn();
|
||||
let wrappedOne, wrappedTwo;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
wrappedOne = InteractionTracking.wrap(fnOne, threadID);
|
||||
InteractionTracking.track(secondEvent.name, currentTime, () => {
|
||||
wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
|
||||
});
|
||||
});
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
expect(onWorkCanceled).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(2);
|
||||
|
||||
wrappedTwo.cancel();
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(secondEvent);
|
||||
expect(onWorkCanceled).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
|
||||
wrappedOne.cancel();
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
expect(onWorkCanceled).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent]),
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(fnOne).not.toHaveBeenCalled();
|
||||
expect(fnTwo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not end an interaction twice if wrap is used to schedule follow up work within another wrap', () => {
|
||||
const fnOne = jest.fn(() => {
|
||||
wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
|
||||
});
|
||||
const fnTwo = jest.fn();
|
||||
let wrappedOne, wrappedTwo;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
wrappedOne = InteractionTracking.wrap(fnOne, threadID);
|
||||
});
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
wrappedOne();
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
wrappedTwo();
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
});
|
||||
|
||||
it('should unsubscribe', () => {
|
||||
InteractionTracking.__subscriberRef.current = null;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {});
|
||||
|
||||
expect(onInteractionTracked).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('advanced integration', () => {
|
||||
it('should return a unique threadID per request', () => {
|
||||
expect(InteractionTracking.getThreadID()).not.toBe(
|
||||
InteractionTracking.getThreadID(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should expose the current set of interaction subscribers to be called externally', () => {
|
||||
expect(
|
||||
InteractionTracking.__subscriberRef.current.onInteractionTracked,
|
||||
).toBe(onInteractionTracked);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableInteractionTrackingObserver disabled', () => {
|
||||
beforeEach(() => {
|
||||
loadModules({
|
||||
enableInteractionTracking: true,
|
||||
enableInteractionTrackingObserver: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create unnecessary objects', () => {
|
||||
expect(InteractionTracking.__subscriberRef).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @jest-environment node
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
describe('InteractionTracking', () => {
|
||||
let InteractionTracking;
|
||||
let InteractionTrackingSubscriptions;
|
||||
let ReactFeatureFlags;
|
||||
|
||||
let currentTime;
|
||||
|
||||
let onInteractionScheduledWorkCompleted;
|
||||
let onInteractionTracked;
|
||||
let onWorkCanceled;
|
||||
let onWorkScheduled;
|
||||
let onWorkStarted;
|
||||
let onWorkStopped;
|
||||
let throwInOnInteractionScheduledWorkCompleted;
|
||||
let throwInOnInteractionTracked;
|
||||
let throwInOnWorkCanceled;
|
||||
let throwInOnWorkScheduled;
|
||||
let throwInOnWorkStarted;
|
||||
let throwInOnWorkStopped;
|
||||
let firstSubscriber;
|
||||
let secondSubscriber;
|
||||
|
||||
const firstEvent = {id: 0, name: 'first', timestamp: 0};
|
||||
const secondEvent = {id: 1, name: 'second', timestamp: 0};
|
||||
const threadID = 123;
|
||||
|
||||
function loadModules({enableInteractionTracking}) {
|
||||
jest.resetModules();
|
||||
jest.useFakeTimers();
|
||||
|
||||
currentTime = 0;
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableInteractionTracking = enableInteractionTracking;
|
||||
|
||||
InteractionTracking = require('interaction-tracking');
|
||||
InteractionTrackingSubscriptions = require('interaction-tracking/subscriptions');
|
||||
|
||||
throwInOnInteractionScheduledWorkCompleted = false;
|
||||
throwInOnInteractionTracked = false;
|
||||
throwInOnWorkCanceled = false;
|
||||
throwInOnWorkScheduled = false;
|
||||
throwInOnWorkStarted = false;
|
||||
throwInOnWorkStopped = false;
|
||||
|
||||
onInteractionScheduledWorkCompleted = jest.fn(() => {
|
||||
if (throwInOnInteractionScheduledWorkCompleted) {
|
||||
throw Error('Expected error onInteractionScheduledWorkCompleted');
|
||||
}
|
||||
});
|
||||
onInteractionTracked = jest.fn(() => {
|
||||
if (throwInOnInteractionTracked) {
|
||||
throw Error('Expected error onInteractionTracked');
|
||||
}
|
||||
});
|
||||
onWorkCanceled = jest.fn(() => {
|
||||
if (throwInOnWorkCanceled) {
|
||||
throw Error('Expected error onWorkCanceled');
|
||||
}
|
||||
});
|
||||
onWorkScheduled = jest.fn(() => {
|
||||
if (throwInOnWorkScheduled) {
|
||||
throw Error('Expected error onWorkScheduled');
|
||||
}
|
||||
});
|
||||
onWorkStarted = jest.fn(() => {
|
||||
if (throwInOnWorkStarted) {
|
||||
throw Error('Expected error onWorkStarted');
|
||||
}
|
||||
});
|
||||
onWorkStopped = jest.fn(() => {
|
||||
if (throwInOnWorkStopped) {
|
||||
throw Error('Expected error onWorkStopped');
|
||||
}
|
||||
});
|
||||
|
||||
firstSubscriber = {
|
||||
onInteractionScheduledWorkCompleted,
|
||||
onInteractionTracked,
|
||||
onWorkCanceled,
|
||||
onWorkScheduled,
|
||||
onWorkStarted,
|
||||
onWorkStopped,
|
||||
};
|
||||
|
||||
secondSubscriber = {
|
||||
onInteractionScheduledWorkCompleted: jest.fn(),
|
||||
onInteractionTracked: jest.fn(),
|
||||
onWorkCanceled: jest.fn(),
|
||||
onWorkScheduled: jest.fn(),
|
||||
onWorkStarted: jest.fn(),
|
||||
onWorkStopped: jest.fn(),
|
||||
};
|
||||
|
||||
InteractionTrackingSubscriptions.subscribe(firstSubscriber);
|
||||
InteractionTrackingSubscriptions.subscribe(secondSubscriber);
|
||||
}
|
||||
|
||||
describe('enabled', () => {
|
||||
beforeEach(() => loadModules({enableInteractionTracking: true}));
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should cover onInteractionTracked/onWorkStarted within', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
const mock = jest.fn();
|
||||
|
||||
// It should call the callback before re-throwing
|
||||
throwInOnInteractionTracked = true;
|
||||
expect(() =>
|
||||
InteractionTracking.track(
|
||||
secondEvent.name,
|
||||
currentTime,
|
||||
mock,
|
||||
threadID,
|
||||
),
|
||||
).toThrow('Expected error onInteractionTracked');
|
||||
throwInOnInteractionTracked = false;
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
|
||||
throwInOnWorkStarted = true;
|
||||
expect(() =>
|
||||
InteractionTracking.track(
|
||||
secondEvent.name,
|
||||
currentTime,
|
||||
mock,
|
||||
threadID,
|
||||
),
|
||||
).toThrow('Expected error onWorkStarted');
|
||||
expect(mock).toHaveBeenCalledTimes(2);
|
||||
|
||||
// It should restore the previous/outer interactions
|
||||
expect(InteractionTracking.getCurrent()).toMatchInteractions([
|
||||
firstEvent,
|
||||
]);
|
||||
|
||||
// It should call other subscribers despite the earlier error
|
||||
expect(secondSubscriber.onInteractionTracked).toHaveBeenCalledTimes(
|
||||
3,
|
||||
);
|
||||
expect(secondSubscriber.onWorkStarted).toHaveBeenCalledTimes(3);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover onWorkStopped within track', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
let innerInteraction;
|
||||
const mock = jest.fn(() => {
|
||||
innerInteraction = Array.from(InteractionTracking.getCurrent())[1];
|
||||
});
|
||||
|
||||
throwInOnWorkStopped = true;
|
||||
expect(() =>
|
||||
InteractionTracking.track(secondEvent.name, currentTime, mock),
|
||||
).toThrow('Expected error onWorkStopped');
|
||||
throwInOnWorkStopped = false;
|
||||
|
||||
// It should restore the previous/outer interactions
|
||||
expect(InteractionTracking.getCurrent()).toMatchInteractions([
|
||||
firstEvent,
|
||||
]);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(innerInteraction.__count).toBe(0);
|
||||
|
||||
// It should call other subscribers despite the earlier error
|
||||
expect(secondSubscriber.onWorkStopped).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover onInteractionScheduledWorkCompleted within track', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
const mock = jest.fn();
|
||||
|
||||
throwInOnInteractionScheduledWorkCompleted = true;
|
||||
expect(() =>
|
||||
InteractionTracking.track(secondEvent.name, currentTime, mock),
|
||||
).toThrow('Expected error onInteractionScheduledWorkCompleted');
|
||||
throwInOnInteractionScheduledWorkCompleted = false;
|
||||
|
||||
// It should restore the previous/outer interactions
|
||||
expect(InteractionTracking.getCurrent()).toMatchInteractions([
|
||||
firstEvent,
|
||||
]);
|
||||
|
||||
// It should call other subscribers despite the earlier error
|
||||
expect(
|
||||
secondSubscriber.onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover the callback within track', done => {
|
||||
expect(onWorkStarted).not.toHaveBeenCalled();
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
|
||||
expect(() => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
throw Error('Expected error callback');
|
||||
});
|
||||
}).toThrow('Expected error callback');
|
||||
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should cover onWorkScheduled within wrap', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
const interaction = Array.from(InteractionTracking.getCurrent())[0];
|
||||
const beforeCount = interaction.__count;
|
||||
|
||||
throwInOnWorkScheduled = true;
|
||||
expect(() => InteractionTracking.wrap(() => {})).toThrow(
|
||||
'Expected error onWorkScheduled',
|
||||
);
|
||||
|
||||
// It should not update the interaction count so as not to interfere with subsequent calls
|
||||
expect(interaction.__count).toBe(beforeCount);
|
||||
|
||||
// It should call other subscribers despite the earlier error
|
||||
expect(secondSubscriber.onWorkScheduled).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover onWorkStarted within wrap', () => {
|
||||
const mock = jest.fn();
|
||||
let interaction, wrapped;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
interaction = Array.from(InteractionTracking.getCurrent())[0];
|
||||
wrapped = InteractionTracking.wrap(mock);
|
||||
});
|
||||
expect(interaction.__count).toBe(1);
|
||||
|
||||
throwInOnWorkStarted = true;
|
||||
expect(wrapped).toThrow('Expected error onWorkStarted');
|
||||
|
||||
// It should call the callback before re-throwing
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(interaction.__count).toBe(0);
|
||||
|
||||
// It should call other subscribers despite the earlier error
|
||||
expect(secondSubscriber.onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should cover onWorkStopped within wrap', done => {
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
const outerInteraction = Array.from(
|
||||
InteractionTracking.getCurrent(),
|
||||
)[0];
|
||||
expect(outerInteraction.__count).toBe(1);
|
||||
|
||||
let wrapped;
|
||||
let innerInteraction;
|
||||
|
||||
InteractionTracking.track(secondEvent.name, currentTime, () => {
|
||||
innerInteraction = Array.from(InteractionTracking.getCurrent())[1];
|
||||
expect(outerInteraction.__count).toBe(1);
|
||||
expect(innerInteraction.__count).toBe(1);
|
||||
|
||||
wrapped = InteractionTracking.wrap(jest.fn());
|
||||
expect(outerInteraction.__count).toBe(2);
|
||||
expect(innerInteraction.__count).toBe(2);
|
||||
});
|
||||
|
||||
expect(outerInteraction.__count).toBe(2);
|
||||
expect(innerInteraction.__count).toBe(1);
|
||||
|
||||
throwInOnWorkStopped = true;
|
||||
expect(wrapped).toThrow('Expected error onWorkStopped');
|
||||
throwInOnWorkStopped = false;
|
||||
|
||||
// It should restore the previous interactions
|
||||
expect(InteractionTracking.getCurrent()).toMatchInteractions([
|
||||
outerInteraction,
|
||||
]);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(outerInteraction.__count).toBe(1);
|
||||
expect(innerInteraction.__count).toBe(0);
|
||||
|
||||
expect(secondSubscriber.onWorkStopped).toHaveBeenCalledTimes(2);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should cover the callback within wrap', done => {
|
||||
expect(onWorkStarted).not.toHaveBeenCalled();
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
|
||||
let wrapped;
|
||||
let interaction;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
interaction = Array.from(InteractionTracking.getCurrent())[0];
|
||||
wrapped = InteractionTracking.wrap(() => {
|
||||
throw Error('Expected error wrap');
|
||||
});
|
||||
});
|
||||
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(wrapped).toThrow('Expected error wrap');
|
||||
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork([interaction]);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should cover onWorkCanceled within wrap', () => {
|
||||
let interaction, wrapped;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
interaction = Array.from(InteractionTracking.getCurrent())[0];
|
||||
wrapped = InteractionTracking.wrap(jest.fn());
|
||||
});
|
||||
expect(interaction.__count).toBe(1);
|
||||
|
||||
throwInOnWorkCanceled = true;
|
||||
expect(wrapped.cancel).toThrow('Expected error onWorkCanceled');
|
||||
|
||||
expect(onWorkCanceled).toHaveBeenCalledTimes(1);
|
||||
|
||||
// It should update the interaction count so as not to interfere with subsequent calls
|
||||
expect(interaction.__count).toBe(0);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
|
||||
// It should call other subscribers despite the earlier error
|
||||
expect(secondSubscriber.onWorkCanceled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls lifecycle methods for track', () => {
|
||||
expect(onInteractionTracked).not.toHaveBeenCalled();
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
InteractionTracking.track(
|
||||
firstEvent.name,
|
||||
currentTime,
|
||||
() => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
|
||||
firstEvent,
|
||||
);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent]),
|
||||
threadID,
|
||||
);
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
|
||||
InteractionTracking.track(
|
||||
secondEvent.name,
|
||||
currentTime,
|
||||
() => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
|
||||
secondEvent,
|
||||
);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
expect(onWorkStopped).not.toHaveBeenCalled();
|
||||
},
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(secondEvent);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
},
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
expect(onWorkScheduled).not.toHaveBeenCalled();
|
||||
expect(onWorkCanceled).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent]),
|
||||
threadID,
|
||||
);
|
||||
});
|
||||
|
||||
it('calls lifecycle methods for wrap', () => {
|
||||
const unwrapped = jest.fn();
|
||||
let wrapped;
|
||||
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
|
||||
firstEvent,
|
||||
);
|
||||
|
||||
InteractionTracking.track(secondEvent.name, currentTime, () => {
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(onInteractionTracked).toHaveBeenLastNotifiedOfInteraction(
|
||||
secondEvent,
|
||||
);
|
||||
|
||||
wrapped = InteractionTracking.wrap(unwrapped, threadID);
|
||||
expect(onWorkScheduled).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkScheduled).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
wrapped();
|
||||
expect(unwrapped).toHaveBeenCalled();
|
||||
|
||||
expect(onWorkScheduled).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkCanceled).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(3);
|
||||
expect(onWorkStarted).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(3);
|
||||
expect(onWorkStopped).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted.mock.calls[0][0],
|
||||
).toMatchInteraction(firstEvent);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted.mock.calls[1][0],
|
||||
).toMatchInteraction(secondEvent);
|
||||
});
|
||||
|
||||
it('should call the correct interaction subscriber methods when a wrapped callback is canceled', () => {
|
||||
const fnOne = jest.fn();
|
||||
const fnTwo = jest.fn();
|
||||
let wrappedOne, wrappedTwo;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
wrappedOne = InteractionTracking.wrap(fnOne, threadID);
|
||||
InteractionTracking.track(secondEvent.name, currentTime, () => {
|
||||
wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
|
||||
});
|
||||
});
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(2);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
expect(onWorkCanceled).not.toHaveBeenCalled();
|
||||
expect(onWorkStarted).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkStopped).toHaveBeenCalledTimes(2);
|
||||
|
||||
wrappedTwo.cancel();
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(secondEvent);
|
||||
expect(onWorkCanceled).toHaveBeenCalledTimes(1);
|
||||
expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent, secondEvent]),
|
||||
threadID,
|
||||
);
|
||||
|
||||
wrappedOne.cancel();
|
||||
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
expect(onWorkCanceled).toHaveBeenCalledTimes(2);
|
||||
expect(onWorkCanceled).toHaveBeenLastNotifiedOfWork(
|
||||
new Set([firstEvent]),
|
||||
threadID,
|
||||
);
|
||||
|
||||
expect(fnOne).not.toHaveBeenCalled();
|
||||
expect(fnTwo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not end an interaction twice if wrap is used to schedule follow up work within another wrap', () => {
|
||||
const fnOne = jest.fn(() => {
|
||||
wrappedTwo = InteractionTracking.wrap(fnTwo, threadID);
|
||||
});
|
||||
const fnTwo = jest.fn();
|
||||
let wrappedOne, wrappedTwo;
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {
|
||||
wrappedOne = InteractionTracking.wrap(fnOne, threadID);
|
||||
});
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
wrappedOne();
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
|
||||
|
||||
wrappedTwo();
|
||||
|
||||
expect(onInteractionTracked).toHaveBeenCalledTimes(1);
|
||||
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
onInteractionScheduledWorkCompleted,
|
||||
).toHaveBeenLastNotifiedOfInteraction(firstEvent);
|
||||
});
|
||||
|
||||
it('should unsubscribe', () => {
|
||||
InteractionTrackingSubscriptions.unsubscribe(firstSubscriber);
|
||||
InteractionTracking.track(firstEvent.name, currentTime, () => {});
|
||||
|
||||
expect(onInteractionTracked).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled', () => {
|
||||
beforeEach(() => loadModules({enableInteractionTracking: false}));
|
||||
|
||||
// TODO
|
||||
});
|
||||
});
|
||||
12
packages/interaction-tracking/subscriptions.js
Normal file
12
packages/interaction-tracking/subscriptions.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export * from './src/InteractionTrackingSubscriptions';
|
||||
@@ -42,9 +42,6 @@ export const enableProfilerTimer = __PROFILE__;
|
||||
// Track which interactions trigger each commit.
|
||||
export const enableInteractionTracking = false;
|
||||
|
||||
// Track which interactions trigger each commit.
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
invariant(false, 'Not implemented.');
|
||||
|
||||
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = __DEV__;
|
||||
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableInteractionTracking = false;
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
|
||||
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableInteractionTracking = false;
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -27,7 +27,6 @@ export const enableUserTimingAPI = __DEV__;
|
||||
export const warnAboutLegacyContextAPI = __DEV__;
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableInteractionTracking = false;
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -22,7 +22,6 @@ export const warnAboutDeprecatedLifecycles = false;
|
||||
export const warnAboutLegacyContextAPI = false;
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableInteractionTracking = false;
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
|
||||
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableInteractionTracking = false;
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
|
||||
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
export const enableProfilerTimer = false;
|
||||
export const enableInteractionTracking = false;
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -22,7 +22,6 @@ export const warnAboutLegacyContextAPI = false;
|
||||
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
export const enableProfilerTimer = false;
|
||||
export const enableInteractionTracking = false;
|
||||
export const enableInteractionTrackingObserver = false;
|
||||
|
||||
// Only used in www builds.
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -33,7 +33,6 @@ export let enableUserTimingAPI = __DEV__;
|
||||
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableInteractionTracking = __PROFILE__;
|
||||
export const enableInteractionTrackingObserver = __PROFILE__;
|
||||
|
||||
let refCount = 0;
|
||||
export function addUserTimingListener() {
|
||||
|
||||
@@ -404,6 +404,15 @@ const bundles = [
|
||||
global: 'InteractionTracking',
|
||||
externals: [],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'interaction-tracking-subscriptions',
|
||||
bundleTypes: [NODE_DEV, NODE_PROD, UMD_DEV, UMD_PROD],
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'interaction-tracking/subscriptions',
|
||||
global: 'InteractionTrackingSubscriptions',
|
||||
externals: ['interaction-tracking'],
|
||||
},
|
||||
];
|
||||
|
||||
// Based on deep-freeze by substack (public domain)
|
||||
|
||||
@@ -21,6 +21,7 @@ const knownGlobals = Object.freeze({
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
'interaction-tracking': 'InteractionTracking',
|
||||
'interaction-tracking/subscriptions': 'InteractionTrackingSubscriptions',
|
||||
});
|
||||
|
||||
// Given ['react'] in bundle externals, returns { 'react': 'React' }.
|
||||
|
||||
Reference in New Issue
Block a user