diff --git a/packages/jaeger-ui/package.json b/packages/jaeger-ui/package.json index c5bc5e9a88..888a11eeba 100644 --- a/packages/jaeger-ui/package.json +++ b/packages/jaeger-ui/package.json @@ -9,8 +9,8 @@ "babel-plugin-import": "1.11.0", "bluebird": "^3.5.0", "customize-cra": "0.2.9", - "enzyme": "^3.2.0", - "enzyme-adapter-react-16": "^1.1.0", + "enzyme": "^3.8.0", + "enzyme-adapter-react-16": "^1.2.0", "enzyme-to-json": "^3.3.0", "http-proxy-middleware": "^0.19.1", "less": "3.9.0", diff --git a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/__snapshots__/DiffSelection.test.js.snap b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/__snapshots__/DiffSelection.test.js.snap index 727c219f47..f9aa7600bd 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/__snapshots__/DiffSelection.test.js.snap +++ b/packages/jaeger-ui/src/components/SearchTracePage/SearchResults/__snapshots__/DiffSelection.test.js.snap @@ -25,25 +25,23 @@ exports[`DiffSelection renders a trace as expected 1`] = `
- - -

- 1 - Selected for comparison -

-
+ +

+ 1 + Selected for comparison +

`; @@ -97,30 +95,28 @@ exports[`DiffSelection renders multiple traces as expected 1`] = `
- - - - -

+

-
+ Compare Traces + + +

+ 2 + Selected for comparison +

`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js index 15fa072b2d..569877873c 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.js @@ -48,22 +48,22 @@ type State = { graphTopOffset: number, }; -function syncStates(urlSt, reduxSt, forceState) { - const { a: urlA, b: urlB } = urlSt; - const { a: reduxA, b: reduxB } = reduxSt; +function syncStates(urlValues, reduxValues, forceState) { + const { a: urlA, b: urlB } = urlValues; + const { a: reduxA, b: reduxB } = reduxValues; if (urlA !== reduxA || urlB !== reduxB) { - forceState(urlSt); + forceState(urlValues); return; } - const urlCohort = new Set(urlSt.cohort || []); - const reduxCohort = new Set(reduxSt.cohort || []); + const urlCohort = new Set(urlValues.cohort); + const reduxCohort = new Set(reduxValues.cohort || []); if (urlCohort.size !== reduxCohort.size) { - forceState(urlSt); + forceState(urlValues); return; } const needSync = Array.from(urlCohort).some(id => !reduxCohort.has(id)); if (needSync) { - forceState(urlSt); + forceState(urlValues); } } diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.test.js new file mode 100644 index 0000000000..c59a3a27f0 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiff.test.js @@ -0,0 +1,370 @@ +// Copyright (c) 2019 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import * as redux from 'redux'; + +import { mapStateToProps, mapDispatchToProps, TraceDiffImpl } from './TraceDiff'; +import TraceDiffHeader from './TraceDiffHeader'; +import { actions as diffActions } from './duck'; +import * as TraceDiffUrl from './url'; +import * as jaegerApiActions from '../../actions/jaeger-api'; +import { fetchedState, TOP_NAV_HEIGHT } from '../../constants'; + +describe('TraceDiff', () => { + const defaultA = 'trace-id-a'; + const defaultB = 'trace-id-b'; + const defaultCohortIds = ['trace-id-cohort-0', 'trace-id-cohort-1', 'trace-id-cohort-2']; + const defaultCohort = [defaultA, defaultB, ...defaultCohortIds]; + const fetchMultipleTracesMock = jest.fn(); + const forceStateMock = jest.fn(); + const historyPushMock = jest.fn(); + const defaultProps = { + a: defaultA, + b: defaultB, + cohort: defaultCohort, + fetchMultipleTraces: fetchMultipleTracesMock, + forceState: forceStateMock, + history: { + push: historyPushMock, + }, + tracesData: new Map(defaultCohort.map(id => [id, { id, state: fetchedState.DONE }])), + traceDiffState: { + a: defaultA, + b: defaultB, + cohort: defaultCohort, + }, + }; + const newAValue = 'newAValue'; + const newBValue = 'newBValue'; + const nonDefaultCohortId = 'non-default-cohort-id'; + const getUrlSpyMockReturnValue = 'getUrlSpyMockReturnValue'; + let getUrlSpy; + let wrapper; + + beforeAll(() => { + getUrlSpy = jest.spyOn(TraceDiffUrl, 'getUrl').mockReturnValue(getUrlSpyMockReturnValue); + }); + + beforeEach(() => { + fetchMultipleTracesMock.mockClear(); + forceStateMock.mockClear(); + getUrlSpy.mockClear(); + historyPushMock.mockClear(); + wrapper = shallow(); + }); + + describe('syncStates', () => { + it('forces state if a is inconsistent between url and reduxState', () => { + wrapper.setProps({ a: newAValue }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: newAValue, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + }); + + it('forces state if b is inconsistent between url and reduxState', () => { + wrapper.setProps({ b: newBValue }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: newBValue, + cohort: defaultProps.cohort, + }); + }); + + it('forces state if cohort size has changed', () => { + const newCohort = [...defaultProps.cohort, nonDefaultCohortId]; + wrapper.setProps({ cohort: newCohort }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: newCohort, + }); + + wrapper.setProps({ + cohort: defaultProps.cohort, + traceDiffState: { ...defaultProps.traceDiffState, cohort: null }, + }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + }); + + it('forces state if cohort entry has changed', () => { + const newCohort = [...defaultProps.cohort.slice(1), nonDefaultCohortId]; + wrapper.setProps({ cohort: newCohort }); + expect(forceStateMock).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: newCohort, + }); + }); + + it('does not force state if cohorts have same values in differing orders', () => { + wrapper.setProps({ + traceDiffState: { + ...defaultProps.traceDiffState, + cohort: defaultProps.traceDiffState.cohort.slice().reverse(), + }, + }); + expect(forceStateMock).not.toHaveBeenCalled(); + }); + }); + + it('requests traces lacking a state', () => { + const newId0 = 'new-id-0'; + const newId1 = 'new-id-1'; + expect(fetchMultipleTracesMock).toHaveBeenCalledTimes(0); + wrapper.setProps({ cohort: [...defaultProps.cohort, newId0, newId1] }); + expect(fetchMultipleTracesMock).toHaveBeenCalledWith([newId0, newId1]); + expect(fetchMultipleTracesMock).toHaveBeenCalledTimes(1); + }); + + it('does not request traces if all traces have a state', () => { + const newId0 = 'new-id-0'; + const newId1 = 'new-id-1'; + expect(fetchMultipleTracesMock).toHaveBeenCalledTimes(0); + const cohort = [...defaultProps.cohort, newId0, newId1]; + const tracesData = new Map(defaultProps.tracesData); + tracesData.set(newId0, { id: newId0, state: fetchedState.ERROR }); + tracesData.set(newId1, { id: newId0, state: fetchedState.LOADING }); + wrapper.setProps({ cohort, tracesData }); + expect(fetchMultipleTracesMock).not.toHaveBeenCalled(); + }); + + it('updates url when TraceDiffHeader sets a or b', () => { + wrapper.find(TraceDiffHeader).prop('diffSetA')(newAValue); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: newAValue.toLowerCase(), + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + + wrapper.find(TraceDiffHeader).prop('diffSetB')(newBValue); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: newBValue.toLowerCase(), + cohort: defaultProps.cohort, + }); + + wrapper.find(TraceDiffHeader).prop('diffSetA')(''); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + + wrapper.find(TraceDiffHeader).prop('diffSetB')(''); + expect(getUrlSpy).toHaveBeenLastCalledWith({ + a: defaultProps.a, + b: defaultProps.b, + cohort: defaultProps.cohort, + }); + + expect(historyPushMock).toHaveBeenCalledTimes(4); + }); + + describe('render', () => { + it('renders as expected', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('handles a and b not in props.tracesData', () => { + const tracesData = new Map(defaultProps.tracesData); + tracesData.delete(defaultA); + tracesData.delete(defaultB); + wrapper.setProps({ tracesData }); + expect(wrapper.find(TraceDiffHeader).props()).toEqual( + expect.objectContaining({ + a: { id: defaultA }, + b: { id: defaultB }, + }) + ); + }); + + it('handles absent a and b', () => { + wrapper.setProps({ a: null, b: null }); + expect(wrapper.find(TraceDiffHeader).props()).toEqual(expect.objectContaining({ a: null, b: null })); + }); + }); + + describe('TraceDiff--graphWrapper top offset', () => { + const arbitraryHeight = TOP_NAV_HEIGHT * 2; + + it('initializes as TOP_NAV_HEIGHT', () => { + expect(wrapper.state().graphTopOffset).toBe(TOP_NAV_HEIGHT); + }); + + it('defaults to TOP_NAV_HEIGHT', () => { + wrapper.setState({ graphTopOffset: arbitraryHeight }); + wrapper.instance().headerWrapperRef(null); + expect(wrapper.state().graphTopOffset).toBe(TOP_NAV_HEIGHT); + }); + + it('adjusts TraceDiff--graphWrapper top offset based on TraceDiffHeader height', () => { + wrapper.instance().headerWrapperRef({ clientHeight: arbitraryHeight }); + expect(wrapper.state().graphTopOffset).toBe(TOP_NAV_HEIGHT + arbitraryHeight); + }); + }); + + describe('mapStateToProps', () => { + const getOwnProps = ({ a = defaultA, b = defaultB } = {}) => ({ + match: { + params: { + a, + b, + }, + }, + }); + const makeTestReduxState = ({ cohortIds = defaultCohortIds } = {}) => ({ + router: { + location: { + search: cohortIds.reduce((search, curr, i) => `${search}${i ? '&' : '?'}cohort=${curr}`, ''), + }, + }, + trace: { + traces: cohortIds.reduce((traces, id) => ({ ...traces, [id]: { id, state: fetchedState.DONE } }), {}), + }, + traceDiff: { + a: 'trace-diff-a', + b: 'trace-diff-b', + }, + }); + + it('gets a and b from ownProps', () => { + expect(mapStateToProps(makeTestReduxState(), getOwnProps())).toEqual( + expect.objectContaining({ + a: defaultA, + b: defaultB, + }) + ); + }); + + it('defaults cohort to empty array if a, b, and cohort are not available', () => { + expect( + mapStateToProps(makeTestReduxState({ cohortIds: [] }), getOwnProps({ a: null, b: null })).cohort + ).toEqual([]); + }); + + it('gets cohort from ownProps and state.router.location.search', () => { + expect(mapStateToProps(makeTestReduxState(), getOwnProps()).cohort).toEqual([ + defaultA, + defaultB, + ...defaultCohortIds, + ]); + }); + + it('filters falsy values from cohort', () => { + expect(mapStateToProps(makeTestReduxState(), getOwnProps({ a: null })).cohort).toEqual([ + defaultB, + ...defaultCohortIds, + ]); + + expect(mapStateToProps(makeTestReduxState(), getOwnProps({ b: null })).cohort).toEqual([ + defaultA, + ...defaultCohortIds, + ]); + + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, '', nonDefaultCohortId] }), + getOwnProps() + ).cohort + ).toEqual([defaultA, defaultB, ...defaultCohortIds, nonDefaultCohortId]); + }); + + it('filters redundant values from cohort', () => { + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, nonDefaultCohortId] }), + getOwnProps({ a: nonDefaultCohortId }) + ).cohort + ).toEqual([nonDefaultCohortId, defaultB, ...defaultCohortIds]); + + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, nonDefaultCohortId] }), + getOwnProps({ b: nonDefaultCohortId }) + ).cohort + ).toEqual([defaultA, nonDefaultCohortId, ...defaultCohortIds]); + + expect( + mapStateToProps( + makeTestReduxState({ cohortIds: [...defaultCohortIds, nonDefaultCohortId, nonDefaultCohortId] }), + getOwnProps() + ).cohort + ).toEqual([defaultA, defaultB, ...defaultCohortIds, nonDefaultCohortId]); + }); + + // This test may false negative if previous tests are failing + it('builds tracesData Map from cohort and state.trace.traces', () => { + const { tracesData, cohort: { length: expectedSize } } = mapStateToProps( + makeTestReduxState(), + getOwnProps() + ); + defaultCohortIds.forEach(id => { + expect(tracesData.get(id)).toEqual({ + id, + state: fetchedState.DONE, + }); + }); + expect(tracesData.get(defaultA)).toEqual({ + id: defaultA, + state: null, + }); + expect(tracesData.get(defaultB)).toEqual({ + id: defaultB, + state: null, + }); + expect(tracesData.size).toBe(expectedSize); + }); + + it('includes state.traceDiff as traceDiffState', () => { + const testReduxState = makeTestReduxState(); + const { traceDiffState } = mapStateToProps(testReduxState, getOwnProps()); + expect(traceDiffState).toBe(testReduxState.traceDiff); + }); + }); + + describe('mapDispatchToProps', () => { + let bindActionCreatorsSpy; + + beforeAll(() => { + bindActionCreatorsSpy = jest.spyOn(redux, 'bindActionCreators').mockImplementation(actions => { + if (actions === jaegerApiActions) { + return { fetchMultipleTraces: fetchMultipleTracesMock }; + } else if (actions === diffActions) { + return { forceState: forceStateMock }; + } + return {}; + }); + }); + + afterAll(() => { + bindActionCreatorsSpy.mockRestore(); + }); + + it('correctly binds actions to dispatch', () => { + const dispatchMock = () => {}; + const result = mapDispatchToProps(dispatchMock); + expect(result.fetchMultipleTraces).toBe(fetchMultipleTracesMock); + expect(result.forceState).toBe(forceStateMock); + expect(bindActionCreatorsSpy.mock.calls[0][1]).toBe(dispatchMock); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js index 9c0ae041cb..b8401b7a8b 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.js @@ -39,7 +39,7 @@ type Props = { const { classNameIsSmall } = DirectedGraph.propsFactories; -class TraceDiffGraph extends React.PureComponent { +export class UnconnectedTraceDiffGraph extends React.PureComponent { props: Props; layoutManager: LayoutManager; @@ -126,4 +126,4 @@ class TraceDiffGraph extends React.PureComponent { } } -export default connect(extractUiFindFromState)(TraceDiffGraph); +export default connect(extractUiFindFromState)(UnconnectedTraceDiffGraph); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.test.js new file mode 100644 index 0000000000..28bf70d558 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/TraceDiffGraph.test.js @@ -0,0 +1,155 @@ +// Copyright (c) 2019 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 * as React from 'react'; +import { shallow } from 'enzyme'; +// import _mapValues from 'lodash/mapValues'; + +import { UnconnectedTraceDiffGraph as TraceDiffGraph } from './TraceDiffGraph'; +import ErrorMessage from '../../common/ErrorMessage'; +import LoadingIndicator from '../../common/LoadingIndicator'; +import { fetchedState } from '../../../constants'; + +describe('TraceDiffGraph', () => { + const props = { + a: { + data: { + spans: [], + traceID: 'trace-id-a', + }, + error: null, + id: 'trace-id-a', + state: fetchedState.DONE, + }, + b: { + data: { + spans: [], + traceID: 'trace-id-b', + }, + error: null, + id: 'trace-id-b', + state: fetchedState.DONE, + }, + }; + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders warning when a or b are not provided', () => { + expect(wrapper.find('h1').length).toBe(0); + + wrapper.setProps({ a: undefined }); + expect(wrapper.find('h1').length).toBe(1); + expect(wrapper.find('h1').text()).toBe('At least two Traces are needed'); + + wrapper.setProps({ b: undefined }); + expect(wrapper.find('h1').length).toBe(1); + expect(wrapper.find('h1').text()).toBe('At least two Traces are needed'); + + wrapper.setProps({ a: props.a }); + expect(wrapper.find('h1').length).toBe(1); + expect(wrapper.find('h1').text()).toBe('At least two Traces are needed'); + }); + + it('renders warning when a or b have errored', () => { + expect(wrapper.find(ErrorMessage).length).toBe(0); + + const errorA = 'some error text for trace a'; + wrapper.setProps({ + a: { + ...props.a, + error: errorA, + }, + }); + + expect(wrapper.find(ErrorMessage).length).toBe(1); + expect(wrapper.find(ErrorMessage).props()).toEqual( + expect.objectContaining({ + error: errorA, + }) + ); + const errorB = 'some error text for trace a'; + wrapper.setProps({ + b: { + ...props.b, + error: errorB, + }, + }); + + expect(wrapper.find(ErrorMessage).length).toBe(2); + expect( + wrapper + .find(ErrorMessage) + .at(1) + .props() + ).toEqual( + expect.objectContaining({ + error: errorB, + }) + ); + wrapper.setProps({ + a: props.a, + }); + expect(wrapper.find(ErrorMessage).length).toBe(1); + expect(wrapper.find(ErrorMessage).props()).toEqual( + expect.objectContaining({ + error: errorB, + }) + ); + }); + + it('renders a loading indicator when a or b are loading', () => { + expect(wrapper.find(LoadingIndicator).length).toBe(0); + + wrapper.setProps({ + a: { + state: fetchedState.LOADING, + }, + }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); + + wrapper.setProps({ + b: { + state: fetchedState.LOADING, + }, + }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); + + wrapper.setProps({ a: props.a }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); + }); + + it('renders an empty div when a or b lack data', () => { + expect(wrapper.children().length).not.toBe(0); + + const { data: unusedAData, ...aWithoutData } = props.a; + wrapper.setProps({ a: aWithoutData }); + expect(wrapper.children().length).toBe(0); + + const { data: unusedBData, ...bWithoutData } = props.b; + wrapper.setProps({ b: bWithoutData }); + expect(wrapper.children().length).toBe(0); + + wrapper.setProps({ a: props.a }); + expect(wrapper.children().length).toBe(0); + }); + + it('cleans up layoutManager before unmounting', () => { + const layoutManager = jest.spyOn(wrapper.instance().layoutManager, 'stopAndRelease'); + wrapper.unmount(); + expect(layoutManager).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/__snapshots__/drawNode.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/__snapshots__/drawNode.test.js.snap new file mode 100644 index 0000000000..7b8d1ccb15 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/__snapshots__/drawNode.test.js.snap @@ -0,0 +1,677 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`drawNode diffNode renders as expected when props.a and props.b are the same 1`] = ` + + + + + 100 + + + + serviceName + + + + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-same" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + +
+ 100 + + + serviceName + + +
+ operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.a is 0 1`] = ` + + + + + + + + + 100 + + + + serviceName + + + + + + + + + + + 100 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-added" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + + + + 100 + + + serviceName + + +
+ + + + + 100 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.a is less than props.b 1`] = ` + + + + + + + + + 50 + + + + serviceName + + + + + + + + + + + 50 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-more" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + + + + 50 + + + serviceName + + +
+ + + + + 50 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.a is more than props.b 1`] = ` + + + + + + - + + 100 + + + + serviceName + + + + + + + + - + + 50 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-less" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + - + + 100 + + + serviceName + + +
+ + - + + 50 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.b is 0 1`] = ` + + + + + + - + + 100 + + + + serviceName + + + + + + + + - + + 100 + + % + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-changed is-removed" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + + +
+ + - + + 100 + + + serviceName + + +
+ + - + + 100 + + % + + + operationName +
+
+`; + +exports[`drawNode diffNode renders as expected when props.isUiFindMatch is true 1`] = ` + + + + + 100 + + + + serviceName + + + + + + + operationName + + + + + } + mouseEnterDelay={0.25} + mouseLeaveDelay={0.1} + overlayClassName="DiffNode--popover is-same is-ui-find-match" + overlayStyle={Object {}} + placement="top" + prefixCls="ant-popover" + transitionName="zoom-big" + trigger="hover" +> + + + + + + + + + + +
+ 100 + + + serviceName + + +
+ operationName +
+
+`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.test.js new file mode 100644 index 0000000000..a4796ee53f --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffGraph/drawNode.test.js @@ -0,0 +1,111 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 } from 'enzyme'; + +import drawNodeGenerator, { DiffNode } from './drawNode'; + +describe('drawNode', () => { + const members = [ + { + span: { + spanID: 'members-span-id-0', + }, + }, + { + span: { + spanID: 'members-span-id-1', + }, + }, + ]; + const operation = 'operationName'; + const service = 'serviceName'; + describe('diffNode', () => { + const defaultCount = 100; + const props = { + a: defaultCount, + b: defaultCount, + members, + operation, + service, + }; + + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders as expected when props.a and props.b are the same', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.a is less than props.b', () => { + wrapper.setProps({ a: defaultCount / 2 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.a is more than props.b', () => { + wrapper.setProps({ a: defaultCount * 2 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.a is 0', () => { + wrapper.setProps({ a: 0 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.b is 0', () => { + wrapper.setProps({ b: 0 }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders as expected when props.isUiFindMatch is true', () => { + wrapper.setProps({ isUiFindMatch: true }); + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('drawNode function', () => { + const dataKey = 'data-key'; + const dataValue = 'data-value'; + const key = 'vertex key'; + const vertex = { + data: { + data: { + [dataKey]: dataValue, + }, + members, + operation, + service, + }, + key, + }; + + it('extracts values from vertex.data', () => { + const drawNodeResult = drawNodeGenerator(new Set())(vertex); + expect(drawNodeResult.props[dataKey]).toBe(dataValue); + expect(drawNodeResult.props.isUiFindMatch).toBe(false); + expect(drawNodeResult.props.members).toBe(members); + expect(drawNodeResult.props.operation).toBe(operation); + expect(drawNodeResult.props.service).toBe(service); + }); + + it('passes isUiFindMatch as true if key is in set', () => { + const drawNodeResult = drawNodeGenerator(new Set([key]))(vertex); + expect(drawNodeResult.props.isUiFindMatch).toBe(true); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js index e3a44153ad..d4fd088fe1 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.js @@ -43,7 +43,7 @@ const defaultRowSelection = { type: 'radio', }; -const NEED_MORE_TRACES_MESSAGE = ( +export const NEED_MORE_TRACES_MESSAGE = (

Enter a Trace ID or perform a search and select from the results.

diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.test.js new file mode 100644 index 0000000000..745e4326ea --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/CohortTable.test.js @@ -0,0 +1,241 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 } from 'enzyme'; +import { Table, Tag } from 'antd'; + +import CohortTable, { NEED_MORE_TRACES_MESSAGE } from './CohortTable'; +import TraceTimelineLink from './TraceTimelineLink'; +import RelativeDate from '../../common/RelativeDate'; +import TraceName from '../../common/TraceName'; +import { fetchedState } from '../../../constants'; +import * as dateUtils from '../../../utils/date'; + +const { Column } = Table; + +describe('CohortTable', () => { + const cohort = [ + { + data: { + traceName: 'trace name 0', + }, + error: 'api error', + id: 'trace-id-0', + state: fetchedState.ERROR, + }, + { + id: 'trace-id-1', + }, + { + id: 'trace-id-2', + }, + ]; + const selectTrace = jest.fn(); + const props = { + cohort, + current: cohort[0].id, + selection: { + [cohort[0].id]: { + label: 'selected index 0', + }, + }, + selectTrace, + }; + + let formatDurationSpy; + let wrapper; + + /** + * Creates a new wrapper with default props and specified props. It is necessary to create a new wrapper + * when props change because enzyme does not support wrapper.setProps for classes that render an array of + * elements. + * + * @param {Object} [specifiedProps={}] - Props to set that are different from props defined above. + * @returns {Object} - New wrapper. + */ + function setProps(specifiedProps = {}) { + wrapper = shallow(); + } + + beforeAll(() => { + formatDurationSpy = jest.spyOn(dateUtils, 'formatDuration'); + }); + + beforeEach(() => { + selectTrace.mockReset(); + formatDurationSpy.mockReset(); + setProps(); + }); + + it('renders as expected', () => { + expect(wrapper).toMatchSnapshot(); + }); + + describe('row selection', () => { + let rowSelection; + + function updateRowSelection() { + rowSelection = wrapper.find(Table).prop('rowSelection'); + } + + beforeEach(() => { + updateRowSelection(); + }); + + it('defaults selectedRowKeys to empty array', () => { + setProps({ current: undefined }); + updateRowSelection(); + expect(rowSelection.selectedRowKeys).toEqual([]); + }); + + it('calls props.selectTrace on row selection', () => { + rowSelection.onChange([cohort[1].id, cohort[2].id]); + expect(selectTrace).toHaveBeenCalledWith(cohort[1].id); + }); + + it('calculates checkbox props for selected and current record with error', () => { + expect(rowSelection.getCheckboxProps(cohort[0])).toEqual({ disabled: true }); + }); + + it('calculates checkbox props for selected and current record without error', () => { + expect(rowSelection.getCheckboxProps({ ...cohort[0], state: fetchedState.DONE })).toEqual({}); + }); + + it('calculates checkbox props for selected but not current record without error', () => { + setProps({ + selection: { + ...props.selecetion, + [cohort[1].id]: { + label: 'selected index 1', + }, + }, + }); + updateRowSelection(); + expect(rowSelection.getCheckboxProps(cohort[1])).toEqual({ disabled: true }); + }); + + it('calculates checkbox props for not selected record', () => { + expect(rowSelection.getCheckboxProps(cohort[1])).toEqual({}); + }); + }); + + it('renders shortened id', () => { + const idRenderer = wrapper + .find(Column) + .find('[dataIndex="id"]') + .prop('render'); + const traceID = 'trace-id-longer-than-eight-characters'; + const renderedId = shallow(idRenderer(traceID)); + expect(renderedId.hasClass('u-tx-muted')).toBe(true); + expect(renderedId.text()).toBe(traceID.slice(0, 7)); + }); + + it('renders TraceName fragment when given complete data', () => { + const traceNameColumnRenderer = wrapper + .find(Column) + .find('[dataIndex="data.traceName"]') + .prop('render'); + const testTrace = cohort[0]; + const { id, error, state, data: { traceName } } = testTrace; + const renderedTraceNameColumn = shallow( + // traceNameRenderer returns a React Fragment, wrapper div helps enzyme +
{traceNameColumnRenderer('unused argument', testTrace)}
+ ); + + const tag = renderedTraceNameColumn.find(Tag); + expect(tag.length).toBe(1); + expect(tag.html().includes(props.selection[id].label)).toBe(true); + + const renderedTraceName = renderedTraceNameColumn.find(TraceName); + expect(renderedTraceName.length).toBe(1); + expect(renderedTraceName.props()).toEqual( + expect.objectContaining({ + error, + state, + traceName, + }) + ); + }); + + it('renders TraceName fragment when given minimal data', () => { + const traceNameColumnRenderer = wrapper + .find(Column) + .find('[dataIndex="data.traceName"]') + .prop('render'); + const testTrace = cohort[1]; + const renderedTraceNameColumn = shallow( + // traceNameRenderer returns a React Fragment, wrapper div helps enzyme +
{traceNameColumnRenderer('unused argument', testTrace)}
+ ); + + expect(renderedTraceNameColumn.find(Tag).length).toBe(0); + expect(renderedTraceNameColumn.find(TraceName).length).toBe(1); + }); + + it('renders date iff record state is fetchedState.DONE', () => { + const dateRenderer = wrapper + .find(Column) + .find('[dataIndex="data.startTime"]') + .prop('render'); + const date = 1548689901403; + + expect(dateRenderer(date, { state: fetchedState.ERROR })).toBe(false); + const renderedDate = dateRenderer(date, { state: fetchedState.DONE }); + expect(renderedDate.type).toBe(RelativeDate); + expect(renderedDate.props).toEqual({ + fullMonthName: true, + includeTime: true, + value: date / 1000, + }); + }); + + it('renders duration iff record state is fetchedState.DONE', () => { + const durationRenderer = wrapper + .find(Column) + .find('[dataIndex="data.duration"]') + .prop('render'); + const duration = 150; + const formatDurationSpyMockReturnValue = 'formatDurationSpyMockReturnValue'; + formatDurationSpy.mockReturnValue(formatDurationSpyMockReturnValue); + + expect(durationRenderer(duration, { state: fetchedState.ERROR })).toBe(false); + expect(formatDurationSpy).toHaveBeenCalledTimes(0); + + expect(durationRenderer(duration, { state: fetchedState.DONE })).toBe(formatDurationSpyMockReturnValue); + expect(formatDurationSpy).toHaveBeenCalledTimes(1); + expect(formatDurationSpy).toHaveBeenCalledWith(duration); + }); + + it('renders link', () => { + const linkRenderer = wrapper + .find(Column) + .find('[dataIndex="data.traceID"]') + .prop('render'); + const traceID = 'trace-id'; + const renderedLink = linkRenderer(traceID); + expect(renderedLink.type).toBe(TraceTimelineLink); + expect(renderedLink.props).toEqual({ + traceID, + }); + }); + + it('renders NEED_MORE_TRACES_MESSAGE if cohort is too small', () => { + expect(wrapper.contains(NEED_MORE_TRACES_MESSAGE)).toBe(false); + setProps({ cohort: cohort.slice(0, 1) }); + expect(wrapper.contains(NEED_MORE_TRACES_MESSAGE)).toBe(true); + setProps({ cohort: [] }); + expect(wrapper.contains(NEED_MORE_TRACES_MESSAGE)).toBe(true); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js index 59bce3ee40..81e515fa94 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.js @@ -76,10 +76,9 @@ export default class TraceDiffHeader extends React.PureComponent { const { tableVisible } = this.state; const { data: aData = {}, id: aId, state: aState, error: aError } = a || {}; const { data: bData = {}, id: bId, state: bState, error: bError } = b || {}; - const selection = { - [aId || '_']: { label: 'A' }, - [bId || '__']: { label: 'B' }, - }; + const selection = {}; + if (aId) selection[aId] = { label: 'A' }; + if (bId) selection[bId] = { label: 'B' }; const cohortTableA = ( ); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.test.js new file mode 100644 index 0000000000..3a126b4a54 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceDiffHeader.test.js @@ -0,0 +1,238 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 } from 'enzyme'; +import { Popover } from 'antd'; + +import TraceDiffHeader from './TraceDiffHeader'; +import { fetchedState } from '../../../constants'; + +describe('TraceDiffHeader', () => { + const cohort = [ + { + data: { + duration: 0, + // purposefully missing spans + startTime: 0, + traceName: 'cohort-trace-name-0', + }, + error: 'error 0', + id: 'cohort-id-0', + state: fetchedState.ERROR, + }, + { + data: { + duration: 100, + spans: [ + { + spanID: 'trace-1-span-0', + }, + ], + startTime: 100, + traceName: 'cohort-trace-name-1', + }, + error: 'error 1', + id: 'cohort-id-1', + state: fetchedState.DONE, + }, + { + data: { + duration: 200, + spans: [ + { + spanID: 'trace-2-span-1', + }, + { + spanID: 'trace-2-span-2', + }, + ], + startTime: 200, + traceName: 'cohort-trace-name-2', + }, + error: 'error 2', + id: 'cohort-id-2', + state: fetchedState.DONE, + }, + { + data: { + duration: 300, + spans: [ + { + spanID: 'trace-3-span-1', + }, + { + spanID: 'trace-3-span-2', + }, + { + spanID: 'trace-3-span-3', + }, + ], + startTime: 300, + traceName: 'cohort-trace-name-3', + }, + error: 'error 3', + id: 'cohort-id-3', + state: fetchedState.DONE, + }, + ]; + const diffSetA = jest.fn(); + const diffSetB = jest.fn(); + const props = { + a: cohort[1], + b: cohort[2], + cohort, + diffSetA, + diffSetB, + }; + + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders as expected', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('handles trace without spans', () => { + wrapper.setProps({ a: cohort[0] }); + }); + + it('handles absent a', () => { + wrapper.setProps({ a: null }); + expect(wrapper).toMatchSnapshot(); + }); + + it('handles absent b', () => { + wrapper.setProps({ b: null }); + expect(wrapper).toMatchSnapshot(); + }); + + it('handles absent a & b', () => { + wrapper.setProps({ a: null, b: null }); + expect(wrapper).toMatchSnapshot(); + }); + + it('manages visibility correctly', () => { + expect(wrapper.state().tableVisible).toBe(null); + const popovers = wrapper.find(Popover); + expect(popovers.length).toBe(2); + popovers.forEach(popover => expect(popover.prop('visible')).toBe(false)); + + wrapper + .find(Popover) + .at(0) + .prop('onVisibleChange')(true); + expect( + wrapper + .find(Popover) + .at(0) + .prop('visible') + ).toBe(true); + expect( + wrapper + .find(Popover) + .at(1) + .prop('visible') + ).toBe(false); + + wrapper + .find(Popover) + .at(1) + .prop('onVisibleChange')(true); + expect( + wrapper + .find(Popover) + .at(0) + .prop('visible') + ).toBe(false); + expect( + wrapper + .find(Popover) + .at(1) + .prop('visible') + ).toBe(true); + + // repeat onVisibleChange call to test that visibility remains correct + wrapper + .find(Popover) + .at(1) + .prop('onVisibleChange')(true); + expect( + wrapper + .find(Popover) + .at(0) + .prop('visible') + ).toBe(false); + expect( + wrapper + .find(Popover) + .at(1) + .prop('visible') + ).toBe(true); + + wrapper + .find(Popover) + .at(1) + .prop('onVisibleChange')(false); + wrapper.find(Popover).forEach(popover => expect(popover.prop('visible')).toBe(false)); + }); + + it('creates bound functions to set a & b and passes them to Popover JSX props correctly', () => { + const cohortTableASelectionID = 'cohortTableASelectionID'; + const traceIdInputASelectionID = 'traceIdInputASelectionID'; + expect(props.diffSetA).not.toHaveBeenCalled(); + const cohortTableBSelectionID = 'cohortTableBSelectionID'; + const traceIdInputBSelectionID = 'traceIdInputBSelectionID'; + expect(props.diffSetB).not.toHaveBeenCalled(); + + wrapper.setState({ tableVisible: 'a' }); + wrapper + .find(Popover) + .at(0) + .prop('content') + .props.selectTrace(cohortTableASelectionID); + expect(props.diffSetA).toHaveBeenLastCalledWith(cohortTableASelectionID); + expect(wrapper.state().tableVisible).toBe(null); + + wrapper.setState({ tableVisible: 'a' }); + wrapper + .find(Popover) + .at(0) + .prop('title') + .props.selectTrace(traceIdInputASelectionID); + expect(props.diffSetA).toHaveBeenLastCalledWith(traceIdInputASelectionID); + expect(wrapper.state().tableVisible).toBe(null); + + wrapper.setState({ tableVisible: 'b' }); + wrapper + .find(Popover) + .at(1) + .prop('content') + .props.selectTrace(cohortTableBSelectionID); + expect(props.diffSetB).toHaveBeenLastCalledWith(cohortTableBSelectionID); + expect(wrapper.state().tableVisible).toBe(null); + + wrapper.setState({ tableVisible: 'b' }); + wrapper + .find(Popover) + .at(1) + .prop('title') + .props.selectTrace(traceIdInputBSelectionID); + expect(props.diffSetB).toHaveBeenLastCalledWith(traceIdInputBSelectionID); + expect(wrapper.state().tableVisible).toBe(null); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceIdInput.test.js b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceIdInput.test.js new file mode 100644 index 0000000000..7c143b2063 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/TraceIdInput.test.js @@ -0,0 +1,32 @@ +// Copyright (c) 2019 The Jaeger Authors. +// +// 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 } from 'enzyme'; +import { Input } from 'antd'; + +import TraceIdInput from './TraceIdInput'; + +describe('TraceIdInput', () => { + const props = { + selectTrace: jest.fn(), + }; + const { Search } = Input; + + it('renders as expected', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find(Search).prop('onSearch')).toBe(props.selectTrace); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/CohortTable.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/CohortTable.test.js.snap new file mode 100644 index 0000000000..93bbf93060 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/CohortTable.test.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CohortTable renders as expected 1`] = ` +Array [ + + + + + + + +
, + "", +] +`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceDiffHeader.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceDiffHeader.test.js.snap new file mode 100644 index 0000000000..fb2acc4d8e --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceDiffHeader.test.js.snap @@ -0,0 +1,1007 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TraceDiffHeader handles absent a & b 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; + +exports[`TraceDiffHeader handles absent a 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; + +exports[`TraceDiffHeader handles absent b 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; + +exports[`TraceDiffHeader renders as expected 1`] = ` +
+
+

+ A +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+

+ VS +

+
+
+

+ B +

+
+ + } + mouseEnterDelay={0.1} + mouseLeaveDelay={0.1} + onVisibleChange={[Function]} + overlayClassName="TraceDiffHeader--popover" + overlayStyle={Object {}} + placement="bottomLeft" + prefixCls="ant-popover" + title={ + + } + transitionName="zoom-big" + trigger="click" + visible={false} + > +
+ +
+
+
+`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceHeader.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceHeader.test.js.snap index 03dd240773..06470e0eb1 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceHeader.test.js.snap +++ b/packages/jaeger-ui/src/components/TraceDiff/TraceDiffHeader/__snapshots__/TraceHeader.test.js.snap @@ -106,29 +106,27 @@ exports[`TraceHeader renders as expected 1`] = ` className="TraecDiffHeader--traceTitle" > - - - - - trace-i - - - + } + key="name" + state={null} + traceName="trace name" + /> + + + trace-i + + - - - - - trace-i - - - + } + key="name" + state="FETCH_DONE" + traceName="trace name" + /> + + + trace-i + + +`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/__snapshots__/TraceDiff.test.js.snap b/packages/jaeger-ui/src/components/TraceDiff/__snapshots__/TraceDiff.test.js.snap new file mode 100644 index 0000000000..e24f00eb34 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/__snapshots__/TraceDiff.test.js.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TraceDiff render renders as expected 1`] = ` + +
+ +
+
+ +
+
+`; diff --git a/packages/jaeger-ui/src/components/TraceDiff/duck.js b/packages/jaeger-ui/src/components/TraceDiff/duck.js index 22cdbc7732..f82a353d0b 100644 --- a/packages/jaeger-ui/src/components/TraceDiff/duck.js +++ b/packages/jaeger-ui/src/components/TraceDiff/duck.js @@ -52,21 +52,21 @@ export const actions = fullActions.jaegerUi.traceDiff; function cohortAddTrace(state, { payload }) { const { traceID } = payload; - const cohort = state.cohort.slice(); - if (cohort.indexOf(traceID) >= 0) { + if (state.cohort.indexOf(traceID) >= 0) { return state; } + const cohort = state.cohort.slice(); cohort.push(traceID); return { ...state, cohort }; } function cohortRemoveTrace(state, { payload }) { const { traceID } = payload; - const cohort = state.cohort.slice(); - const i = cohort.indexOf(traceID); + const i = state.cohort.indexOf(traceID); if (i < 0) { return state; } + const cohort = state.cohort.slice(); cohort.splice(i, 1); const a = state.a === traceID ? null : state.a; const b = state.b === traceID ? null : state.b; diff --git a/packages/jaeger-ui/src/components/TraceDiff/duck.test.js b/packages/jaeger-ui/src/components/TraceDiff/duck.test.js new file mode 100644 index 0000000000..0d948d6821 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/duck.test.js @@ -0,0 +1,135 @@ +// 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 { createStore } from 'redux'; + +import reducer, { actions, newInitialState } from './duck'; + +describe('TraceDiff/duck', () => { + const initialCohort = ['trace-id-0', 'trace-id-1', 'trace-id-2']; + const newTraceId = 'new-trace-id'; + let store; + + beforeEach(() => { + store = createStore(reducer, { + a: initialCohort[0], + b: initialCohort[1], + cohort: initialCohort, + }); + }); + + describe('newInitialState', () => { + it('creates an empty set', () => { + expect(newInitialState()).toEqual({ + a: null, + b: null, + cohort: [], + }); + }); + }); + + describe('cohortAddTrace', () => { + it('adds trace that does not already exist in state', () => { + const oldCohort = store.getState().cohort; + expect(oldCohort.includes(newTraceId)).toBe(false); + + store.dispatch(actions.cohortAddTrace(newTraceId)); + const newCohort = store.getState().cohort; + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(newTraceId)).toBe(true); + expect(newCohort).toEqual(expect.arrayContaining(oldCohort)); + }); + + it('returns original state if traceID already exists in state', () => { + const state = store.getState(); + store.dispatch(actions.cohortAddTrace(initialCohort[0])); + expect(store.getState()).toBe(state); + }); + }); + + describe('cohortRemoveTrace', () => { + it('removes trace that exists in state.cohort', () => { + const oldCohort = store.getState().cohort; + store.dispatch(actions.cohortRemoveTrace(initialCohort[2])); + const newCohort = store.getState().cohort; + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(initialCohort[2])).toBe(false); + expect(newCohort).toEqual(oldCohort.slice(0, 2)); + }); + + it('removes state.a', () => { + const oldState = store.getState(); + const oldCohort = oldState.cohort; + store.dispatch(actions.cohortRemoveTrace(oldState.a)); + const newState = store.getState(); + const newCohort = newState.cohort; + expect(newState.a).toBe(null); + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(oldState.a)).toBe(false); + expect(newCohort).toEqual(oldCohort.slice(1)); + }); + + it('removes state.b', () => { + const oldState = store.getState(); + const oldCohort = oldState.cohort; + store.dispatch(actions.cohortRemoveTrace(oldState.b)); + const newState = store.getState(); + const newCohort = newState.cohort; + expect(newState.b).toBe(null); + expect(newCohort).not.toBe(oldCohort); + expect(newCohort.includes(oldState.b)).toBe(false); + expect(newCohort).toEqual(oldCohort.filter(entry => entry !== oldState.b)); + }); + + it('returns original state if traceID already exists in state', () => { + const state = store.getState(); + store.dispatch(actions.cohortRemoveTrace(newTraceId)); + expect(store.getState()).toBe(state); + }); + }); + + describe('diffSetA', () => { + it('set a to provided traceId', () => { + const oldState = store.getState(); + store.dispatch(actions.diffSetA(newTraceId)); + const newState = store.getState(); + expect(newState).not.toBe(oldState); + expect(newState).toEqual({ + ...oldState, + a: newTraceId, + }); + }); + }); + + describe('diffSetB', () => { + it('set b to provided traceId', () => { + const oldState = store.getState(); + store.dispatch(actions.diffSetB(newTraceId)); + const newState = store.getState(); + expect(newState).not.toBe(oldState); + expect(newState).toEqual({ + ...oldState, + b: newTraceId, + }); + }); + }); + + describe('forceState', () => { + it('returns given state', () => { + const newState = newInitialState(); + store.dispatch(actions.forceState(newState)); + expect(store.getState()).toBe(newState); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/getValidState.test.js b/packages/jaeger-ui/src/components/TraceDiff/getValidState.test.js new file mode 100644 index 0000000000..52c1dc5e02 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/getValidState.test.js @@ -0,0 +1,53 @@ +// Copyright (c) 2019 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 getValidState from './getValidState'; + +describe('getValidState', () => { + const a = 'a string'; + const b = 'b string'; + const cohort = ['first string', 'second string', 'third string']; + + it('handles absent argument', () => { + expect(getValidState()).toEqual({ + a: undefined, + b: undefined, + cohort: [], + }); + }); + + it('uses cohort kwarg when a and b are missing', () => { + expect(getValidState({ cohort })).toEqual({ + a: cohort[0], + b: cohort[1], + cohort, + }); + }); + + it('uses a and b when provided', () => { + expect(getValidState({ a, b, cohort })).toEqual({ + a, + b, + cohort: [a, b, ...cohort], + }); + }); + + it('uses b as a and cohort[0] for b when only b is provided', () => { + expect(getValidState({ b, cohort })).toEqual({ + a: b, + b: cohort[0], + cohort: [b, ...cohort], + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TraceDiff/url.test.js b/packages/jaeger-ui/src/components/TraceDiff/url.test.js new file mode 100644 index 0000000000..8997bde879 --- /dev/null +++ b/packages/jaeger-ui/src/components/TraceDiff/url.test.js @@ -0,0 +1,64 @@ +// Copyright (c) 2019 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 * as reactRouterDom from 'react-router-dom'; + +import { ROUTE_PATH, matches, getUrl } from './url'; + +describe('TraceDiff/url', () => { + describe('matches', () => { + const path = 'path argument'; + let matchPathSpy; + + beforeAll(() => { + matchPathSpy = jest.spyOn(reactRouterDom, 'matchPath'); + }); + + it('calls matchPath with expected arguments', () => { + matches(path); + expect(matchPathSpy).toHaveBeenLastCalledWith(path, { + path: ROUTE_PATH, + strict: true, + exact: true, + }); + }); + + it("returns truthiness of matchPath's return value", () => { + matchPathSpy.mockReturnValueOnce(null); + expect(matches(path)).toBe(false); + matchPathSpy.mockReturnValueOnce({}); + expect(matches(path)).toBe(true); + }); + }); + + describe('getUrl', () => { + it('handles an empty state', () => { + expect(getUrl()).toEqual('/trace/...'); + }); + + it('handles a single traceId', () => { + const cohort = ['first']; + expect(getUrl({ cohort })).toEqual(`/trace/${cohort[0]}...?cohort=${cohort[0]}`); + }); + + it('handles multiple traceIds', () => { + const cohort = ['first', 'second', 'third']; + const result = getUrl({ cohort }); + expect(result).toMatch(`${cohort[0]}...${cohort[1]}`); + cohort.forEach(cohortEntry => { + expect(result).toMatch(`cohort=${cohortEntry}`); + }); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/index.test.js b/packages/jaeger-ui/src/components/TracePage/index.test.js index c601a7a014..42a36d132d 100644 --- a/packages/jaeger-ui/src/components/TracePage/index.test.js +++ b/packages/jaeger-ui/src/components/TracePage/index.test.js @@ -20,8 +20,10 @@ jest.mock('./scroll-page'); jest.mock('../../utils/filter-spans'); jest.mock('../../utils/update-ui-find'); // mock these to enable mount() +jest.mock('./TraceGraph/TraceGraph'); jest.mock('./TracePageHeader/SpanGraph'); jest.mock('./TracePageHeader/TracePageHeader.track'); +jest.mock('./TracePageHeader/TracePageSearchBar'); jest.mock('./TraceTimelineViewer'); import React from 'react'; @@ -37,6 +39,7 @@ import { VIEW_MIN_RANGE, } from './index'; import * as track from './index.track'; +import ArchiveNotifier from './ArchiveNotifier'; import { reset as resetShortcuts } from './keyboard-shortcuts'; import { cancel as cancelScroll } from './scroll-page'; import SpanGraph from './TracePageHeader/SpanGraph'; @@ -79,6 +82,7 @@ describe('', () => { const trace = transformTraceData(traceGenerator.trace({})); const defaultProps = { + acknowledgeArchive: () => {}, fetchTrace() {}, id: trace.traceID, history: { @@ -152,10 +156,6 @@ describe('', () => { expect(filterSpansSpy).toHaveBeenLastCalledWith(uiFind, newTrace.data.spans); }); - it.skip('renders a ', () => { - expect(wrapper.find(TracePageHeader).get(0)).toBeTruthy(); - }); - it('renders a a loading indicator when not provided a fetched trace', () => { wrapper.setProps({ trace: null }); const loading = wrapper.find(LoadingIndicator); @@ -173,6 +173,37 @@ describe('', () => { expect(loading.length).toBe(1); }); + it('forces lowercase id', () => { + const replaceMock = jest.fn(); + const props = { + ...defaultProps, + id: trace.traceID.toUpperCase(), + history: { + replace: replaceMock, + }, + }; + shallow(); + expect(replaceMock).toHaveBeenCalledWith( + expect.objectContaining({ + pathname: expect.stringContaining(trace.traceID), + }) + ); + }); + + it('focuses on search bar when there is a search bar and focusOnSearchBar is called', () => { + const focus = jest.fn(); + wrapper.instance()._searchBar.current = { + focus, + }; + wrapper.instance().focusOnSearchBar(); + expect(focus).toHaveBeenCalledTimes(1); + }); + + it('handles absent search bar when there is not a search bar and focusOnSearchBar is called', () => { + expect(wrapper.instance()._searchBar.current).toBe(null); + wrapper.instance().focusOnSearchBar(); + }); + it('fetches the trace if necessary', () => { const fetchTrace = sinon.spy(); wrapper = mount(); @@ -180,13 +211,13 @@ describe('', () => { expect(fetchTrace.calledWith(trace.traceID)).toBe(true); }); - it.skip("doesn't fetch the trace if already present", () => { + it("doesn't fetch the trace if already present", () => { const fetchTrace = sinon.spy(); wrapper = mount(); expect(fetchTrace.called).toBeFalsy(); }); - it.skip('resets the view range when the trace changes', () => { + it('resets the view range when the trace changes', () => { const altTrace = { ...trace, traceID: 'some-other-id' }; // mount because `.componentDidUpdate()` wrapper = mount(); @@ -214,6 +245,130 @@ describe('', () => { expect(cancelScroll.mock.calls).toEqual([[]]); }); + describe('TracePageHeader props', () => { + describe('canCollapse', () => { + it('is true if !embedded', () => { + expect(wrapper.find(TracePageHeader).prop('canCollapse')).toBe(true); + }); + + it('is true if either of embedded.timeline.hideSummary and embedded.timeline.hideMinimap are false', () => { + [true, false].forEach(hideSummary => { + [true, false].forEach(hideMinimap => { + const embedded = { + timeline: { + hideSummary, + hideMinimap, + }, + }; + wrapper.setProps({ embedded }); + expect(wrapper.find(TracePageHeader).prop('canCollapse')).toBe(!hideSummary || !hideMinimap); + }); + }); + }); + }); + + describe('calculates hideMap correctly', () => { + it('is true if on traceGraphView', () => { + wrapper.instance().traceDagEV = { vertices: [], nodes: [] }; + wrapper.setState({ traceGraphView: true }); + expect(wrapper.find(TracePageHeader).prop('hideMap')).toBe(true); + }); + + it('is true if embedded indicates it should be', () => { + wrapper.setProps({ + embedded: { + timeline: { + hideMinimap: false, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideMap')).toBe(false); + wrapper.setProps({ + embedded: { + timeline: { + hideMinimap: true, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideMap')).toBe(true); + }); + }); + + describe('calculates hideSummary correctly', () => { + it('is false if embedded is not provided', () => { + expect(wrapper.find(TracePageHeader).prop('hideSummary')).toBe(false); + }); + + it('is true if embedded indicates it should be', () => { + wrapper.setProps({ + embedded: { + timeline: { + hideSummary: false, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideSummary')).toBe(false); + wrapper.setProps({ + embedded: { + timeline: { + hideSummary: true, + }, + }, + }); + expect(wrapper.find(TracePageHeader).prop('hideSummary')).toBe(true); + }); + }); + + describe('showArchiveButton', () => { + it('is true when not embedded and archive is enabled', () => { + [{ timeline: {} }, undefined].forEach(embedded => { + [true, false].forEach(archiveEnabled => { + wrapper.setProps({ embedded, archiveEnabled }); + expect(wrapper.find(TracePageHeader).prop('showArchiveButton')).toBe(!embedded && archiveEnabled); + }); + }); + }); + }); + + describe('resultCount', () => { + it('is the size of findMatchesIDs when available', () => { + expect(wrapper.find(TracePageHeader).prop('resultCount')).toBe(0); + + const size = 20; + filterSpansSpy.mockReturnValueOnce({ size }); + wrapper.setProps({ uiFind: 'new ui find to bust memo' }); + expect(wrapper.find(TracePageHeader).prop('resultCount')).toBe(size); + }); + + it('defaults to 0', () => { + filterSpansSpy.mockReturnValueOnce(null); + wrapper.setProps({ uiFind: 'new ui find to bust memo' }); + expect(wrapper.find(TracePageHeader).prop('resultCount')).toBe(0); + }); + }); + + describe('isEmbedded derived props', () => { + it('toggles derived props when embedded is provided', () => { + expect(wrapper.find(TracePageHeader).props()).toEqual( + expect.objectContaining({ + showShortcutsHelp: true, + showStandaloneLink: false, + showViewOptions: true, + }) + ); + + wrapper.setProps({ embedded: { timeline: {} } }); + expect(wrapper.find(TracePageHeader).props()).toEqual( + expect.objectContaining({ + showShortcutsHelp: false, + showStandaloneLink: true, + showViewOptions: false, + }) + ); + }); + }); + }); + describe('_adjustViewRange()', () => { let instance; let time; @@ -277,6 +432,28 @@ describe('', () => { }); }); + describe('Archive', () => { + it('renders ArchiveNotifier if props.archiveEnabled is true', () => { + expect(wrapper.find(ArchiveNotifier).length).toBe(0); + wrapper.setProps({ archiveEnabled: true }); + expect(wrapper.find(ArchiveNotifier).length).toBe(1); + }); + + it('calls props.acknowledgeArchive when ArchiveNotifier acknowledges', () => { + const acknowledgeArchive = jest.fn(); + wrapper.setProps({ acknowledgeArchive, archiveEnabled: true }); + wrapper.find(ArchiveNotifier).prop('acknowledge')(); + expect(acknowledgeArchive).toHaveBeenCalledWith(defaultProps.id); + }); + + it("calls props.archiveTrace when TracePageHeader's archive button is clicked", () => { + const archiveTrace = jest.fn(); + wrapper.setProps({ archiveTrace }); + wrapper.find(TracePageHeader).prop('onArchiveClicked')(); + expect(archiveTrace).toHaveBeenCalledWith(defaultProps.id); + }); + }); + describe('manages various UI state', () => { let header; let spanGraph; @@ -296,7 +473,7 @@ describe('', () => { refreshWrappers(); }); - it.skip('propagates headerHeight changes', () => { + it('propagates headerHeight changes', () => { const h = 100; const { setHeaderHeight } = wrapper.instance(); // use the method directly because it is a `ref` prop @@ -313,17 +490,7 @@ describe('', () => { expect(sections.length).toBe(0); }); - it.skip('propagates textFilter changes', () => { - const s = 'abc'; - const { updateTextFilter } = header.props(); - expect(header.prop('textFilter')).toBe(''); - updateTextFilter(s); - wrapper.update(); - refreshWrappers(); - expect(header.prop('textFilter')).toBe(s); - }); - - it.skip('propagates slimView changes', () => { + it('propagates slimView changes', () => { const { onSlimViewClicked } = header.props(); expect(header.prop('slimView')).toBe(false); expect(spanGraph.type()).toBeDefined(); @@ -334,7 +501,24 @@ describe('', () => { expect(spanGraph.length).toBe(0); }); - it.skip('propagates viewRange changes', () => { + it('propagates textFilter changes', () => { + const s = 'abc'; + expect(header.prop('textFilter')).toBeUndefined(); + wrapper.setProps({ uiFind: s }); + refreshWrappers(); + expect(header.prop('textFilter')).toBe(s); + }); + + it('propagates traceGraphView changes', () => { + const { onTraceGraphViewClicked } = header.props(); + expect(header.prop('traceGraphView')).toBe(false); + onTraceGraphViewClicked(); + wrapper.update(); + refreshWrappers(); + expect(header.prop('traceGraphView')).toBe(true); + }); + + it('propagates viewRange changes', () => { const viewRange = { time: { current: [0, 1] }, }; @@ -375,7 +559,7 @@ describe('', () => { refreshWrappers(); }); - it.skip('tracks setting the header to slim-view', () => { + it('tracks setting the header to slim-view', () => { const { onSlimViewClicked } = header.props(); trackSlimHeaderToggle.mockReset(); onSlimViewClicked(true); @@ -383,15 +567,7 @@ describe('', () => { expect(trackSlimHeaderToggle.mock.calls).toEqual([[true], [false]]); }); - it.skip('tracks setting or clearing the filter', () => { - const { updateTextFilter } = header.props(); - track.trackFilter.mockClear(); - updateTextFilter('abc'); - updateTextFilter(''); - expect(track.trackFilter.mock.calls).toEqual([['abc'], ['']]); - }); - - it.skip('tracks changes to the viewRange', () => { + it('tracks changes to the viewRange', () => { const src = 'some-source'; const { updateViewRangeTime } = spanGraph.props(); track.trackRange.mockClear(); @@ -453,6 +629,23 @@ describe('mapStateToProps()', () => { }); }); + it('handles falsy ownProps.match.params.id', () => { + const props = mapStateToProps(state, { + match: { + params: { + id: '', + }, + }, + }); + expect(props).toEqual( + expect.objectContaining({ + archiveTraceState: null, + id: '', + trace: null, + }) + ); + }); + it('propagates fromSearch correctly', () => { const fakeUrl = 'fake-url'; state.router.location.state = { fromSearch: fakeUrl }; diff --git a/packages/jaeger-ui/src/utils/plexus/set-on-graph.js b/packages/jaeger-ui/src/utils/plexus/set-on-graph.js index 70b006734e..5f04315a01 100644 --- a/packages/jaeger-ui/src/utils/plexus/set-on-graph.js +++ b/packages/jaeger-ui/src/utils/plexus/set-on-graph.js @@ -1,6 +1,6 @@ // @flow -// Copyright (c) 2018 Uber Technologies, Inc. +// Copyright (c) 2019 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. @@ -17,7 +17,7 @@ import _get from 'lodash/get'; const BASE_MATCH_SIZE = 8; -const SCALABLE_MATCH_SIZE = 8; +const SCALABLE_MATCH_SIZE = 4; export function setOnEdgesContainer(state: Object) { const { zoomTransform } = state; diff --git a/packages/jaeger-ui/src/utils/plexus/set-on-graph.test.js b/packages/jaeger-ui/src/utils/plexus/set-on-graph.test.js new file mode 100644 index 0000000000..d481e891e5 --- /dev/null +++ b/packages/jaeger-ui/src/utils/plexus/set-on-graph.test.js @@ -0,0 +1,72 @@ +// Copyright (c) 2019 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 { setOnNodesContainer, setOnEdgesContainer, setOnNode } from './set-on-graph'; + +describe('Set on graph utils', () => { + describe('setOnNodesContainer', () => { + function getComputedWidth(k) { + const { style } = setOnNodesContainer({ zoomTransform: k != undefined ? { k } : undefined }); // eslint-disable-line eqeqeq + return Number.parseInt(style.outline.split(' ')[2].split('px')[0], 10); + } + + const SIZE_IDENTITY = 12; + + it('defaults style object with outline width off of 2 if zoomTransform.k is not provided', () => { + expect(getComputedWidth(null)).toBe(SIZE_IDENTITY); + expect(getComputedWidth(undefined)).toBe(SIZE_IDENTITY); + }); + + it('calculates style object with outline width at default size if zoomTransform.k is 1', () => { + expect(getComputedWidth(1)).toBe(SIZE_IDENTITY); + }); + + it('calculates style object with outline width one third larger if zoomTransform.k is .5', () => { + expect(getComputedWidth(0.5)).toBe(4 / 3 * SIZE_IDENTITY); + }); + + it('calculates style object with outline width two thirds larger if zoomTransform.k is .33', () => { + expect(getComputedWidth(0.33)).toBe(5 / 3 * SIZE_IDENTITY); + }); + + it('calculates style object with outline width twice as large if zoomTransform.k is .25', () => { + expect(getComputedWidth(0.25)).toBe(2 * SIZE_IDENTITY); + }); + }); + + describe('setOnEdgesContainer', () => { + it('returns null if zoomTransform kwarg is falsy', () => { + expect(setOnEdgesContainer({ zoomTransform: null })).toBe(null); + expect(setOnEdgesContainer({ zoomTransform: undefined })).toBe(null); + }); + + it('calculates style object with opacity off of zoomTransform.k', () => { + expect(setOnEdgesContainer({ zoomTransform: { k: 0.0 } }).style.opacity).toBe(0.1); + expect(setOnEdgesContainer({ zoomTransform: { k: 0.3 } }).style.opacity).toBe(0.37); + expect(setOnEdgesContainer({ zoomTransform: { k: 0.5 } }).style.opacity).toBe(0.55); + expect(setOnEdgesContainer({ zoomTransform: { k: 0.7 } }).style.opacity).toBe(0.73); + expect(setOnEdgesContainer({ zoomTransform: { k: 1.0 } }).style.opacity).toBe(1); + }); + }); + + describe('setOnNode', () => { + it("inherits container's outline", () => { + expect(setOnNode()).toEqual({ + style: { + outline: 'inherit', + }, + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index f1208e8d4c..5852325951 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1551,6 +1551,15 @@ array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" +array.prototype.flat@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4" + integrity sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw== + dependencies: + define-properties "^1.1.2" + es-abstract "^1.10.0" + function-bind "^1.1.1" + arraybuffer.slice@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" @@ -4985,24 +4994,28 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" -enzyme-adapter-react-16@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.0.tgz#86c5db7c10f0be6ec25d54ca41b59f2abb397cf4" +enzyme-adapter-react-16@^1.2.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.7.1.tgz#c37c4cb0fd75e88a063154a7a88096474914496a" + integrity sha512-OQXKgfHWyHN3sFu2nKj3mhgRcqIPIJX6aOzq5AHVFES4R9Dw/vCBZFMPyaG81g2AZ5DogVh39P3MMNUbqNLTcw== dependencies: - enzyme-adapter-utils "^1.1.0" - lodash "^4.17.4" - object.assign "^4.0.4" + enzyme-adapter-utils "^1.9.0" + function.prototype.name "^1.1.0" + object.assign "^4.1.0" object.values "^1.0.4" - prop-types "^15.5.10" + prop-types "^15.6.2" + react-is "^16.6.1" react-test-renderer "^16.0.0-0" -enzyme-adapter-utils@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.2.0.tgz#7f4471ee0a70b91169ec8860d2bf0a6b551664b2" +enzyme-adapter-utils@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.9.1.tgz#68196fdaf2a9f51f31603cbae874618661233d72" + integrity sha512-LWc88BbKztLXlpRf5Ba/pSMJRaNezAwZBvis3N/IuB65ltZEh2E2obWU9B36pAbw7rORYeBUuqc79OL17ZzN1A== dependencies: - lodash "^4.17.4" - object.assign "^4.0.4" - prop-types "^15.5.10" + function.prototype.name "^1.1.0" + object.assign "^4.1.0" + prop-types "^15.6.2" + semver "^5.6.0" enzyme-to-json@^3.3.0: version "3.3.0" @@ -5010,21 +5023,30 @@ enzyme-to-json@^3.3.0: dependencies: lodash "^4.17.4" -enzyme@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.2.0.tgz#998bdcda0fc71b8764a0017f7cc692c943f54a7a" +enzyme@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.8.0.tgz#646d2d5d0798cb98fdec39afcee8a53237b47ad5" + integrity sha512-bfsWo5nHyZm1O1vnIsbwdfhU989jk+squU9NKvB+Puwo5j6/Wg9pN5CO0YJelm98Dao3NPjkDZk+vvgwpMwYxw== dependencies: + array.prototype.flat "^1.2.1" cheerio "^1.0.0-rc.2" - function.prototype.name "^1.0.3" - has "^1.0.1" + function.prototype.name "^1.1.0" + has "^1.0.3" + is-boolean-object "^1.0.0" + is-callable "^1.1.4" + is-number-object "^1.0.3" + is-string "^1.0.4" is-subset "^0.1.1" - lodash "^4.17.4" + lodash.escape "^4.0.1" + lodash.isequal "^4.5.0" + object-inspect "^1.6.0" object-is "^1.0.1" - object.assign "^4.0.4" + object.assign "^4.1.0" object.entries "^1.0.4" object.values "^1.0.4" raf "^3.4.0" rst-selector-parser "^2.2.3" + string.prototype.trim "^1.1.2" errno@^0.1.1, errno@~0.1.7: version "0.1.7" @@ -5050,7 +5072,7 @@ error-stack-parser@1.3.6: dependencies: stackframe "^0.3.1" -es-abstract@^1.11.0: +es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.5.0: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" dependencies: @@ -6184,12 +6206,13 @@ function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" -function.prototype.name@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.0.3.tgz#0099ae5572e9dd6f03c97d023fd92bcc5e639eac" +function.prototype.name@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327" + integrity sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg== dependencies: define-properties "^1.1.2" - function-bind "^1.1.0" + function-bind "^1.1.1" is-callable "^1.1.3" functional-red-black-tree@^1.0.1: @@ -8729,6 +8752,11 @@ lodash.debounce@^4.0.0, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -8745,6 +8773,11 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.isplainobject@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz#9a8238ae16b200432960cd7346512d0123fbf4c5" @@ -9904,32 +9937,29 @@ object-inspect@^1.1.0: version "1.5.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.5.0.tgz#9d876c11e40f485c79215670281b767488f9bfe3" +object-inspect@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" + integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== + object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" -object-keys@^1.0.10, object-keys@^1.0.8, object-keys@^1.0.9: - version "1.0.11" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" - object-keys@^1.0.11, object-keys@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" +object-keys@^1.0.8, object-keys@^1.0.9: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" dependencies: isobject "^3.0.0" -object.assign@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.0" - object-keys "^1.0.10" - object.assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" @@ -11909,6 +11939,11 @@ react-input-autosize@^2.1.0: dependencies: prop-types "^15.5.8" +react-is@^16.6.1: + version "16.7.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa" + integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g== + react-lazy-load@^3.0.12: version "3.0.13" resolved "https://registry.yarnpkg.com/react-lazy-load/-/react-lazy-load-3.0.13.tgz#3b0a92d336d43d3f0d73cbe6f35b17050b08b824" @@ -13584,6 +13619,15 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string.prototype.trim@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" + integrity sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.0" + function-bind "^1.0.2" + string_decoder@^1.0.0, string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"