<|start_filename|>src/__tests__/trackEventMethodDecorator.test.js<|end_filename|>
describe('trackEventMethodDecorator', () => {
  // eslint-disable-next-line global-require
  const trackEventMethodDecorator = require('../trackEventMethodDecorator')
    .default;

  it('is a decorator (exports a function, that returns a function)', () => {
    expect(typeof trackEventMethodDecorator).toBe('function');

    const decorated = trackEventMethodDecorator();
    expect(typeof decorated).toBe('function');
  });

  it('properly calls trackEvent when trackingData is a plain object', () => {
    const dummyData = {};
    const trackingData = dummyData;
    const trackEvent = jest.fn();
    const spyTestEvent = jest.fn();

    class TestClass {
      constructor() {
        this.props = {
          tracking: {
            trackEvent,
          },
        };
      }

      @trackEventMethodDecorator(trackingData)
      // eslint-disable-next-line class-methods-use-this
      handleTestEvent(x) {
        spyTestEvent(x);
      }
    }

    const myTC = new TestClass();
    myTC.handleTestEvent('x');

    expect(trackEvent).toHaveBeenCalledWith(dummyData);
    expect(spyTestEvent).toHaveBeenCalledWith('x');
  });

  it('properly calls trackEvent when trackingData is a function', () => {
    const dummyData = {};
    const trackingData = jest.fn(() => dummyData);
    const trackEvent = jest.fn();
    const spyTestEvent = jest.fn();

    class TestClass {
      constructor() {
        this.props = {
          tracking: {
            trackEvent,
          },
        };
      }

      @trackEventMethodDecorator(trackingData)
      handleTestEvent = spyTestEvent;
    }

    const myTC = new TestClass();
    myTC.handleTestEvent('x');

    expect(trackingData).toHaveBeenCalledTimes(1);
    expect(trackEvent).toHaveBeenCalledWith(dummyData);
    expect(spyTestEvent).toHaveBeenCalledWith('x');
  });

  it('properly passes through the correct arguments when trackingData is a function', () => {
    const dummyData = {};
    const trackingData = jest.fn(() => dummyData);
    const trackEvent = jest.fn();
    const eventData = 'eventData';
    const spyTestEvent = jest.fn(() => eventData);
    const dummyArgument = 'x';

    class TestClass {
      constructor() {
        this.props = {
          tracking: {
            trackEvent,
          },
        };
        this.state = {
          myState: 'someState',
        };
      }

      @trackEventMethodDecorator(trackingData)
      handleTestEvent = spyTestEvent;
    }

    const myTC = new TestClass();
    const result = myTC.handleTestEvent(dummyArgument);

    // Access the trackingData arguments
    const trackingDataArguments = trackingData.mock.calls[0];

    expect(result).toEqual(eventData);
    expect(trackingData).toHaveBeenCalledTimes(1);
    expect(trackingDataArguments[0]).toEqual(myTC.props);
    expect(trackingDataArguments[1]).toEqual(myTC.state);
    // Here we have access to the raw `arguments` object, which is not an actual Array,
    // so in order to compare, we convert the arguments to an array.
    expect(Array.from(trackingDataArguments[2])).toEqual([dummyArgument]);

    expect(trackEvent).toHaveBeenCalledWith(dummyData);
    expect(spyTestEvent).toHaveBeenCalledWith(dummyArgument);
  });

  it('properly passes through the correct arguments when trackingData is a function and the decorated method returns a promise', async () => {
    const dummyData = {};
    const trackingData = jest.fn(() => dummyData);
    const trackEvent = jest.fn();
    const dummyResolve = 'res';
    const spyTestEvent = jest.fn(() => Promise.resolve(dummyResolve));
    const dummyArgument = 'x';

    class TestClass {
      constructor() {
        this.props = {
          tracking: {
            trackEvent,
          },
        };
        this.state = {
          myState: 'someState',
        };
      }

      @trackEventMethodDecorator(trackingData)
      handleTestEvent = spyTestEvent;
    }

    const myTC = new TestClass();
    const result = await myTC.handleTestEvent(dummyArgument);

    // Access the trackingData arguments
    const trackingDataArguments = trackingData.mock.calls[0];

    expect(result).toEqual(dummyResolve);
    expect(trackingData).toHaveBeenCalledTimes(1);
    expect(trackingDataArguments[0]).toEqual(myTC.props);
    expect(trackingDataArguments[1]).toEqual(myTC.state);
    // Here we have access to the raw `arguments` object, which is not an actual Array,
    // so in order to compare, we convert the arguments to an array.
    expect(Array.from(trackingDataArguments[2])).toEqual([dummyArgument]);
    expect(trackingDataArguments[3]).toEqual([dummyResolve]);

    expect(trackEvent).toHaveBeenCalledWith(dummyData);
    expect(spyTestEvent).toHaveBeenCalledWith(dummyArgument);
  });

  [null, undefined].forEach(value => {
    it(`does not call trackEvent if the data is ${value}`, () => {
      const trackingData = jest.fn(() => value);
      const trackEvent = jest.fn();
      const spyTestEvent = jest.fn();

      class TestClass {
        constructor() {
          this.props = {
            tracking: {
              trackEvent,
            },
          };
        }

        @trackEventMethodDecorator(trackingData)
        handleTestEvent = spyTestEvent;
      }

      const myTC = new TestClass();
      myTC.handleTestEvent('x');

      expect(trackingData).toHaveBeenCalledTimes(1);
      expect(trackEvent).not.toHaveBeenCalled();
      expect(spyTestEvent).toHaveBeenCalledWith('x');
    });
  });

  it('properly calls trackData when an async method has resolved', async () => {
    const dummyData = {};
    const trackingData = jest.fn(() => dummyData);
    const trackEvent = jest.fn();
    let resolveTest;
    const spyTestEvent = jest.fn(
      () =>
        new Promise(resolve => {
          resolveTest = resolve;
        })
    );

    class TestClass {
      constructor() {
        this.props = {
          tracking: {
            trackEvent,
          },
        };
      }

      @trackEventMethodDecorator(trackingData)
      handleTestEvent = spyTestEvent;
    }

    const myTC = new TestClass();
    myTC.handleTestEvent();

    expect(trackEvent).not.toHaveBeenCalled();
    await resolveTest();
    expect(trackEvent).toHaveBeenCalledWith(dummyData);
  });

  it('calls tracking function when the async function throws and will rethrow the error', async () => {
    const dummyData = {};
    const trackingData = jest.fn(() => dummyData);
    const trackEvent = jest.fn();
    const spyTestEvent = jest.fn(
      () =>
        new Promise(() => {
          throw new Error();
        })
    );

    class TestClass {
      constructor() {
        this.props = {
          tracking: {
            trackEvent,
          },
        };
      }

      @trackEventMethodDecorator(trackingData)
      handleTestEvent = spyTestEvent;
    }

    const myTC = new TestClass();

    try {
      await myTC.handleTestEvent();
    } catch (error) {
      expect(trackEvent).toHaveBeenCalledWith(dummyData);
      const trackingDataArguments = trackingData.mock.calls[0];
      // the resulting error should be passed to the tracking data
      expect(trackingDataArguments[3]).toEqual([{}, error]);
      expect(error).toBeInstanceOf(Error);
    }
  });

  it('calls the tracking method before the tracking decorator function', async () => {
    const dummyData = {};
    const trackingData = jest.fn(() => dummyData);
    const trackEvent = jest.fn();
    const spyTestEvent = jest.fn(() => Promise.resolve());

    class TestClass {
      constructor() {
        this.props = {
          tracking: {
            trackEvent,
          },
        };
      }

      @trackEventMethodDecorator(trackingData)
      handleTestEvent = spyTestEvent;
    }

    const myTC = new TestClass();
    myTC.handleTestEvent();

    await myTC.handleTestEvent();

    // all function calls should happen before all tracking calls
    spyTestEvent.mock.invocationCallOrder.forEach(fnOrder =>
      trackEvent.mock.invocationCallOrder.forEach(trackOrder =>
        expect(fnOrder).toBeLessThan(trackOrder)
      )
    );
  });
});


<|start_filename|>src/__tests__/dispatchTrackingEvent.test.js<|end_filename|>
import dispatchTrackingEvent from '../dispatchTrackingEvent';

const testEventData = { test: 'magik' };

describe('dispatchTrackingEvent', () => {
  beforeEach(() => {
    window.dataLayer = undefined;
  });

  it('exports a function', () => {
    expect(typeof dispatchTrackingEvent).toBe('function');
  });

  it('will create window.dataLayer[] if it does not exit', () => {
    expect(window.dataLayer).not.toBeDefined();

    dispatchTrackingEvent(testEventData);

    expect(window.dataLayer).toEqual([testEventData]);
  });

  it('will push to window.dataLayer[] if it exists', () => {
    expect(window.dataLayer).not.toBeDefined();

    dispatchTrackingEvent(testEventData);
    expect(window.dataLayer).toEqual([testEventData]);

    dispatchTrackingEvent(testEventData);
    expect(window.dataLayer).toEqual([testEventData, testEventData]);
  });

  it('will not push to window.dataLayer[] if the event data is empty', () => {
    expect(window.dataLayer).not.toBeDefined();

    dispatchTrackingEvent(testEventData);
    expect(window.dataLayer).toEqual([testEventData]);

    dispatchTrackingEvent({});
    expect(window.dataLayer).toEqual([testEventData]);
  });
});


<|start_filename|>src/__tests__/trackingHoC.test.js<|end_filename|>
/* eslint-disable react/no-multi-comp */
import React from 'react';

const wTCDmock = jest.fn(() => () => {});
jest.setMock('../withTrackingComponentDecorator', wTCDmock);

const tEMDmock = jest.fn(() => () => {});
jest.setMock('../trackEventMethodDecorator', tEMDmock);

describe('tracking HoC', () => {
  // eslint-disable-next-line global-require
  const trackingHoC = require('../trackingHoC').default;

  it('detects a class', () => {
    const testClass = { testClass: true };
    const options = {};

    @trackingHoC(testClass, options)
    class TestClass extends React.Component {} // eslint-disable-line

    new TestClass(); // eslint-disable-line no-new

    expect(wTCDmock).toHaveBeenCalledWith(testClass, options);
  });

  it('detects a class method', () => {
    const testMethod = { testMethod: true };
    class TestMethod {
      @trackingHoC(testMethod)
      blah = () => {};
    }

    const myTest = new TestMethod();
    myTest.blah();

    expect(tEMDmock).toHaveBeenCalledWith(testMethod);
  });

  it('works on stateless functional components', () => {
    const testStateless = { testStateless: true };
    const options = {};
    const TestComponent = () => <div />;

    trackingHoC(testStateless, options)(TestComponent);

    expect(wTCDmock).toHaveBeenCalledWith(testStateless, options);
  });
});


<|start_filename|>src/TrackingPropType.js<|end_filename|>
import PropTypes from 'prop-types';

export default PropTypes.shape({
  trackEvent: PropTypes.func,
  getTrackingData: PropTypes.func,
});


<|start_filename|>src/__tests__/withTrackingComponentDecorator.test.js<|end_filename|>
/* eslint-disable react/destructuring-assignment,react/prop-types,react/jsx-props-no-spreading */
import React from 'react';
import { shallow, mount } from 'enzyme';

const mockDispatchTrackingEvent = jest.fn();
jest.setMock('../dispatchTrackingEvent', mockDispatchTrackingEvent);

describe('withTrackingComponentDecorator', () => {
  // eslint-disable-next-line global-require
  const withTrackingComponentDecorator = require('../withTrackingComponentDecorator')
    .default;

  it('is a decorator (exports a function, that returns a function)', () => {
    expect(typeof withTrackingComponentDecorator).toBe('function');

    const decorated = withTrackingComponentDecorator();
    expect(typeof decorated).toBe('function');
  });

  describe('with process option', () => {
    const props = { props: 1 };
    const trackingContext = { page: 1 };
    const process = jest.fn(() => null);

    @withTrackingComponentDecorator({}, { process })
    class ParentTestComponent extends React.Component {
      static displayName = 'ParentTestComponent';

      render() {
        return this.props.children;
      }
    }

    @withTrackingComponentDecorator(trackingContext)
    class TestComponent extends React.Component {
      static displayName = 'TestComponent';

      render() {
        return null;
      }
    }

    beforeEach(() => {
      mockDispatchTrackingEvent.mockClear();
    });

    it('process function gets called', () => {
      mount(
        <ParentTestComponent {...props}>
          <TestComponent />
        </ParentTestComponent>
      );

      expect(process).toHaveBeenCalled();
      expect(mockDispatchTrackingEvent).not.toHaveBeenCalled();
    });
  });

  describe('with process option from parent and dispatchOnMount option on component', () => {
    const props = { props: 1 };
    const trackingContext = { page: 1 };
    const process = jest.fn(() => ({ event: 'pageView' }));
    const dispatchOnMount = jest.fn(() => ({ specificEvent: true }));

    @withTrackingComponentDecorator({}, { process })
    class ParentTestComponent extends React.Component {
      static displayName = 'ParentTestComponent';

      render() {
        return this.props.children;
      }
    }

    @withTrackingComponentDecorator(trackingContext, { dispatchOnMount })
    class TestComponent extends React.Component {
      static displayName = 'TestComponent';

      render() {
        return null;
      }
    }

    beforeEach(() => {
      mockDispatchTrackingEvent.mockClear();
    });

    it('dispatches only once when process and dispatchOnMount functions are passed', () => {
      mount(
        <ParentTestComponent {...props}>
          <TestComponent />
        </ParentTestComponent>
      );

      expect(process).toHaveBeenCalled();
      expect(dispatchOnMount).toHaveBeenCalled();
      expect(mockDispatchTrackingEvent).toHaveBeenCalledWith({
        page: 1,
        event: 'pageView',
        specificEvent: true,
      });
      expect(mockDispatchTrackingEvent).toHaveBeenCalledTimes(1);
    });
  });

  describe('with function trackingContext', () => {
    const props = { page: 1 };
    const trackingContext = jest.fn(p => ({ page: p.page }));
    const process = jest.fn(() => ({ event: 'pageView' }));
    const dispatchOnMount = jest.fn(() => ({ specificEvent: true }));

    @withTrackingComponentDecorator({}, { process })
    class ParentTestComponent extends React.Component {
      static displayName = 'ParentTestComponent';

      render() {
        return this.props.children;
      }
    }

    @withTrackingComponentDecorator(trackingContext, { dispatchOnMount })
    class TestComponent extends React.Component {
      static displayName = 'TestComponent';

      render() {
        return null;
      }
    }

    beforeEach(() => {
      mockDispatchTrackingEvent.mockClear();
    });

    it('dispatches only once when process and dispatchOnMount functions are passed', () => {
      mount(
        <ParentTestComponent>
          <TestComponent {...props} />
        </ParentTestComponent>
      );

      expect(process).toHaveBeenCalled();
      expect(dispatchOnMount).toHaveBeenCalled();
      expect(trackingContext).toHaveBeenCalled();
      expect(mockDispatchTrackingEvent).toHaveBeenCalledWith({
        page: 1,
        event: 'pageView',
        specificEvent: true,
      });
      expect(mockDispatchTrackingEvent).toHaveBeenCalledTimes(1);
    });
  });

  describe('with process option from parent is falsey and dispatchOnMount option is true', () => {
    const props = { props: 1 };
    const trackingContext = { page: 1 };
    const process = jest.fn(() => null);
    const dispatchOnMount = true;

    @withTrackingComponentDecorator({}, { process })
    class ParentTestComponent extends React.Component {
      static displayName = 'ParentTestComponent';

      render() {
        return this.props.children;
      }
    }

    @withTrackingComponentDecorator(trackingContext, { dispatchOnMount })
    class TestComponent extends React.Component {
      static displayName = 'TestComponent';

      render() {
        return null;
      }
    }

    beforeEach(() => {
      mockDispatchTrackingEvent.mockClear();
    });

    it('dispatches only once when process and dispatchOnMount functions are passed', () => {
      mount(
        <ParentTestComponent {...props}>
          <TestComponent />
        </ParentTestComponent>
      );

      expect(process).toHaveBeenCalled();
      expect(mockDispatchTrackingEvent).toHaveBeenCalledWith({
        page: 1,
      });
      expect(mockDispatchTrackingEvent).toHaveBeenCalledTimes(1);
    });
  });

  describe('with a prop called tracking that has two functions as keys', () => {
    const dummyData = { page: 1 };

    @withTrackingComponentDecorator(dummyData)
    class TestComponent {
      static displayName = 'TestComponent';
    }

    const component = shallow(<TestComponent />).children();

    beforeEach(() => {
      mockDispatchTrackingEvent.mockClear();
    });

    it('prop is named tracking and has two keys, trackEvent and getTrackingData', () => {
      expect(component.props().tracking).toBeDefined();
      expect(component.props().tracking).toBeInstanceOf(Object);
      expect(component.props().tracking).toHaveProperty('trackEvent');
      expect(component.props().tracking).toHaveProperty('getTrackingData');
    });

    it('prop named trackEvent is a function', () => {
      expect(component.props().tracking.trackEvent).toBeInstanceOf(Function);
    });

    it('when trackEvent is called, from props, it will dispatch event in trackEvent', () => {
      expect(mockDispatchTrackingEvent).not.toHaveBeenCalled();
      component.props().tracking.trackEvent(dummyData);
      expect(mockDispatchTrackingEvent).toHaveBeenCalledWith(dummyData);
    });

    it('prop named getTrackingData is a function', () => {
      expect(component.props().tracking.getTrackingData).toBeInstanceOf(
        Function
      );
    });

    it('when getTrackingData is called, from props, it will return the data passed to the decorator', () => {
      expect(component.props().tracking.getTrackingData()).toMatchObject(
        dummyData
      );
    });
  });

  describe('hoist non react statics', () => {
    const dummyData = { page: 1 };

    it("should hoist a class's static method when decorated", () => {
      @withTrackingComponentDecorator(dummyData)
      class StaticComponent {
        static someMethod() {
          return 'test';
        }

        static someVar = 'test';
      }

      expect(StaticComponent.someMethod).toBeDefined();
      expect(StaticComponent.someMethod()).toEqual('test');
      expect(StaticComponent.someVar).toEqual('test');
    });

    it("should hoist a class's static method when wrapped via HoC", () => {
      class StaticComponent {
        static someMethod() {
          return 'test';
        }

        static someVar = 'test';
      }

      const DecoratedComponent = withTrackingComponentDecorator(dummyData)(
        StaticComponent
      );

      expect(DecoratedComponent.someMethod).toBeDefined();
      expect(DecoratedComponent.someMethod()).toEqual('test');
      expect(DecoratedComponent.someMethod).toEqual(StaticComponent.someMethod);
      expect(DecoratedComponent.someVar).toEqual('test');
    });
  });
});


<|start_filename|>src/__tests__/makeClassMemberDecorator.test.js<|end_filename|>
import makeClassMemberDecorator from '../makeClassMemberDecorator';

describe('makeClassMemberDecorator', () => {
  it('is a decorator (exports a function, that returns a function)', () => {
    expect(typeof makeClassMemberDecorator).toBe('function');

    const decorated = makeClassMemberDecorator();
    expect(typeof decorated).toBe('function');
  });

  it('properly decorates class methods', () => {
    const mockDecorator = jest.fn(x => x);
    const thingSpy = jest.fn();
    class Test {
      @makeClassMemberDecorator(mockDecorator)
      // eslint-disable-next-line class-methods-use-this
      thing() {
        thingSpy();
      }
    }
    const myTest = new Test();

    expect(thingSpy).not.toHaveBeenCalled();
    myTest.thing();
    expect(mockDecorator).toHaveBeenCalledTimes(1);
    expect(thingSpy).toHaveBeenCalledTimes(1);
  });

  it('properly decorates field syntax', () => {
    const mockDecorator = jest.fn(x => x);
    const fieldSpy = jest.fn();
    class Test {
      @makeClassMemberDecorator(mockDecorator)
      field = fieldSpy;
    }
    const myTest = new Test();

    expect(fieldSpy).not.toHaveBeenCalled();
    expect(mockDecorator).not.toHaveBeenCalled();
    myTest.field();
    expect(mockDecorator).toHaveBeenCalledTimes(1);
    expect(fieldSpy).toHaveBeenCalledTimes(1);
  });

  it('throws when called on unsupported descriptor', () => {
    expect(() => makeClassMemberDecorator()(null, null, {})).toThrow();
  });
});


<|start_filename|>src/makeClassMemberDecorator.js<|end_filename|>
/**
 * Helper to decorate class member
 * Supports class plain methods, field syntax and lazy methods
 * @param {Function} decorate Actual decorator function.
 * Example:
 *   decoratedFn => function () {
 *     // do stuff...
 *     return Reflect.apply(decoratedFn, this, arguments);
 *   }
 * @returns {Function} Class member decorator ((target, name, descriptor) => newDescriptor)
 */
export default function makeClassMemberDecorator(decorate) {
  return function decorateClassMember(target, name, descriptor) {
    const { configurable, enumerable, value, get, initializer } = descriptor;

    if (value) {
      return {
        configurable,
        enumerable,
        value: decorate(value),
      };
    }

    // support lazy initializer
    if (get || initializer) {
      return {
        configurable,
        enumerable,
        get() {
          // This happens if someone accesses the
          // property directly on the prototype
          if (this === target) {
            return null;
          }

          const resolvedValue = initializer
            ? Reflect.apply(initializer, this, [])
            : Reflect.apply(get, this, []);
          const decoratedValue = decorate(resolvedValue).bind(this);

          Reflect.defineProperty(this, name, {
            configurable,
            enumerable,
            value: decoratedValue,
          });

          return decoratedValue;
        },
      };
    }

    throw new Error(
      'called makeClassMemberDecorator on unsupported descriptor'
    );
  };
}


<|start_filename|>src/index.js<|end_filename|>
import track from './trackingHoC';

export { default as withTracking } from './withTrackingComponentDecorator';
export { default as trackEvent } from './trackEventMethodDecorator';
export { default as TrackingPropType } from './TrackingPropType';
export { default as useTracking } from './useTracking';

export { track, track as default };


<|start_filename|>src/dispatchTrackingEvent.js<|end_filename|>
export default function dispatchTrackingEvent(data) {
  if (Object.keys(data).length > 0) {
    (window.dataLayer = window.dataLayer || []).push(data);
  }
}
