433 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			433 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
import API from '~/api';
 | 
						|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 | 
						|
import InternalEvents from '~/tracking/internal_events';
 | 
						|
import { LOAD_INTERNAL_EVENTS_SELECTOR } from '~/tracking/constants';
 | 
						|
import * as utils from '~/tracking/utils';
 | 
						|
import { Tracker } from '~/tracking/tracker';
 | 
						|
import Tracking from '~/tracking';
 | 
						|
import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
 | 
						|
 | 
						|
const allowedAdditionalProps = {
 | 
						|
  property: 'value',
 | 
						|
  label: 'value',
 | 
						|
  value: 2,
 | 
						|
};
 | 
						|
 | 
						|
jest.mock('~/api', () => ({
 | 
						|
  trackInternalEvent: jest.fn(),
 | 
						|
}));
 | 
						|
 | 
						|
jest.mock('~/tracking/utils', () => ({
 | 
						|
  ...jest.requireActual('~/tracking/utils'),
 | 
						|
  getInternalEventHandlers: jest.fn(),
 | 
						|
}));
 | 
						|
 | 
						|
Tracker.enabled = jest.fn();
 | 
						|
 | 
						|
const event = 'TestEvent';
 | 
						|
 | 
						|
describe('InternalEvents', () => {
 | 
						|
  beforeEach(() => {
 | 
						|
    setHTMLFixture(`<div><button data-event-tracking data-testid="button" /></div>`);
 | 
						|
  });
 | 
						|
 | 
						|
  afterEach(() => {
 | 
						|
    resetHTMLFixture();
 | 
						|
  });
 | 
						|
 | 
						|
  const findButton = () => document.querySelector('[data-testid="button"]');
 | 
						|
  const triggerClick = () => {
 | 
						|
    findButton().dispatchEvent(new Event('click', { bubbles: true }));
 | 
						|
  };
 | 
						|
 | 
						|
  describe('trackEvent', () => {
 | 
						|
    const category = 'TestCategory';
 | 
						|
 | 
						|
    it('trackEvent calls API.trackInternalEvent with correct arguments', () => {
 | 
						|
      InternalEvents.trackEvent(event, {}, category);
 | 
						|
 | 
						|
      expect(API.trackInternalEvent).toHaveBeenCalledTimes(1);
 | 
						|
      expect(API.trackInternalEvent).toHaveBeenCalledWith(event, {});
 | 
						|
    });
 | 
						|
 | 
						|
    it('trackEvent calls Tracking.event with correct arguments including category', () => {
 | 
						|
      jest.spyOn(Tracking, 'event').mockImplementation(() => {});
 | 
						|
 | 
						|
      InternalEvents.trackEvent(event, {}, category);
 | 
						|
 | 
						|
      expect(Tracking.event).toHaveBeenCalledWith(category, event, expect.any(Object));
 | 
						|
    });
 | 
						|
 | 
						|
    it('trackEvent calls Tracking.event with event name, category and additional properties', () => {
 | 
						|
      jest.spyOn(Tracking, 'event').mockImplementation(() => {});
 | 
						|
 | 
						|
      InternalEvents.trackEvent(event, allowedAdditionalProps, category);
 | 
						|
 | 
						|
      expect(Tracking.event).toHaveBeenCalledWith(
 | 
						|
        category,
 | 
						|
        event,
 | 
						|
        expect.objectContaining({
 | 
						|
          context: expect.any(Object),
 | 
						|
          value: 2,
 | 
						|
          property: 'value',
 | 
						|
          label: 'value',
 | 
						|
        }),
 | 
						|
      );
 | 
						|
    });
 | 
						|
 | 
						|
    it('trackEvent calls Tracking.event with event name, category, base and custom properties', () => {
 | 
						|
      jest.spyOn(Tracking, 'event').mockImplementation(() => {});
 | 
						|
 | 
						|
      const additionalProps = {
 | 
						|
        ...allowedAdditionalProps,
 | 
						|
        key: 'value',
 | 
						|
      };
 | 
						|
 | 
						|
      InternalEvents.trackEvent(event, additionalProps, category);
 | 
						|
 | 
						|
      expect(Tracking.event).toHaveBeenCalledWith(
 | 
						|
        category,
 | 
						|
        event,
 | 
						|
        expect.objectContaining({
 | 
						|
          context: expect.any(Object),
 | 
						|
          value: 2,
 | 
						|
          property: 'value',
 | 
						|
          label: 'value',
 | 
						|
          extra: { key: 'value' },
 | 
						|
        }),
 | 
						|
      );
 | 
						|
    });
 | 
						|
 | 
						|
    it('trackEvent calls trackBrowserSDK with event name', () => {
 | 
						|
      jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
 | 
						|
 | 
						|
      InternalEvents.trackEvent(event);
 | 
						|
 | 
						|
      expect(InternalEvents.trackBrowserSDK).toHaveBeenCalledTimes(1);
 | 
						|
      expect(InternalEvents.trackBrowserSDK).toHaveBeenCalledWith(event, {});
 | 
						|
    });
 | 
						|
 | 
						|
    it('trackEvent calls trackBrowserSDK with event name and additional Properties', () => {
 | 
						|
      jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
 | 
						|
 | 
						|
      InternalEvents.trackEvent(event, allowedAdditionalProps);
 | 
						|
 | 
						|
      expect(InternalEvents.trackBrowserSDK).toHaveBeenCalledTimes(1);
 | 
						|
      expect(InternalEvents.trackBrowserSDK).toHaveBeenCalledWith(event, {
 | 
						|
        property: 'value',
 | 
						|
        label: 'value',
 | 
						|
        value: 2,
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it('throws an error if base property has incorrect type', () => {
 | 
						|
      jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
 | 
						|
      jest.spyOn(Tracking, 'event').mockImplementation(() => {});
 | 
						|
 | 
						|
      const additionalProps = {
 | 
						|
        ...allowedAdditionalProps,
 | 
						|
        value: 'invalidType',
 | 
						|
      };
 | 
						|
 | 
						|
      expect(() => {
 | 
						|
        InternalEvents.trackEvent(event, additionalProps, category);
 | 
						|
      }).toThrow('value should be of type: number. Provided type is: string.');
 | 
						|
 | 
						|
      expect(InternalEvents.trackBrowserSDK).not.toHaveBeenCalled();
 | 
						|
      expect(Tracking.event).not.toHaveBeenCalled();
 | 
						|
      expect(API.trackInternalEvent).not.toHaveBeenCalled();
 | 
						|
    });
 | 
						|
 | 
						|
    it('does not throw an error for custom properties', () => {
 | 
						|
      jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
 | 
						|
      jest.spyOn(Tracking, 'event').mockImplementation(() => {});
 | 
						|
 | 
						|
      const additionalProps = {
 | 
						|
        ...allowedAdditionalProps,
 | 
						|
        key1: 'value1',
 | 
						|
        key2: 2,
 | 
						|
      };
 | 
						|
 | 
						|
      expect(() => {
 | 
						|
        InternalEvents.trackEvent(event, additionalProps, category);
 | 
						|
      }).not.toThrow();
 | 
						|
 | 
						|
      expect(InternalEvents.trackBrowserSDK).toHaveBeenCalled();
 | 
						|
      expect(Tracking.event).toHaveBeenCalled();
 | 
						|
      expect(API.trackInternalEvent).toHaveBeenCalled();
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('mixin', () => {
 | 
						|
    let wrapper;
 | 
						|
    const Component = {
 | 
						|
      template: `
 | 
						|
    <div>
 | 
						|
      <button data-testid="button" @click="handleButton1Click">Button</button>
 | 
						|
      <button data-testid="button2" @click="handleButton2Click">Button2</button>
 | 
						|
    </div>
 | 
						|
  `,
 | 
						|
      methods: {
 | 
						|
        handleButton1Click() {
 | 
						|
          this.trackEvent(event);
 | 
						|
        },
 | 
						|
        handleButton2Click() {
 | 
						|
          this.trackEvent(event, {
 | 
						|
            property: 'value',
 | 
						|
            label: 'value',
 | 
						|
            value: 2,
 | 
						|
          });
 | 
						|
        },
 | 
						|
      },
 | 
						|
      mixins: [InternalEvents.mixin()],
 | 
						|
    };
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      wrapper = shallowMountExtended(Component);
 | 
						|
    });
 | 
						|
 | 
						|
    it('this.trackEvent function calls InternalEvent`s track function with an event', async () => {
 | 
						|
      const trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent');
 | 
						|
 | 
						|
      await wrapper.findByTestId('button').trigger('click');
 | 
						|
 | 
						|
      expect(trackEventSpy).toHaveBeenCalledTimes(1);
 | 
						|
      expect(trackEventSpy).toHaveBeenCalledWith(event, {}, undefined);
 | 
						|
    });
 | 
						|
 | 
						|
    it('this.trackEvent function calls InternalEvent`s track function with an event and additional Properties', async () => {
 | 
						|
      const trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent');
 | 
						|
 | 
						|
      await wrapper.findByTestId('button2').trigger('click');
 | 
						|
 | 
						|
      expect(trackEventSpy).toHaveBeenCalledTimes(1);
 | 
						|
      expect(trackEventSpy).toHaveBeenCalledWith(
 | 
						|
        event,
 | 
						|
        {
 | 
						|
          property: 'value',
 | 
						|
          label: 'value',
 | 
						|
          value: 2,
 | 
						|
        },
 | 
						|
        undefined,
 | 
						|
      );
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('bindInternalEventDocument', () => {
 | 
						|
    let disposeBind;
 | 
						|
    let trackEventSpy;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      Tracker.enabled.mockReturnValue(true);
 | 
						|
      trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent');
 | 
						|
    });
 | 
						|
 | 
						|
    afterEach(() => {
 | 
						|
      disposeBind?.();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should not bind event handlers if tracker is not enabled', () => {
 | 
						|
      Tracker.enabled.mockReturnValue(false);
 | 
						|
 | 
						|
      disposeBind = InternalEvents.bindInternalEventDocument();
 | 
						|
 | 
						|
      expect(disposeBind).toBe(null);
 | 
						|
      expect(utils.getInternalEventHandlers).not.toHaveBeenCalled();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should not bind event handlers if already bound', () => {
 | 
						|
      disposeBind = InternalEvents.bindInternalEventDocument();
 | 
						|
 | 
						|
      utils.getInternalEventHandlers.mockReset();
 | 
						|
 | 
						|
      const nextDisposeBind = InternalEvents.bindInternalEventDocument();
 | 
						|
 | 
						|
      expect(nextDisposeBind).toBe(null);
 | 
						|
      expect(utils.getInternalEventHandlers).not.toHaveBeenCalled();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should bind event handlers when not bound yet', () => {
 | 
						|
      disposeBind = InternalEvents.bindInternalEventDocument();
 | 
						|
 | 
						|
      triggerClick();
 | 
						|
 | 
						|
      expect(trackEventSpy).toHaveBeenCalledWith('', {});
 | 
						|
    });
 | 
						|
 | 
						|
    it('returns function that disposes listener', () => {
 | 
						|
      disposeBind = InternalEvents.bindInternalEventDocument();
 | 
						|
      disposeBind();
 | 
						|
 | 
						|
      triggerClick();
 | 
						|
 | 
						|
      expect(trackEventSpy).not.toHaveBeenCalled();
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('trackInternalLoadEvents', () => {
 | 
						|
    let querySelectorAllMock;
 | 
						|
    let mockElements;
 | 
						|
    const action = 'i_devops_action';
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      Tracker.enabled.mockReturnValue(true);
 | 
						|
      querySelectorAllMock = jest.fn();
 | 
						|
      document.querySelectorAll = querySelectorAllMock;
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return an empty array if Tracker is not enabled', () => {
 | 
						|
      Tracker.enabled.mockReturnValue(false);
 | 
						|
      const result = InternalEvents.trackInternalLoadEvents();
 | 
						|
      expect(result).toEqual([]);
 | 
						|
    });
 | 
						|
 | 
						|
    describe('tracking', () => {
 | 
						|
      let trackEventSpy;
 | 
						|
      beforeEach(() => {
 | 
						|
        trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent');
 | 
						|
      });
 | 
						|
 | 
						|
      it('should track event if action exists', () => {
 | 
						|
        mockElements = [{ dataset: { eventTracking: action, eventTrackingLoad: true } }];
 | 
						|
        querySelectorAllMock.mockReturnValue(mockElements);
 | 
						|
 | 
						|
        const result = InternalEvents.trackInternalLoadEvents();
 | 
						|
        expect(trackEventSpy).toHaveBeenCalledWith(action, {});
 | 
						|
        expect(trackEventSpy).toHaveBeenCalledTimes(1);
 | 
						|
        expect(querySelectorAllMock).toHaveBeenCalledWith(LOAD_INTERNAL_EVENTS_SELECTOR);
 | 
						|
        expect(result).toEqual(mockElements);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should track event along with additional Properties if action exists', () => {
 | 
						|
        mockElements = [
 | 
						|
          {
 | 
						|
            dataset: {
 | 
						|
              eventTracking: action,
 | 
						|
              eventTrackingLoad: true,
 | 
						|
              eventProperty: 'test-property',
 | 
						|
              eventLabel: 'test-label',
 | 
						|
              eventValue: 2,
 | 
						|
            },
 | 
						|
          },
 | 
						|
        ];
 | 
						|
        querySelectorAllMock.mockReturnValue(mockElements);
 | 
						|
 | 
						|
        const result = InternalEvents.trackInternalLoadEvents();
 | 
						|
        expect(trackEventSpy).toHaveBeenCalledWith(action, {
 | 
						|
          label: 'test-label',
 | 
						|
          property: 'test-property',
 | 
						|
          value: 2,
 | 
						|
        });
 | 
						|
        expect(trackEventSpy).toHaveBeenCalledTimes(1);
 | 
						|
        expect(querySelectorAllMock).toHaveBeenCalledWith(LOAD_INTERNAL_EVENTS_SELECTOR);
 | 
						|
        expect(result).toEqual(mockElements);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should not track event if action is not present', () => {
 | 
						|
        mockElements = [{ dataset: { eventTracking: undefined, eventTrackingLoad: true } }];
 | 
						|
        querySelectorAllMock.mockReturnValue(mockElements);
 | 
						|
 | 
						|
        InternalEvents.trackInternalLoadEvents();
 | 
						|
        expect(trackEventSpy).toHaveBeenCalledTimes(0);
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('initBrowserSDK', () => {
 | 
						|
    beforeEach(() => {
 | 
						|
      window.glClient = {
 | 
						|
        setDocumentTitle: jest.fn(),
 | 
						|
        page: jest.fn(),
 | 
						|
      };
 | 
						|
      window.gl = {
 | 
						|
        environment: 'testing',
 | 
						|
        key: 'value',
 | 
						|
      };
 | 
						|
    });
 | 
						|
 | 
						|
    it('should not call setDocumentTitle or page methods when window.glClient is undefined', () => {
 | 
						|
      window.glClient = undefined;
 | 
						|
 | 
						|
      InternalEvents.initBrowserSDK();
 | 
						|
 | 
						|
      expect(window.glClient?.setDocumentTitle).toBeUndefined();
 | 
						|
      expect(window.glClient?.page).toBeUndefined();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should call setDocumentTitle and page methods on window.glClient when it is defined', () => {
 | 
						|
      InternalEvents.initBrowserSDK();
 | 
						|
 | 
						|
      expect(window.glClient.setDocumentTitle).toHaveBeenCalledWith('GitLab');
 | 
						|
      expect(window.glClient.page).toHaveBeenCalledWith({
 | 
						|
        title: 'GitLab',
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it('should call setDocumentTitle and page methods with default data when window.gl is undefined', () => {
 | 
						|
      window.gl = undefined;
 | 
						|
 | 
						|
      InternalEvents.initBrowserSDK();
 | 
						|
 | 
						|
      expect(window.glClient.setDocumentTitle).toHaveBeenCalledWith('GitLab');
 | 
						|
      expect(window.glClient.page).toHaveBeenCalledWith({
 | 
						|
        title: 'GitLab',
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe('trackBrowserSDK', () => {
 | 
						|
    beforeEach(() => {
 | 
						|
      window.glClient = { track: jest.fn() };
 | 
						|
      Tracker.enabled = jest.fn();
 | 
						|
    });
 | 
						|
 | 
						|
    afterEach(() => {
 | 
						|
      window.glClient = null;
 | 
						|
      window.gl = null;
 | 
						|
    });
 | 
						|
 | 
						|
    const mockSnowplowContext = (projectId, namespaceId) => {
 | 
						|
      window.gl = {
 | 
						|
        snowplowStandardContext: {
 | 
						|
          data: { project_id: projectId, namespace_id: namespaceId },
 | 
						|
        },
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    it('should not call glClient.track if Tracker is not enabled', () => {
 | 
						|
      Tracker.enabled.mockReturnValue(false);
 | 
						|
 | 
						|
      InternalEvents.trackBrowserSDK(event);
 | 
						|
 | 
						|
      expect(window.glClient.track).not.toHaveBeenCalled();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should call glClient.track with event name if Tracker is enabled and no project_id and namespace_id present', () => {
 | 
						|
      mockSnowplowContext(null, null);
 | 
						|
      Tracker.enabled.mockReturnValue(true);
 | 
						|
 | 
						|
      InternalEvents.trackBrowserSDK(event);
 | 
						|
 | 
						|
      expect(window.glClient.track).toHaveBeenCalledTimes(1);
 | 
						|
      expect(window.glClient.track).toHaveBeenCalledWith(event, {
 | 
						|
        project_id: null,
 | 
						|
        namespace_id: null,
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it('should call glClient.track with event name and additional properties if Tracker is enabled', () => {
 | 
						|
      mockSnowplowContext(123, 456);
 | 
						|
      Tracker.enabled.mockReturnValue(true);
 | 
						|
 | 
						|
      InternalEvents.trackBrowserSDK(event, allowedAdditionalProps);
 | 
						|
 | 
						|
      expect(window.glClient.track).toHaveBeenCalledTimes(1);
 | 
						|
      expect(window.glClient.track).toHaveBeenCalledWith(event, {
 | 
						|
        project_id: 123,
 | 
						|
        namespace_id: 456,
 | 
						|
        ...allowedAdditionalProps,
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |