diff --git a/src/components/TracePage/TimelineScrubber.js b/src/components/TracePage/TimelineScrubber.js index e9fa69917c..fe4b951899 100644 --- a/src/components/TracePage/TimelineScrubber.js +++ b/src/components/TracePage/TimelineScrubber.js @@ -21,7 +21,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import tracePropTypes from '../../propTypes/trace'; import { getTraceTimestamp, getTraceDuration } from '../../selectors/trace'; import { getPercentageOfInterval } from '../../utils/date'; @@ -79,7 +78,7 @@ export default function TimelineScrubber({ TimelineScrubber.propTypes = { onMouseDown: PropTypes.func, - trace: tracePropTypes.isRequired, + trace: PropTypes.object, timestamp: PropTypes.number.isRequired, handleTopOffset: PropTypes.number, handleWidth: PropTypes.number, diff --git a/src/components/TracePage/TimelineScrubber.test.js b/src/components/TracePage/TimelineScrubber.test.js index 34dc6c93a4..fef202c9cb 100644 --- a/src/components/TracePage/TimelineScrubber.test.js +++ b/src/components/TracePage/TimelineScrubber.test.js @@ -27,53 +27,51 @@ import traceGenerator from '../../../src/demo/trace-generators'; import { getTraceTimestamp, getTraceDuration } from '../../../src/selectors/trace'; -const generatedTrace = traceGenerator.trace({ numberOfSpans: 45 }); +describe('', () => { + const generatedTrace = traceGenerator.trace({ numberOfSpans: 45 }); + const defaultProps = { + onMouseDown: sinon.spy(), + trace: generatedTrace, + timestamp: getTraceTimestamp(generatedTrace), + }; -const defaultProps = { - onMouseDown: sinon.spy(), - trace: generatedTrace, - timestamp: getTraceTimestamp(generatedTrace), -}; + let wrapper; -it(' should contain the proper svg components', () => { - const wrapper = shallow(); + beforeEach(() => { + wrapper = shallow(); + }); - expect( - wrapper.matchesElement( - - - - - - - - ) - ).toBeTruthy(); -}); - -it(' should calculate the correct x% for a timestamp', () => { - const timestamp = getTraceDuration(generatedTrace) * 0.5 + getTraceTimestamp(generatedTrace); - - const wrapper = shallow(); - const line = wrapper.find('line').first(); - const rect = wrapper.find('rect').first(); - - expect(line.prop('x1')).toBe('50%'); - expect(line.prop('x2')).toBe('50%'); - expect(rect.prop('x')).toBe('50%'); -}); - -it(' should support onMouseDown', () => { - const wrapper = shallow(); - const event = {}; + it('contains the proper svg components', () => { + expect( + wrapper.matchesElement( + + + + + + + + ) + ).toBeTruthy(); + }); - wrapper.find('g').prop('onMouseDown')(event); - - expect(defaultProps.onMouseDown.calledWith(event)).toBeTruthy(); -}); + it('calculates the correct x% for a timestamp', () => { + const timestamp = getTraceDuration(generatedTrace) * 0.5 + getTraceTimestamp(generatedTrace); + wrapper = shallow(); + const line = wrapper.find('line').first(); + const rect = wrapper.find('rect').first(); + expect(line.prop('x1')).toBe('50%'); + expect(line.prop('x2')).toBe('50%'); + expect(rect.prop('x')).toBe('50%'); + }); -it(' should not fail if onMouseDown is not provided', () => { - const wrapper = shallow(); + it('supports onMouseDown', () => { + const event = {}; + wrapper.find('g').prop('onMouseDown')(event); + expect(defaultProps.onMouseDown.calledWith(event)).toBeTruthy(); + }); - expect(() => wrapper.find('g').prop('onMouseDown')()).not.toThrow(); + it("doesn't fail if onMouseDown is not provided", () => { + expect(() => wrapper.find('g').prop('onMouseDown')()).not.toThrow(); + }); }); diff --git a/src/components/TracePage/TracePageHeader.test.js b/src/components/TracePage/TracePageHeader.test.js index d0905c39a6..71e5124e0e 100644 --- a/src/components/TracePage/TracePageHeader.test.js +++ b/src/components/TracePage/TracePageHeader.test.js @@ -20,65 +20,69 @@ import React from 'react'; import sinon from 'sinon'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import TracePageHeader, { HEADER_ITEMS } from './TracePageHeader'; -import traceGenerator from '../../demo/trace-generators'; -import { getTraceName } from '../../selectors/trace'; -const defaultProps = { - trace: traceGenerator.trace({ numberOfSpans: 50 }), -}; - -const defaultOptions = { - context: { - textFilter: '', - updateTextFilter: () => {}, - }, -}; - -it(' should render a
', () => { - const wrapper = shallow(, defaultOptions); - - expect(wrapper.find('header').length).toBe(1); -}); - -it(' should render an empty
if no trace present', () => { - const wrapper = shallow(, defaultOptions); - - expect(wrapper.matchesElement(
)).toBeTruthy(); -}); - -it(' should render the trace title', () => { - const wrapper = shallow(, defaultOptions); - const h2 = wrapper.find('h2').first(); - - expect(h2.contains(getTraceName(defaultProps.trace))).toBeTruthy(); -}); +describe('', () => { + const defaultProps = { + traceID: 'some-trace-id', + name: 'some-trace-name', + + // maxDepth: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + // numServices: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + // numSpans: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + // durationMs: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + // timestampMs: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + // slimView: PropTypes.bool, + // onSlimViewClicked: PropTypes.func, + }; + + const defaultOptions = { + context: { + textFilter: '', + updateTextFilter: () => {}, + }, + }; -it(' should render the header items', () => { - const wrapper = shallow(, defaultOptions); + let wrapper; - wrapper.find('.horizontal .item').forEach((item, idx) => { - expect(item.contains(HEADER_ITEMS[idx].title)).toBeTruthy(); - expect(item.contains(HEADER_ITEMS[idx].renderer(defaultProps.trace))).toBeTruthy(); + beforeEach(() => { + wrapper = shallow(, defaultOptions); }); -}); -it(' should call the context filter method onChange of the input', () => { - const updateTextFilter = sinon.spy(); + it('renders a
', () => { + expect(wrapper.find('header').length).toBe(1); + }); - const wrapper = shallow(, { - ...defaultOptions, - context: { - ...defaultOptions.context, - updateTextFilter, - }, + it('renders an empty
if no traceID is present', () => { + wrapper = mount(, defaultOptions); + expect(wrapper.children().length).toBe(0); }); - const event = { target: { value: 'my new value' } }; + it('renders the trace title', () => { + const h2 = wrapper.find('h2').first(); + expect(h2.contains(defaultProps.name)).toBeTruthy(); + }); - wrapper.find('#trace-page__text-filter').first().prop('onChange')(event); + it('renders the header items', () => { + wrapper.find('.horizontal .item').forEach((item, i) => { + expect(item.contains(HEADER_ITEMS[i].title)).toBeTruthy(); + expect(item.contains(HEADER_ITEMS[i].renderer(defaultProps.trace))).toBeTruthy(); + }); + }); - expect(updateTextFilter.calledWith('my new value')).toBeTruthy(); + it('calls the context updateTextFilter() function for onChange of the input', () => { + const updateTextFilter = sinon.spy(); + wrapper = shallow(, { + ...defaultOptions, + context: { + ...defaultOptions.context, + updateTextFilter, + }, + }); + const event = { target: { value: 'my new value' } }; + wrapper.find('#trace-page__text-filter').first().prop('onChange')(event); + expect(updateTextFilter.calledWith('my new value')).toBeTruthy(); + }); }); diff --git a/src/components/TracePage/TraceSpanGraph.js b/src/components/TracePage/TraceSpanGraph.js index 5768a1b4ac..e517bed876 100644 --- a/src/components/TracePage/TraceSpanGraph.js +++ b/src/components/TracePage/TraceSpanGraph.js @@ -24,20 +24,18 @@ import { window } from 'global'; import SpanGraph from './SpanGraph'; import SpanGraphTickHeader from './SpanGraph/SpanGraphTickHeader'; -import tracePropTypes from '../../propTypes/trace'; import TimelineScrubber from './TimelineScrubber'; import { getTraceId, getTraceTimestamp, getTraceEndTimestamp, getTraceDuration } from '../../selectors/trace'; import { getPercentageOfInterval } from '../../utils/date'; const TIMELINE_TICK_INTERVAL = 4; -const TIMELINE_TICK_WIDTH = 2; export default class TraceSpanGraph extends Component { static get propTypes() { return { xformedTrace: PropTypes.object, - trace: tracePropTypes, + trace: PropTypes.object, height: PropTypes.number.isRequired, }; } diff --git a/src/components/TracePage/TraceSpanGraph.test.js b/src/components/TracePage/TraceSpanGraph.test.js index a7ac73d1b8..3e64cbd325 100644 --- a/src/components/TracePage/TraceSpanGraph.test.js +++ b/src/components/TracePage/TraceSpanGraph.test.js @@ -27,396 +27,224 @@ import TraceSpanGraph from './TraceSpanGraph'; import SpanGraphTickHeader from './SpanGraph/SpanGraphTickHeader'; import TimelineScrubber from './TimelineScrubber'; import traceGenerator from '../../../src/demo/trace-generators'; +import { transformTrace } from './TraceTimelineViewer/transforms'; +import { hydrateSpansWithProcesses } from '../../selectors/trace'; -const listeners = {}; -const addEventListener = sinon.spy((name, fn) => Object.assign(listeners, { [name]: fn })); -const removeEventListener = sinon.spy((name, fn) => Object.assign(listeners, { [name]: fn })); -const clearListeners = () => - Object.keys(listeners).forEach(key => { - delete listeners[key]; - }); - -const timestamp = new Date().getTime() * 1000; -const defaultProps = { - trace: { - traceID: 'trace-id', - spans: [ - { - spanID: 'spanID-2', - traceID: 'trace-id', - startTime: timestamp + 10000, - duration: 10000, - operationName: 'whatever', - process: { - serviceName: 'my-other-service', - }, - }, - { - spanID: 'spanID-3', - traceID: 'trace-id', - startTime: timestamp + 20000, - duration: 10000, - operationName: 'bob', - process: { - serviceName: 'my-service', - }, - }, - { - spanID: 'spanID-1', - traceID: 'trace-id', - startTime: timestamp, - duration: 50000, - operationName: 'whatever', - process: { - serviceName: 'my-service', - }, - }, - ], - }, -}; - -const defaultOptions = { - context: { - timeRangeFilter: [timestamp, timestamp + 50000], - updateTimeRangeFilter: () => {}, - }, -}; - -it(' should render a ', () => { - const wrapper = shallow(, defaultOptions); +describe('', () => { + const trace = hydrateSpansWithProcesses(traceGenerator.trace({})); + const xformedTrace = transformTrace(trace); - expect(wrapper.find(SpanGraph).length).toBe(1); -}); - -it(' should render a ', () => { - const wrapper = shallow(, defaultOptions); - - expect(wrapper.find(SpanGraphTickHeader).length).toBe(1); -}); + const props = { + trace, + xformedTrace, + }; -it(' should just return a
if no trace is present', () => { - const wrapper = shallow(, defaultOptions); - - expect(wrapper.matchesElement(
)).toBeTruthy(); -}); - -it(' should render a filtering box if leftBound exists', () => { - const wrapper = shallow(, { - ...defaultOptions, + const options = { context: { - ...defaultOptions.context, - timeRangeFilter: [timestamp + 10000, timestamp + 50000], + timeRangeFilter: [trace.timestamp, trace.timestamp + trace.duration], + updateTimeRangeFilter: () => {}, }, - }); + }; - expect( - wrapper.containsMatchingElement( - - ) - ).toBeTruthy(); -}); + let wrapper; -it(' should render a filtering box if rightBound exists', () => { - const wrapper = shallow(, { - ...defaultOptions, - context: { - ...defaultOptions.context, - timeRangeFilter: [timestamp, timestamp + 40000], - }, + beforeEach(() => { + wrapper = shallow(, options); }); - expect( - wrapper.containsMatchingElement( - - ) - ).toBeTruthy(); -}); - -it(' should render handles for the timeRangeFilter', () => { - const wrapper = shallow(, defaultOptions); - - expect( - wrapper.containsMatchingElement( - - ) - ).toBeTruthy(); - expect( - wrapper.containsMatchingElement( - - ) - ).toBeTruthy(); -}); - -it(' should call startDragging for the leftBound handle', () => { - const wrapper = shallow(, defaultOptions); - const event = { clientX: 50 }; - - sinon.stub(wrapper.instance(), 'startDragging'); - - wrapper.find('#trace-page-timeline__left-bound-handle').prop('onMouseDown')(event); - - expect(wrapper.instance().startDragging.calledWith('leftBound', event)).toBeTruthy(); -}); - -it(' should call startDragging for the rightBound handle', () => { - const wrapper = shallow(, defaultOptions); - const event = { clientX: 50 }; - - sinon.stub(wrapper.instance(), 'startDragging'); - - wrapper.find('#trace-page-timeline__right-bound-handle').prop('onMouseDown')(event); - - expect(wrapper.instance().startDragging.calledWith('rightBound', event)).toBeTruthy(); -}); - -it(' should render without handles if no filtering', () => { - const wrapper = shallow(, { - ...defaultOptions, - context: { - ...defaultOptions.context, - timeRangeFilter: [], - }, + it('renders a ', () => { + expect(wrapper.find(SpanGraph).length).toBe(1); }); - expect(wrapper.find('rect').length).toBe(0); - expect(wrapper.find(TimelineScrubber).length).toBe(0); -}); - -it(' should timeline-sort the trace before rendering', () => { - // manually sort the spans in the defaultProps. - const sortedTraceSpans = [ - defaultProps.trace.spans[2], - defaultProps.trace.spans[0], - defaultProps.trace.spans[1], - ]; - - const wrapper = shallow(, defaultOptions); - const spanGraph = wrapper.find(SpanGraph).first(); - - expect(spanGraph.prop('spans')).toEqual(sortedTraceSpans); -}); - -it(' should create ticks and pass them to components', () => { - // manually build a ticks object for the trace - const ticks = [ - { timestamp, width: 2 }, - { timestamp: timestamp + 10000, width: 2 }, - { timestamp: timestamp + 20000, width: 2 }, - { timestamp: timestamp + 30000, width: 2 }, - { timestamp: timestamp + 40000, width: 2 }, - { timestamp: timestamp + 50000, width: 2 }, - ]; - - const wrapper = shallow(, defaultOptions); - const spanGraph = wrapper.find(SpanGraph).first(); - const spanGraphTickHeader = wrapper.find(SpanGraphTickHeader).first(); - - expect(spanGraph.prop('ticks')).toEqual(ticks); - expect(spanGraphTickHeader.prop('ticks')).toEqual(ticks); -}); - -it(' should calculate the rowHeight', () => { - const wrapper = shallow(, defaultOptions); - const spanGraph = wrapper.find(SpanGraph).first(); - - expect(spanGraph.prop('rowHeight')).toBe(50 / 3); -}); - -it(' should pass the props through to SpanGraph', () => { - const wrapper = shallow(, defaultOptions); - const spanGraph = wrapper.find(SpanGraph).first(); - - expect(spanGraph.prop('rowPadding')).toBe(0); -}); - -it(' should pass the props through to SpanGraphTickHeader', () => { - const wrapper = shallow(, defaultOptions); - const spanGraphTickHeader = wrapper.find(SpanGraphTickHeader).first(); - - expect(spanGraphTickHeader.prop('trace')).toEqual(defaultProps.trace); -}); - -it('TraceSpanGraph.shouldComponentUpdate should return true for new timeRange', () => { - const wrapper = shallow(, defaultOptions); - - expect( - wrapper.instance().shouldComponentUpdate(defaultProps, wrapper.state(), { - timeRangeFilter: [timestamp, timestamp + 10000], - }) - ).toBe(true); -}); - -it('TraceSpanGraph.shouldComponentUpdate should return true for new traces', () => { - const wrapper = shallow(, defaultOptions); - - expect( - wrapper - .instance() - .shouldComponentUpdate( - { ...defaultProps, trace: traceGenerator.trace({ numberOfSpans: 45 }) }, - wrapper.state(), - defaultOptions.context - ) - ).toBe(true); -}); - -it('TraceSpanGraph.shouldComponentUpdate should return true for currentlyDragging', () => { - const wrapper = shallow(, defaultOptions); - - expect( - wrapper.instance().shouldComponentUpdate( - defaultProps, - { - ...wrapper.state(), - currentlyDragging: !wrapper.state('currentlyDragging'), - }, - defaultOptions.context - ) - ).toBe(true); -}); - -it('TraceSpanGraph.shouldComponentUpdate should return false otherwise', () => { - const wrapper = shallow(, defaultOptions); - - expect( - wrapper.instance().shouldComponentUpdate(defaultProps, wrapper.state(), defaultOptions.context) - ).toBe(false); -}); - -it('TraceSpanGraph.onMouseMove should do nothing if currentlyDragging is false', () => { - const updateTimeRangeFilter = sinon.spy(); - const wrapper = shallow(, { - ...defaultOptions, - context: { - ...defaultOptions.context, - updateTimeRangeFilter, - }, + it('renders a ', () => { + expect(wrapper.find(SpanGraphTickHeader).length).toBe(1); }); - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ currentlyDragging: null }); - - wrapper.instance().onMouseMove({ clientX: 45 }); - - expect(wrapper.state('prevX')).toBe(null); - expect(updateTimeRangeFilter.called).toBeFalsy(); -}); - -it('TraceSpanGraph.onMouseMove should store the clientX on the state', () => { - const wrapper = shallow(, defaultOptions); - - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ currentlyDragging: 'leftBound' }); - - wrapper.instance().onMouseMove({ clientX: 45 }); - - expect(wrapper.state('prevX')).toBe(45); -}); - -it('TraceSpanGraph.onMouseMove should update the timeRangeFilter for the left handle', () => { - const updateTimeRangeFilter = sinon.spy(); - const wrapper = shallow(, { - ...defaultOptions, - context: { - ...defaultOptions.context, - updateTimeRangeFilter, - }, + it('returns a
if a trace is not provided', () => { + wrapper = shallow(, options); + expect(wrapper.matchesElement(
)).toBeTruthy(); }); - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ prevX: 0, currentlyDragging: 'leftBound' }); - - wrapper.instance().onMouseMove({ clientX: 45 }); - - expect(updateTimeRangeFilter.calledWith(timestamp + 22500, timestamp + 50000)).toBeTruthy(); -}); - -it('TraceSpanGraph.onMouseMove should update the timeRangeFilter for the right handle', () => { - const updateTimeRangeFilter = sinon.spy(); - const wrapper = shallow(, { - ...defaultOptions, - context: { - ...defaultOptions.context, - updateTimeRangeFilter, - }, + it('renders a filtering box if leftBound exists', () => { + const context = { + ...options.context, + timeRangeFilter: [trace.timestamp + 0.2 * trace.duration, trace.timestamp + trace.duration], + }; + wrapper = shallow(, { ...options, context }); + const leftBox = wrapper.find('.trace-page-timeline__graph--inactive'); + expect(leftBox.length).toBe(1); + const width = Number(leftBox.prop('width').slice(0, -1)); + const x = leftBox.prop('x'); + expect(Math.round(width)).toBe(20); + expect(x).toBe(0); }); - wrapper.instance().svg = { clientWidth: 100 }; - wrapper.setState({ prevX: 100, currentlyDragging: 'rightBound' }); - - wrapper.instance().onMouseMove({ clientX: 45 }); - - expect(updateTimeRangeFilter.calledWith(timestamp, timestamp + 22500)).toBeTruthy(); -}); - -it('TraceSpanGraph.startDragging should store the boundName and the prevX in state', () => { - const wrapper = shallow(, defaultOptions); - - wrapper.instance().startDragging('leftBound', { clientX: 100 }); - - expect(wrapper.state('currentlyDragging')).toBe('leftBound'); - expect(wrapper.state('prevX')).toBe(100); -}); - -// TODO: Need to figure out how to mock to window events. -it.skip('TraceSpanGraph.startDragging should bind event listeners to the window', () => { - const wrapper = shallow(, defaultOptions); - - clearListeners(); - - wrapper.instance().startDragging('leftBound', { clientX: 100 }); - - expect(addEventListener.calledWith('mousemove', sinon.match.func)).toBeTruthy(); - expect(addEventListener.calledWith('mouseup', sinon.match.func)).toBeTruthy(); -}); - -it.skip('TraceSpanGraph.startDragging should call onMouseMove on the window', () => { - const wrapper = shallow(, defaultOptions); - - clearListeners(); - - wrapper.instance().startDragging('leftBound', { clientX: 100 }); - sinon.stub(wrapper.instance(), 'onMouseMove'); - - const event = { clientX: 99 }; - listeners.mousemove(event); - - expect(wrapper.instance().onMouseMove.calledWith(event)).toBeTruthy(); -}); - -it.skip('TraceSpanGraph.startDragging mouseup should call stopDragging', () => { - const wrapper = shallow(, defaultOptions); - - clearListeners(); - - wrapper.instance().startDragging('leftBound', { clientX: 100 }); - sinon.stub(wrapper.instance(), 'stopDragging'); + it('renders a filtering box if rightBound exists', () => { + const context = { + ...options.context, + timeRangeFilter: [trace.timestamp, trace.timestamp + 0.8 * trace.duration], + }; + wrapper = shallow(, { ...options, context }); + const rightBox = wrapper.find('.trace-page-timeline__graph--inactive'); + const width = Number(rightBox.prop('width').slice(0, -1)); + const x = Number(rightBox.prop('x').slice(0, -1)); + expect(rightBox.length).toBe(1); + expect(Math.round(width)).toBe(20); + expect(Math.round(x)).toBe(80); + }); - const event = { clientX: 99 }; - listeners.mouseup(event); + it('renders handles for the timeRangeFilter', () => { + const timeRangeFilter = options.context.timeRangeFilter; + let scrubber = ; + expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); + scrubber = ; + expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); + }); - expect(wrapper.instance().stopDragging.called).toBeTruthy(); -}); + it('calls startDragging() for the leftBound handle', () => { + const event = { clientX: 50 }; + sinon.stub(wrapper.instance(), 'startDragging'); + wrapper.find('#trace-page-timeline__left-bound-handle').prop('onMouseDown')(event); + expect(wrapper.instance().startDragging.calledWith('leftBound', event)).toBeTruthy(); + }); -it.skip('TraceSpanGraph.startDragging mouseup should stop listening to the events', () => { - const wrapper = shallow(, defaultOptions); + it('calls startDragging for the rightBound handle', () => { + const event = { clientX: 50 }; + sinon.stub(wrapper.instance(), 'startDragging'); + wrapper.find('#trace-page-timeline__right-bound-handle').prop('onMouseDown')(event); + expect(wrapper.instance().startDragging.calledWith('rightBound', event)).toBeTruthy(); + }); - clearListeners(); + it('renders without handles if not filtering', () => { + const context = { ...options.context, timeRangeFilter: [] }; + wrapper = shallow(, { ...options, context }); + expect(wrapper.find('rect').length).toBe(0); + expect(wrapper.find(TimelineScrubber).length).toBe(0); + }); - wrapper.instance().startDragging('leftBound', { clientX: 100 }); + it('passes the number of ticks to rendered to components', () => { + const tickHeader = wrapper.find(SpanGraphTickHeader); + const spanGraph = wrapper.find(SpanGraph); + expect(tickHeader.prop('numTicks')).toBeGreaterThan(1); + expect(spanGraph.prop('numTicks')).toBeGreaterThan(1); + expect(tickHeader.prop('numTicks')).toBe(spanGraph.prop('numTicks')); + }); - const event = { clientX: 99 }; - listeners.mouseup(event); + it('passes items to SpanGraph', () => { + const spanGraph = wrapper.find(SpanGraph).first(); + const items = xformedTrace.spans.map(span => ({ + valueOffset: span.relativeStartTime, + valueWidth: span.duration, + serviceName: span.process.serviceName, + })); + expect(spanGraph.prop('items')).toEqual(items); + }); - expect(removeEventListener.calledWith('mousemove', sinon.match.func)).toBeTruthy(); - expect(removeEventListener.calledWith('mouseup', sinon.match.func)).toBeTruthy(); -}); + describe('# shouldComponentUpdate()', () => { + it('returns true for new timeRangeFilter', () => { + const state = wrapper.state(); + const context = { timeRangeFilter: [Math.random(), Math.random()] }; + const instance = wrapper.instance(); + expect(instance.shouldComponentUpdate(props, state, context)).toBe(true); + }); + + it('returns true for new trace', () => { + const state = wrapper.state(); + const instance = wrapper.instance(); + const trace2 = hydrateSpansWithProcesses(traceGenerator.trace({})); + const xformedTrace2 = transformTrace(trace2); + const altProps = { trace: trace2, xformedTrace: xformedTrace2 }; + expect(instance.shouldComponentUpdate(altProps, state, options.context)).toBe(true); + }); + + it('returns true for currentlyDragging', () => { + const state = { ...wrapper.state(), currentlyDragging: !wrapper.state('currentlyDragging') }; + const instance = wrapper.instance(); + expect(instance.shouldComponentUpdate(props, state, options.context)).toBe(true); + }); + + it('returns false, generally', () => { + const state = wrapper.state(); + const instance = wrapper.instance(); + expect(instance.shouldComponentUpdate(props, state, options.context)).toBe(false); + }); + }); -it('TraceSpanGraph.stopDragging should clear currentlyDragging and prevX', () => { - const wrapper = shallow(, defaultOptions); + describe('# onMouseMove()', () => { + it('does nothing if currentlyDragging is false', () => { + const updateTimeRangeFilter = sinon.spy(); + const context = { ...options.context, updateTimeRangeFilter }; + wrapper = shallow(, { ...options, context }); + wrapper.instance().svg = { clientWidth: 100 }; + wrapper.setState({ currentlyDragging: null }); + wrapper.instance().onMouseMove({ clientX: 45 }); + expect(wrapper.state('prevX')).toBe(null); + expect(updateTimeRangeFilter.called).toBeFalsy(); + }); + + it('stores the clientX on .state', () => { + wrapper.instance().svg = { clientWidth: 100 }; + wrapper.setState({ currentlyDragging: 'leftBound' }); + wrapper.instance().onMouseMove({ clientX: 45 }); + expect(wrapper.state('prevX')).toBe(45); + }); + + it('updates the timeRangeFilter for the left handle', () => { + const timestamp = trace.timestamp; + const duration = trace.duration; + const updateTimeRangeFilter = sinon.spy(); + const context = { ...options.context, updateTimeRangeFilter }; + wrapper = shallow(, { ...options, context }); + wrapper.instance().svg = { clientWidth: 100 }; + wrapper.setState({ prevX: 0, currentlyDragging: 'leftBound' }); + wrapper.instance().onMouseMove({ clientX: 45 }); + expect( + updateTimeRangeFilter.calledWith(timestamp + 0.45 * duration, timestamp + duration) + ).toBeTruthy(); + }); + + it('updates the timeRangeFilter for the right handle', () => { + const timestamp = trace.timestamp; + const duration = trace.duration; + const updateTimeRangeFilter = sinon.spy(); + const context = { ...options.context, updateTimeRangeFilter }; + wrapper = shallow(, { ...options, context }); + wrapper.instance().svg = { clientWidth: 100 }; + wrapper.setState({ prevX: 100, currentlyDragging: 'rightBound' }); + wrapper.instance().onMouseMove({ clientX: 45 }); + expect(updateTimeRangeFilter.calledWith(timestamp, timestamp + 0.45 * duration)).toBeTruthy(); + }); + }); - wrapper.instance().stopDragging(); + describe('# startDragging()', () => { + it('stores the boundName and the prevX in state', () => { + wrapper.instance().startDragging('leftBound', { clientX: 100 }); + expect(wrapper.state('currentlyDragging')).toBe('leftBound'); + expect(wrapper.state('prevX')).toBe(100); + }); + + it('binds event listeners to the window', () => { + const oldFn = window.addEventListener; + const fn = jest.fn(); + window.addEventListener = fn; + + wrapper.instance().startDragging('leftBound', { clientX: 100 }); + expect(fn.mock.calls.length).toBe(2); + const eventNames = [fn.mock.calls[0][0], fn.mock.calls[1][0]].sort(); + expect(eventNames).toEqual(['mousemove', 'mouseup']); + window.addEventListener = oldFn; + }); + }); - expect(wrapper.state('currentlyDragging')).toBe(null); - expect(wrapper.state('prevX')).toBe(null); + describe('# stopDragging()', () => { + it('TraceSpanGraph.stopDragging should clear currentlyDragging and prevX', () => { + const instance = wrapper.instance(); + instance.startDragging('leftBound', { clientX: 100 }); + expect(wrapper.state('currentlyDragging')).toBe('leftBound'); + expect(wrapper.state('prevX')).toBe(100); + instance.stopDragging(); + expect(wrapper.state('currentlyDragging')).toBe(null); + expect(wrapper.state('prevX')).toBe(null); + }); + }); }); diff --git a/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js b/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js index d3ced84730..e0593c0c86 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanTreeOffset.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -// import PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import React from 'react'; import './SpanTreeOffset.css'; @@ -35,3 +35,16 @@ export default function SpanTreeOffset({ level, hasChildren, childrenVisible, on ); } + +SpanTreeOffset.propTypes = { + level: PropTypes.number.isRequired, + hasChildren: PropTypes.bool, + childrenVisible: PropTypes.bool, + onClick: PropTypes.func, +}; + +SpanTreeOffset.defaultProps = { + hasChildren: false, + childrenVisible: false, + onClick: null, +}; diff --git a/src/components/TracePage/index.js b/src/components/TracePage/index.js index f946531913..327eb08ea6 100644 --- a/src/components/TracePage/index.js +++ b/src/components/TracePage/index.js @@ -45,13 +45,11 @@ import TracePageHeader from './TracePageHeader'; import TraceTimelineViewer from './TraceTimelineViewer'; import TraceSpanGraph from './TraceSpanGraph'; -import tracePropTypes from '../../propTypes/trace'; - export default class TracePage extends Component { static get propTypes() { return { fetchTrace: PropTypes.func.isRequired, - trace: tracePropTypes, + trace: PropTypes.object, xformedTrace: PropTypes.object, loading: PropTypes.bool, id: PropTypes.string.isRequired, diff --git a/src/components/TracePage/index.test.js b/src/components/TracePage/index.test.js index 30f51011be..07ad44d1f5 100644 --- a/src/components/TracePage/index.test.js +++ b/src/components/TracePage/index.test.js @@ -20,86 +20,56 @@ import React from 'react'; import sinon from 'sinon'; -import { shallow } from 'enzyme'; -import TracePage from '../../../src/components/TracePage'; -import TracePageHeader from '../../../src/components/TracePage/TracePageHeader'; -import TraceSpanGraph from '../../../src/components/TracePage/TraceSpanGraph'; +import { shallow, mount } from 'enzyme'; -const traceID = 'trace-id'; -const timestamp = new Date().getTime() * 1000; -const defaultProps = { - fetchTrace() {}, - id: traceID, - trace: { - traceID, - spans: [ - { - spanID: 'spanID-2', - traceID, - timestamp: timestamp + 10000, - duration: 10000, - operationName: 'whatever', - process: { - serviceName: 'my-other-service', - }, - }, - { - spanID: 'spanID-3', - traceID, - timestamp: timestamp + 20000, - duration: 10000, - operationName: 'bob', - process: { - serviceName: 'my-service', - }, - }, - { - spanID: 'spanID-1', - traceID, - timestamp, - duration: 50000, - operationName: 'whatever', - process: { - serviceName: 'my-service', - }, - }, - ], - }, -}; +import traceGenerator from '../../demo/trace-generators'; +import TracePage from './'; +import TracePageHeader from './TracePageHeader'; +import TraceSpanGraph from './TraceSpanGraph'; +import { transformTrace } from './TraceTimelineViewer/transforms'; -it(' should render a with the trace', () => { - const wrapper = shallow(); - expect(wrapper.find(TracePageHeader).get(0)).toBeTruthy(); -}); - -it(' should render a with the trace', () => { - const wrapper = shallow(); +describe('', () => { + const trace = traceGenerator.trace({}); + const defaultProps = { + trace, + fetchTrace() {}, + id: trace.traceID, + xformedTrace: transformTrace(trace), + }; - expect(wrapper.contains()).toBeTruthy(); -}); + let wrapper; -it(' should render an empty page if no trace', () => { - const wrapper = shallow(); + beforeEach(() => { + wrapper = shallow(); + }); - expect(wrapper.matchesElement(
)).toBeTruthy(); -}); + it('renders a ', () => { + expect(wrapper.find(TracePageHeader).get(0)).toBeTruthy(); + }); -// can't do mount tests in standard tape run. -it('TracePage should fetch the trace if necessary', () => { - const fetchTrace = sinon.spy(); - const wrapper = shallow(); - - wrapper.instance().componentDidMount(); - - expect(fetchTrace.called).toBeTruthy(); - expect(fetchTrace.calledWith(traceID)).toBeTruthy(); -}); + it('renders a ', () => { + const props = { trace: defaultProps.trace, xformedTrace: defaultProps.xformedTrace }; + expect(wrapper.contains()).toBeTruthy(); + }); -it('TracePage should not fetch the trace if already present', () => { - const fetchTrace = sinon.spy(); - const wrapper = shallow(); + it('renders an empty page when not provided a trace', () => { + wrapper = shallow(); + const isEmpty = wrapper.matchesElement(
); + expect(isEmpty).toBe(true); + }); - wrapper.instance().componentDidMount(); + // can't do mount tests in standard tape run. + it('fetches the trace if necessary', () => { + const fetchTrace = sinon.spy(); + wrapper = mount(); + expect(fetchTrace.called).toBeTruthy(); + expect(fetchTrace.calledWith(trace.traceID)).toBe(true); + }); - expect(!fetchTrace.called).toBeTruthy(); + it("doesn't fetch the trace if already present", () => { + const fetchTrace = sinon.spy(); + wrapper = shallow(); + wrapper.instance().componentDidMount(); + expect(fetchTrace.called).toBeFalsy(); + }); }); diff --git a/src/index.js b/src/index.js index 71ce057ddc..f8929e0a37 100644 --- a/src/index.js +++ b/src/index.js @@ -26,7 +26,6 @@ import 'basscss/css/basscss.css'; import JaegerUIApp from './components/App'; -export { default as SpanGraph } from './components/TracePage/SpanGraph'; export { default as TracePage } from './components/TracePage'; export { SearchTracePage } from './components/SearchTracePage'; export default JaegerUIApp; diff --git a/src/index.test.js b/src/index.test.js index af8157cbd9..6942e5b531 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -18,17 +18,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import JaegerUIApp, { SpanGraph, TracePage, SearchTracePage } from './index'; +import JaegerUIApp, { TracePage, SearchTracePage } from './index'; /* eslint-disable global-require, import/newline-after-import */ it('JaegerUIApp should be exported as default', () => { expect(JaegerUIApp).toBe(require('../src/components/App').default); }); -it('SpanGraph should be exported as as a named export', () => { - expect(SpanGraph).toBe(require('../src/components/SpanGraph').default); -}); - it('TracePage should be exported as as a named export', () => { expect(TracePage).toBe(require('../src/components/TracePage').default); });