Home Reference Source Repository

src/tools/__tests__/RelayProfiler-test.js

/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @emails oncall+relay
 */

'use strict';

jest.unmock('RelayProfiler');

const RelayProfiler = require('RelayProfiler');

describe('RelayProfiler', function() {
  const DEV = __DEV__;

  let mockMethod;
  let mockObject;
  const mockDisableDEV = () => {
    window.__DEV__ = 0;
  };

  beforeEach(() => {
    jest.resetModuleRegistry();

    mockMethod = jest.fn();
    const mockMethod2 = jest.fn();
    mockObject = {
      mockMethod: RelayProfiler.instrument('mock', mockMethod),
      mockMethod2: RelayProfiler.instrument('mock2', mockMethod2),
    };
  });

  afterEach(() => {
    window.__DEV__ = DEV;
  });

  describe('instance', () => {
    it('preserves context, arguments, and return value', () => {
      const expectedArgument = {};
      const expectedContext = mockObject;
      const expectedReturnValue = {};

      mockMethod.mockImplementation(function(actualArgument) {
        expect(actualArgument).toBe(expectedArgument);
        expect(this).toBe(expectedContext);
        return expectedReturnValue;
      });

      const actualReturnValue = mockObject.mockMethod(expectedArgument);

      expect(actualReturnValue).toBe(expectedReturnValue);
    });

    it('invokes attached handlers', () => {
      const actualOrdering = [];

      mockMethod.mockImplementation(() => {
        actualOrdering.push('mockMethod');
      });

      mockObject.mockMethod.attachHandler((name, callback) => {
        expect(name).toBe('mock');
        actualOrdering.push('beforeCallback');
        callback();
        actualOrdering.push('afterCallback');
      });

      mockObject.mockMethod();

      expect(actualOrdering).toEqual([
        'beforeCallback',
        'mockMethod',
        'afterCallback',
      ]);
    });

    it('invokes nested attached handlers', () => {
      const actualOrdering = [];

      mockMethod.mockImplementation(() => {
        actualOrdering.push('0: mockMethod');
      });

      mockObject.mockMethod.attachHandler((name, callback) => {
        expect(name).toBe('mock');
        actualOrdering.push('1: beforeCallback');
        callback();
        actualOrdering.push('1: afterCallback');
      });

      mockObject.mockMethod.attachHandler((name, callback) => {
        expect(name).toBe('mock');
        actualOrdering.push('2: beforeCallback');
        callback();
        actualOrdering.push('2: afterCallback');
      });

      mockObject.mockMethod();

      expect(actualOrdering).toEqual([
        '2: beforeCallback',
        '1: beforeCallback',
        '0: mockMethod',
        '1: afterCallback',
        '2: afterCallback',
      ]);
    });

    it('does not invoke detached handlers', () => {
      const mockHandler = jest.fn()
        .mockImplementation((name, callback) => {
          callback();
        });

      mockObject.mockMethod.attachHandler(mockHandler);
      mockObject.mockMethod.detachHandler(mockHandler);
      mockObject.mockMethod();

      expect(mockHandler).not.toBeCalled();
    });

    it('throws if callback is not invoked by handler', () => {
      mockObject.mockMethod.attachHandler(jest.fn());

      expect(() => {
        mockObject.mockMethod();
      }).toThrowError(
        'RelayProfiler: Handler did not invoke original function.'
      );
    });

    it('ignores names starting with "@" unless __DEV__', () => {
      mockDisableDEV();

      mockMethod = jest.fn();
      mockObject = {mockMethod: RelayProfiler.instrument('@mock', mockMethod)};

      expect(mockObject.mockMethod).toBe(mockMethod);
      expect(() => {
        mockObject.mockMethod.attachHandler();
        mockObject.mockMethod.detachHandler();
      }).not.toThrow();
    });

    it('instruments names without "@" when not in __DEV__', () => {
      mockDisableDEV();

      mockMethod = jest.fn();
      mockObject = {mockMethod: RelayProfiler.instrument('mock', mockMethod)};

      expect(mockObject.mockMethod).not.toBe(mockMethod);
    });
  });

  describe('aggregate', () => {
    it('invokes aggregate handlers first', () => {
      const actualOrdering = [];

      mockMethod.mockImplementation(() => {
        actualOrdering.push('0: mockMethod');
      });

      mockObject.mockMethod.attachHandler((name, callback) => {
        actualOrdering.push('1: beforeCallback');
        callback();
        actualOrdering.push('1: afterCallback');
      });

      RelayProfiler.attachAggregateHandler('mock', (name, callback) => {
        expect(name).toBe('mock');
        actualOrdering.push('3: beforeCallback (aggregate)');
        callback();
        actualOrdering.push('3: afterCallback (aggregate)');
      });

      RelayProfiler.attachAggregateHandler('*', (name, callback) => {
        actualOrdering.push('5: beforeCallback (aggregate *): ' + name);
        callback();
        actualOrdering.push('5: afterCallback (aggregate *): ' + name);
      });

      RelayProfiler.attachAggregateHandler('mock', (name, callback) => {
        expect(name).toBe('mock');
        actualOrdering.push('4: beforeCallback (aggregate)');
        callback();
        actualOrdering.push('4: afterCallback (aggregate)');
      });

      mockObject.mockMethod.attachHandler((name, callback) => {
        actualOrdering.push('2: beforeCallback');
        callback();
        actualOrdering.push('2: afterCallback');
      });

      mockObject.mockMethod();
      mockObject.mockMethod2();

      expect(actualOrdering).toEqual([
        '5: beforeCallback (aggregate *): mock',
        '4: beforeCallback (aggregate)',
        '3: beforeCallback (aggregate)',
        '2: beforeCallback',
        '1: beforeCallback',
        '0: mockMethod',
        '1: afterCallback',
        '2: afterCallback',
        '3: afterCallback (aggregate)',
        '4: afterCallback (aggregate)',
        '5: afterCallback (aggregate *): mock',
        '5: beforeCallback (aggregate *): mock2',
        '5: afterCallback (aggregate *): mock2',
      ]);
    });

    it('aggregates methods instrumented after being attached', () => {
      const mockHandler = jest.fn()
        .mockImplementation((name, callback) => {
          callback();
        });
      RelayProfiler.attachAggregateHandler('mockFuture', mockHandler);

      const mockFutureMethod = RelayProfiler.instrument('mockFuture', mockMethod);

      expect(mockHandler).not.toBeCalled();
      mockFutureMethod();
      expect(mockHandler).toBeCalled();
    });

    it('detaches aggregate handlers', () => {
      const mockHandler = jest.fn()
        .mockImplementation((name, callback) => {
          callback();
        });

      RelayProfiler.attachAggregateHandler('mock', mockHandler);
      RelayProfiler.detachAggregateHandler('mock', mockHandler);
      mockObject.mockMethod();

      expect(mockHandler).not.toBeCalled();
    });
  });

  describe('profile', () => {
    it('invokes attached profile handlers', () => {
      const actualOrdering = [];

      RelayProfiler.attachProfileHandler('mockBehavior', (name) => {
        expect(name).toBe('mockBehavior');
        actualOrdering.push('1: beforeEnd');
        return () => {
          actualOrdering.push('1: afterEnd');
        };
      });

      RelayProfiler.attachProfileHandler('mockBehavior', (name) => {
        expect(name).toBe('mockBehavior');
        actualOrdering.push('2: beforeEnd');
        return () => {
          actualOrdering.push('2: afterEnd');
        };
      });

      const profiler = RelayProfiler.profile('mockBehavior');

      expect(actualOrdering).toEqual([
        '2: beforeEnd',
        '1: beforeEnd',
      ]);

      profiler.stop();

      expect(actualOrdering).toEqual([
        '2: beforeEnd',
        '1: beforeEnd',
        '1: afterEnd',
        '2: afterEnd',
      ]);
    });

    it('does not invoke detached profile handlers', () => {
      const mockStop = jest.fn();
      const mockStart = jest.fn(() => mockStop);

      RelayProfiler.attachProfileHandler('mockBehavior', mockStart);
      RelayProfiler.detachProfileHandler('mockBehavior', mockStart);
      RelayProfiler.profile('mockBehavior');

      expect(mockStop).not.toBeCalled();
      expect(mockStart).not.toBeCalled();
    });

    it('passes state to each profile handler', () => {
      const mockStop = jest.fn();
      const mockStart = jest.fn(() => mockStop);
      const state = {};

      RelayProfiler.attachProfileHandler('mockBehavior', mockStart);
      const profiler = RelayProfiler.profile('mockBehavior', state);
      profiler.stop();

      expect(mockStart).toBeCalledWith('mockBehavior', state);
      expect(mockStop).toBeCalled();
      expect(mockStop.mock.calls[0].length).toBe(0);
    });
  });
});