text: // Copyright (c) 2017 Uber Technologies, Inc.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  // http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  import React from 'react';
  import { shallow, mount } from 'enzyme';
  
  import ListView from './ListView';
  import SpanBarRow from './SpanBarRow';
  import DetailState from './SpanDetail/DetailState';
  import SpanDetailRow from './SpanDetailRow';
  import VirtualizedTraceView, { DEFAULT_HEIGHTS } from './VirtualizedTraceView';
  import traceGenerator from '../demo/trace-generators';
  import transformTraceData from '../model/transform-trace-data';
  import SpanTreeOffset from './SpanTreeOffset';
  
  jest.mock('./SpanTreeOffset');
  
  describe('<VirtualizedTraceViewImpl>', () => {
    let wrapper;
    let instance;
  
    const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 }));
    const props = {
      childrenHiddenIDs: new Set(),
      childrenToggle: jest.fn(),
      clearShouldScrollToFirstUiFindMatch: jest.fn(),
      currentViewRangeTime: [0.25, 0.75],
      detailLogItemToggle: jest.fn(),
      detailLogsToggle: jest.fn(),
      detailProcessToggle: jest.fn(),
      detailStates: new Map(),
      detailTagsToggle: jest.fn(),
      detailToggle: jest.fn(),
      findMatchesIDs: null,
      registerAccessors: jest.fn(),
      scrollToFirstVisibleSpan: jest.fn(),
      setSpanNameColumnWidth: jest.fn(),
      setTrace: jest.fn(),
      shouldScrollToFirstUiFindMatch: false,
      spanNameColumnWidth: 0.5,
      trace,
      uiFind: 'uiFind',
    };
  
    function expandRow(rowIndex) {
      const detailStates = new Map();
      const detailState = new DetailState();
      detailStates.set(trace.spans[rowIndex].spanID, detailState);
      wrapper.setProps({ detailStates });
      return detailState;
    }
  
    function addSpansAndCollapseTheirParent(newSpanID = 'some-id') {
      const childrenHiddenIDs = new Set([newSpanID]);
      const spans = [
        trace.spans[0],
        // this span is condidered to have collapsed children
        { spanID: newSpanID, depth: 1, traceID: trace.traceID },
        // these two "spans" are children and should be hidden
        { depth: 2 },
        { depth: 3 },
        ...trace.spans.slice(1),
      ];
      const _trace = { ...trace, spans };
      wrapper.setProps({ childrenHiddenIDs, trace: _trace });
      return spans;
    }
  
    function updateSpan(srcTrace, spanIndex, update) {
      const span = { ...srcTrace.spans[spanIndex], ...update };
      const spans = [...srcTrace.spans.slice(0, spanIndex), span, ...srcTrace.spans.slice(spanIndex + 1)];
      return { ...srcTrace, spans };
    }
  
    beforeEach(() => {
      SpanTreeOffset.mockReturnValue(<div />);
      Object.keys(props).forEach((key) => {
        if (typeof props[key] === 'function') {
          props[key].mockReset();
        }
      });
      wrapper = shallow(<VirtualizedTraceView {...props} />)
        .dive()
        .dive();
      instance = wrapper.instance();
    });
  
    it('renders without exploding', () => {
      expect(wrapper).toBeDefined();
    });
  
    it('renders when a trace is not set', () => {
      wrapper.setProps({ trace: null });
      expect(wrapper).toBeDefined();
    });
  
    it('renders a ListView', () => {
      expect(wrapper.find(ListView)).toBeDefined();
    });
  
    it('sets the trace for global state.traceTimeline', () => {
      expect(props.setTrace.mock.calls).toEqual([[trace, props.uiFind]]);
      props.setTrace.mockReset();
      const traceID = 'some-other-id';
      const _trace = { ...trace, traceID };
      wrapper.setProps({ trace: _trace });
      expect(props.setTrace.mock.calls).toEqual([[_trace, props.uiFind]]);
    });
  
    describe('props.registerAccessors', () => {
      let lv;
      let expectedArg;
  
      beforeEach(() => {
        const getBottomRowIndexVisible = () => {};
        const getTopRowIndexVisible = () => {};
        lv = {
          getViewHeight: () => {},
          getBottomVisibleIndex: getBottomRowIndexVisible,
          getTopVisibleIndex: getTopRowIndexVisible,
          getRowPosition: () => {},
        };
        expectedArg = {
          getBottomRowIndexVisible,
          getTopRowIndexVisible,
          getViewHeight: lv.getViewHeight,
          getRowPosition: lv.getRowPosition,
          getViewRange: instance.getViewRange,
          getSearchedSpanIDs: instance.getSearchedSpanIDs,
          getCollapsedChildren: instance.getCollapsedChildren,
          mapRowIndexToSpanIndex: instance.mapRowIndexToSpanIndex,
          mapSpanIndexToRowIndex: instance.mapSpanIndexToRowIndex,
        };
      });
  
      it('invokes when the listView is set', () => {
        expect(props.registerAccessors.mock.calls.length).toBe(0);
        instance.setListView(lv);
        expect(props.registerAccessors.mock.calls).toEqual([[expectedArg]]);
      });
  
      it('invokes when registerAccessors changes', () => {
        const registerAccessors = jest.fn();
        instance.setListView(lv);
        wrapper.setProps({ registerAccessors });
        expect(registerAccessors.mock.calls).toEqual([[expectedArg]]);
      });
    });
  
    it('returns the current view range via getViewRange()', () => {
      expect(instance.getViewRange()).toBe(props.currentViewRangeTime);
    });
  
    it('returns findMatchesIDs via getSearchedSpanIDs()', () => {
      const findMatchesIDs = new Set();
      wrapper.setProps({ findMatchesIDs });
      expect(instance.getSearchedSpanIDs()).toBe(findMatchesIDs);
    });
  
    it('returns childrenHiddenIDs via getCollapsedChildren()', () => {
      const childrenHiddenIDs = new Set();
      wrapper.setProps({ childrenHiddenIDs });
      expect(instance.getCollapsedChildren()).toBe(childrenHiddenIDs);
    });
  
    describe('mapRowIndexToSpanIndex() maps row index to span index', () => {
      it('works when nothing is collapsed or expanded', () => {
        const i = trace.spans.length - 1;
        expect(instance.mapRowIndexToSpanIndex(i)).toBe(i);
      });
  
      it('works when a span is expanded', () => {
        expandRow(1);
        expect(instance.mapRowIndexToSpanIndex(0)).toBe(0);
        expect(instance.mapRowIndexToSpanIndex(1)).toBe(1);
        expect(instance.mapRowIndexToSpanIndex(2)).toBe(1);
        expect(instance.mapRowIndexToSpanIndex(3)).toBe(2);
      });
  
      it('works when a parent span is collapsed', () => {
        addSpansAndCollapseTheirParent();
        expect(instance.mapRowIndexToSpanIndex(0)).toBe(0);
        expect(instance.mapRowIndexToSpanIndex(1)).toBe(1);
        expect(instance.mapRowIndexToSpanIndex(2)).toBe(4);
        expect(instance.mapRowIndexToSpanIndex(3)).toBe(5);
      });
    });
  
    describe('mapSpanIndexToRowIndex() maps span index to row index', () => {
      it('works when nothing is collapsed or expanded', () => {
        const i = trace.spans.length - 1;
        expect(instance.mapSpanIndexToRowIndex(i)).toBe(i);
      });
  
      it('works when a span is expanded', () => {
        expandRow(1);
        expect(instance.mapSpanIndexToRowIndex(0)).toBe(0);
        expect(instance.mapSpanIndexToRowIndex(1)).toBe(1);
        expect(instance.mapSpanIndexToRowIndex(2)).toBe(3);
        expect(instance.mapSpanIndexToRowIndex(3)).toBe(4);
      });
  
      it('works when a parent span is collapsed', () => {
        addSpansAndCollapseTheirParent();
        expect(instance.mapSpanIndexToRowIndex(0)).toBe(0);
        expect(instance.mapSpanIndexToRowIndex(1)).toBe(1);
        expect(() => instance.mapSpanIndexToRowIndex(2)).toThrow();
        expect(() => instance.mapSpanIndexToRowIndex(3)).toThrow();
        expect(instance.mapSpanIndexToRowIndex(4)).toBe(2);
      });
    });
  
    describe('getKeyFromIndex() generates a "key" from a row index', () => {
      function verify(input, output) {
        expect(instance.getKeyFromIndex(input)).toBe(output);
      }
  
      it('works when nothing is expanded or collapsed', () => {
        verify(0, `${trace.spans[0].traceID}--${trace.spans[0].spanID}--bar`);
      });
  
      it('works when rows are expanded', () => {
        expandRow(1);
        verify(1, `${trace.spans[1].traceID}--${trace.spans[1].spanID}--bar`);
        verify(2, `${trace.spans[1].traceID}--${trace.spans[1].spanID}--detail`);
        verify(3, `${trace.spans[2].traceID}--${trace.spans[2].spanID}--bar`);
      });
  
      it('works when a parent span is collapsed', () => {
        const spans = addSpansAndCollapseTheirParent();
        verify(1, `${spans[1].traceID}--${spans[1].spanID}--bar`);
        verify(2, `${spans[4].traceID}--${spans[4].spanID}--bar`);
      });
    });
  
    describe('getIndexFromKey() converts a "key" to the corresponding row index', () => {
      function verify(input, output) {
        expect(instance.getIndexFromKey(input)).toBe(output);
      }
  
      it('works when nothing is expanded or collapsed', () => {
        verify(`${trace.traceID}--${trace.spans[0].spanID}--bar`, 0);
      });
  
      it('works when rows are expanded', () => {
        expandRow(1);
        verify(`${trace.spans[1].traceID}--${trace.spans[1].spanID}--bar`, 1);
        verify(`${trace.spans[1].traceID}--${trace.spans[1].spanID}--detail`, 2);
        verify(`${trace.spans[2].traceID}--${trace.spans[2].spanID}--bar`, 3);
      });
  
      it('works when a parent span is collapsed', () => {
        const spans = addSpansAndCollapseTheirParent();
        verify(`${spans[1].traceID}--${spans[1].spanID}--bar`, 1);
        verify(`${spans[4].traceID}--${spans[4].spanID}--bar`, 2);
      });
    });
  
    describe('getRowHeight()', () => {
      it('returns the expected height for non-detail rows', () => {
        expect(instance.getRowHeight(0)).toBe(DEFAULT_HEIGHTS.bar);
      });
  
      it('returns the expected height for detail rows that do not have logs', () => {
        expandRow(0);
        expect(instance.getRowHeight(1)).toBe(DEFAULT_HEIGHTS.detail);
      });
  
      it('returns the expected height for detail rows that do have logs', () => {
        const logs = [
          {
            timestamp: Date.now(),
            fields: traceGenerator.tags(),
          },
        ];
        const altTrace = updateSpan(trace, 0, { logs });
        expandRow(0);
        wrapper.setProps({ trace: altTrace });
        expect(instance.getRowHeight(1)).toBe(DEFAULT_HEIGHTS.detailWithLogs);
      });
    });
  
    describe('renderRow()', () => {
      it('renders a SpanBarRow when it is not a detail', () => {
        const span = trace.spans[1];
        const row = instance.renderRow('some-key', {}, 1, {});
        const rowWrapper = shallow(row);
  
        expect(
          rowWrapper.containsMatchingElement(
            <SpanBarRow
              clippingLeft={instance.getClipping().left}
              clippingRight={instance.getClipping().right}
              columnDivision={props.spanNameColumnWidth}
              isChildrenExpanded
              isDetailExpanded={false}
              isMatchingFilter={false}
              numTicks={5}
              onDetailToggled={props.detailToggle}
              onChildrenToggled={props.childrenToggle}
              rpc={undefined}
              showErrorIcon={false}
              span={span}
            />
          )
        ).toBe(true);
      });
  
      it('renders a SpanBarRow with a RPC span if the row is collapsed and a client span', () => {
        const clientTags = [{ key: 'span.kind', value: 'client' }, ...trace.spans[0].tags];
        const serverTags = [{ key: 'span.kind', value: 'server' }, ...trace.spans[1].tags];
        let altTrace = updateSpan(trace, 0, { tags: clientTags });
        altTrace = updateSpan(altTrace, 1, { tags: serverTags });
        const childrenHiddenIDs = new Set([altTrace.spans[0].spanID]);
        wrapper.setProps({ childrenHiddenIDs, trace: altTrace });
  
        const rowWrapper = mount(instance.renderRow('some-key', {}, 0, {}));
        const spanBarRow = rowWrapper.find(SpanBarRow);
        expect(spanBarRow.length).toBe(1);
        expect(spanBarRow.prop('rpc')).toBeDefined();
      });
  
      it('renders a SpanDetailRow when it is a detail', () => {
        const detailState = expandRow(1);
        const span = trace.spans[1];
        const row = instance.renderRow('some-key', {}, 2, {});
        const rowWrapper = shallow(row);
        expect(
          rowWrapper.containsMatchingElement(
            <SpanDetailRow
              columnDivision={props.spanNameColumnWidth}
              onDetailToggled={props.detailToggle}
              detailState={detailState}
              logItemToggle={props.detailLogItemToggle}
              logsToggle={props.detailLogsToggle}
              processToggle={props.detailProcessToggle}
              span={span}
              tagsToggle={props.detailTagsToggle}
            />
          )
        ).toBe(true);
      });
  
      it('renders a SpanBarRow with a client span and no instrumented server span', () => {
        const externServiceName = 'externalServiceTest';
        const leafSpan = trace.spans.find((span) => !span.hasChildren);
        const leafSpanIndex = trace.spans.indexOf(leafSpan);
        const clientTags = [
          { key: 'span.kind', value: 'client' },
          { key: 'peer.service', value: externServiceName },
          ...leafSpan.tags,
        ];
        const altTrace = updateSpan(trace, leafSpanIndex, { tags: clientTags });
        wrapper.setProps({ trace: altTrace });
        const rowWrapper = mount(instance.renderRow('some-key', {}, leafSpanIndex, {}));
        const spanBarRow = rowWrapper.find(SpanBarRow);
        expect(spanBarRow.length).toBe(1);
        expect(spanBarRow.prop('noInstrumentedServer')).not.toBeNull();
      });
    });
  
    describe('shouldScrollToFirstUiFindMatch', () => {
      const propsWithTrueShouldScrollToFirstUiFindMatch = { ...props, shouldScrollToFirstUiFindMatch: true };
  
      beforeEach(() => {
        props.scrollToFirstVisibleSpan.mockReset();
        props.clearShouldScrollToFirstUiFindMatch.mockReset();
      });
  
      it('calls props.scrollToFirstVisibleSpan if shouldScrollToFirstUiFindMatch is true', () => {
        expect(props.scrollToFirstVisibleSpan).not.toHaveBeenCalled();
        expect(props.clearShouldScrollToFirstUiFindMatch).not.toHaveBeenCalled();
  
        wrapper.setProps(propsWithTrueShouldScrollToFirstUiFindMatch);
        expect(props.scrollToFirstVisibleSpan).toHaveBeenCalledTimes(1);
        expect(props.clearShouldScrollToFirstUiFindMatch).toHaveBeenCalledTimes(1);
      });
  
      describe('shouldComponentUpdate', () => {
        it('returns true if props.shouldScrollToFirstUiFindMatch changes to true', () => {
          expect(wrapper.instance().shouldComponentUpdate(propsWithTrueShouldScrollToFirstUiFindMatch)).toBe(true);
        });
  
        it('returns true if props.shouldScrollToFirstUiFindMatch changes to false and another props change', () => {
          const propsWithOtherDifferenceAndTrueshouldScrollToFirstUiFindMatch = {
            ...propsWithTrueShouldScrollToFirstUiFindMatch,
            clearShouldScrollToFirstUiFindMatch: () => {},
          };
          wrapper.setProps(propsWithOtherDifferenceAndTrueshouldScrollToFirstUiFindMatch);
          expect(wrapper.instance().shouldComponentUpdate(props)).toBe(true);
        });
  
        it('returns false if props.shouldScrollToFirstUiFindMatch changes to false and no other props change', () => {
          wrapper.setProps(propsWithTrueShouldScrollToFirstUiFindMatch);
          expect(wrapper.instance().shouldComponentUpdate(props)).toBe(false);
        });
  
        it('returns false if all props are unchanged', () => {
          expect(wrapper.instance().shouldComponentUpdate(props)).toBe(false);
        });
      });
    });
  });
  