diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap index ba4575b179..6b44bc63d0 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap @@ -56,7 +56,7 @@ exports[` DdgNodeContent.getNodeRenderer() returns a DdgNodeContent.getNodeRenderer() returns a `; +exports[` DdgNodeContent.getNodeRenderer() returns a focal 1`] = ` +
+
+
+

+ +

+
+ +
+
+
+
+ + + + View traces + + +
+
+`; + exports[` omits the operation if it is null 1`] = `
', () => { const vertexKey = 'some-key'; const service = 'some-service'; const operation = 'some-operation'; + const props = { + focalNodeUrl: 'some-url', + getVisiblePathElems: jest.fn(), + isFocalNode: false, + operation, + setViewModifier: jest.fn(), + service, + vertexKey, + }; let wrapper; - let props; beforeEach(() => { - props = { - vertexKey, - service, - operation, - isFocalNode: false, - focalNodeUrl: 'some-url', - setViewModifier: jest.fn(), - }; + props.getVisiblePathElems.mockReset(); wrapper = shallow(); }); @@ -72,23 +76,191 @@ describe('', () => { expect(wrapper).toMatchSnapshot(); }); - describe('DdgNodeContent.getNodeRenderer()', () => { - let ddgVertex; + describe('measureNode', () => { + it('returns twice the RADIUS with a buffer for svg border', () => { + const diameterWithBuffer = 2 * RADIUS + 2; + expect(DdgNodeContent.measureNode()).toEqual({ + height: diameterWithBuffer, + width: diameterWithBuffer, + }); + }); + }); + + describe('viewTraces', () => { + const click = () => + wrapper + .find('.DdgNodeContent--actionsItem') + .at(1) + .simulate('click'); + const pad = num => `000${num}`.slice(-4); + const mockReturn = ids => + props.getVisiblePathElems.mockReturnValue(ids.map(traceIDs => ({ memberOf: { traceIDs } }))); + const calcIdxWithinLimit = arr => Math.floor(0.75 * arr.length); + const falsifyDuplicateAndMock = ids => { + const withFalsyAndDuplicate = ids.map(arr => arr.slice()); + withFalsyAndDuplicate[0].splice( + calcIdxWithinLimit(withFalsyAndDuplicate[0]), + 0, + withFalsyAndDuplicate[1][calcIdxWithinLimit(withFalsyAndDuplicate[1])], + '' + ); + withFalsyAndDuplicate[1].splice( + calcIdxWithinLimit(withFalsyAndDuplicate[1]), + 0, + withFalsyAndDuplicate[0][calcIdxWithinLimit(withFalsyAndDuplicate[0])], + '' + ); + mockReturn(withFalsyAndDuplicate); + }; + const makeIDsAndMock = (idCounts, makeID = count => `test traceID${count}`) => { + let idCount = 0; + const ids = idCounts.map(count => { + const rv = []; + for (let i = 0; i < count; i++) { + rv.push(makeID(pad(idCount++))); + } + return rv; + }); + mockReturn(ids); + return ids; + }; + let getSearchUrlSpy; + const lastIDs = () => getSearchUrlSpy.mock.calls[getSearchUrlSpy.mock.calls.length - 1][0].traceID; + let originalOpen; + + beforeAll(() => { + originalOpen = window.open; + window.open = jest.fn(); + getSearchUrlSpy = jest.spyOn(getSearchUrl, 'getUrl'); + }); beforeEach(() => { - ddgVertex = { - isFocalNode: false, - key: 'some-key', - operation: 'the-operation', - service: 'the-service', - }; + window.open.mockReset(); + }); + + afterAll(() => { + window.open = originalOpen; + }); + + it('no-ops if there are no elems for key', () => { + props.getVisiblePathElems.mockReturnValue(); + click(); + expect(window.open).not.toHaveBeenCalled(); + }); + + it('opens new tab viewing single traceID from single elem', () => { + const ids = makeIDsAndMock([1]); + click(); + + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + expect(props.getVisiblePathElems).toHaveBeenCalledTimes(1); + expect(props.getVisiblePathElems).toHaveBeenCalledWith(vertexKey); + }); + + it('opens new tab viewing multiple traceIDs from single elem', () => { + const ids = makeIDsAndMock([3]); + click(); + + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + }); + + it('opens new tab viewing multiple traceIDs from multiple elems', () => { + const ids = makeIDsAndMock([3, 2]); + click(); + + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + }); + + it('ignores falsy and duplicate IDs', () => { + const ids = makeIDsAndMock([3, 3]); + falsifyDuplicateAndMock(ids); + click(); + + expect(lastIDs().sort()).toEqual([].concat(...ids).sort()); + }); + + describe('MAX_LINKED_TRACES', () => { + const ids = makeIDsAndMock([MAX_LINKED_TRACES, MAX_LINKED_TRACES, 1]); + const expected = [ + ...ids[0].slice(MAX_LINKED_TRACES / 2 + 1), + ...ids[1].slice(MAX_LINKED_TRACES / 2 + 1), + ids[2][0], + ].sort(); + + it('limits link to only include MAX_LINKED_TRACES, taking equal from each pathElem', () => { + mockReturn(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); + + it('does not count falsy and duplicate IDs towards MAX_LINKED_TRACES', () => { + falsifyDuplicateAndMock(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); }); + describe('MAX_LENGTH', () => { + const effectiveMaxLength = MAX_LENGTH - MIN_LENGTH; + const TARGET_ID_COUNT = 31; + const paddingLength = Math.floor(effectiveMaxLength / TARGET_ID_COUNT) - PARAM_NAME_LENGTH; + const idPadding = 'x'.repeat(paddingLength - pad(0).length); + const ids = makeIDsAndMock([TARGET_ID_COUNT, TARGET_ID_COUNT, 1], num => `${idPadding}${num}`); + const expected = [ + ...ids[0].slice(TARGET_ID_COUNT / 2 + 1), + ...ids[1].slice(TARGET_ID_COUNT / 2 + 1), + ids[2][0], + ].sort(); + + it('limits link to only include MAX_LENGTH, taking equal from each pathElem', () => { + mockReturn(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); + + it('does not count falsy and duplicate IDs towards MAX_LEN', () => { + falsifyDuplicateAndMock(ids); + click(); + + expect(lastIDs().sort()).toEqual(expected); + }); + }); + }); + + describe('DdgNodeContent.getNodeRenderer()', () => { + const ddgVertex = { + isFocalNode: false, + key: 'some-key', + operation: 'the-operation', + service: 'the-service', + }; + const noOp = () => {}; + it('returns a ', () => { - const ddgNode = DdgNodeContent.getNodeRenderer(() => undefined)(ddgVertex); + const ddgNode = DdgNodeContent.getNodeRenderer( + noOp, + noOp, + EDdgDensity.PreventPathEntanglement, + true, + 'testBaseUrl', + { maxDuration: '100ms' } + )(ddgVertex); expect(ddgNode).toBeDefined(); expect(shallow(ddgNode)).toMatchSnapshot(); expect(ddgNode.type).toBe(DdgNodeContent); }); + + it('returns a focal ', () => { + const focalNode = DdgNodeContent.getNodeRenderer(noOp, noOp)({ + ...ddgVertex, + isFocalNode: true, + }); + expect(focalNode).toBeDefined(); + expect(shallow(focalNode)).toMatchSnapshot(); + expect(focalNode.type).toBe(DdgNodeContent); + }); }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index 18901e13a8..589aaf2842 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -90,7 +90,7 @@ export default class DdgNodeContent extends React.PureComponent { const urlIds: Set = new Set(); let currLength = MIN_LENGTH; // Because there is a limit on traceIDs, attempt to get some from each elem rather than all from one. - const allIDs = elems.map(({ memberOf: m }) => m.traceIDs.slice()); + const allIDs = elems.map(({ memberOf }) => memberOf.traceIDs.slice()); while (allIDs.length) { const ids = allIDs.shift(); if (ids && ids.length) { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/__snapshots__/getNodeRenderers.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/Graph/__snapshots__/getNodeRenderers.test.js.snap new file mode 100644 index 0000000000..ffa7697a15 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/__snapshots__/getNodeRenderers.test.js.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, with .is-hovered, with .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, with .is-hovered, with .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, with .is-hovered, without .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, with .is-hovered, without .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, without .is-hovered, with .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, without .is-hovered, with .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, without .is-hovered, without .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle with .is-findMatch, without .is-hovered, without .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, with .is-hovered, with .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, with .is-hovered, with .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, with .is-hovered, without .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, with .is-hovered, without .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, without .is-hovered, with .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, without .is-hovered, with .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, without .is-hovered, without .is-pathHovered, and with .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorBorder returns circle without .is-findMatch, without .is-hovered, without .is-pathHovered, and without .is-focalNode 1`] = ` + +`; + +exports[`getNodeRenderers vectorFindColorBand returns circle with correct size and className 1`] = ` + +`; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/getNodeRenderers.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Graph/getNodeRenderers.test.js new file mode 100644 index 0000000000..cbb0d0334c --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/getNodeRenderers.test.js @@ -0,0 +1,99 @@ +// 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 { shallow } from 'enzyme'; + +import getNodeRenderers from './getNodeRenderers'; + +import { EViewModifier } from '../../../model/ddg/types'; + +describe('getNodeRenderers', () => { + const key = 'test vertex key'; + const lv = { + vertex: { + key, + }, + height: 200, + width: 100, + }; + const focalLv = { ...lv, vertex: { key, isFocalNode: true } }; + + describe('vectorBorder', () => { + // Short, DRY way to calculate with (w/) versus (v) without (w/o) + const wvwo = someBoolean => (someBoolean ? 'with' : 'without'); + + [true, false].forEach(findMatch => { + [true, false].forEach(hovered => { + [true, false].forEach(pathHovered => { + [true, false].forEach(focalNode => { + it(`returns circle ${wvwo(findMatch)} .is-findMatch,\t${wvwo(hovered)} .is-hovered,\t${wvwo( + pathHovered + )} .is-pathHovered,\tand ${wvwo(focalNode)} .is-focalNode`, () => { + const testLv = focalNode ? focalLv : lv; + const findMatches = new Set(findMatch ? [testLv.vertex] : undefined); + const vm = + // eslint-disable-next-line no-bitwise + (hovered ? EViewModifier.Hovered : 0) | (pathHovered ? EViewModifier.PathHovered : 0); + const vms = new Map([[key, vm]]); + expect(shallow(getNodeRenderers(findMatches, vms).vectorBorder(testLv))).toMatchSnapshot(); + }); + }); + }); + }); + }); + }); + + describe('htmlEmphasis', () => { + it('returns null if vertex is neither a findMatch nor focalNode', () => { + expect(getNodeRenderers(new Set(), new Map()).htmlEmphasis(lv)).toBe(null); + }); + + it('returns div with .is-findMatch if vertex is a findMatch', () => { + const wrapper = shallow(getNodeRenderers(new Set([lv.vertex]), new Map()).htmlEmphasis(lv)); + expect(wrapper.hasClass('is-findMatch')).toBe(true); + expect(wrapper.type()).toBe('div'); + }); + + it('returns div with .is-focalNode if vertex is a focalNode', () => { + const wrapper = shallow(getNodeRenderers(new Set(), new Map()).htmlEmphasis(focalLv)); + expect(wrapper.hasClass('is-focalNode')).toBe(true); + expect(wrapper.type()).toBe('div'); + }); + + it('returns div with .is-findMatch and .is-focalNode if vertex is a focalNode and a findMatch', () => { + const wrapper = shallow(getNodeRenderers(new Set([focalLv.vertex]), new Map()).htmlEmphasis(focalLv)); + expect(wrapper.hasClass('is-findMatch')).toBe(true); + expect(wrapper.hasClass('is-focalNode')).toBe(true); + expect(wrapper.type()).toBe('div'); + }); + }); + + describe('vectorFindColorBand', () => { + it('is null if findMatches set is empty', () => { + expect(getNodeRenderers(new Set(), new Map()).vectorFindColorBand).toBe(null); + }); + + it('returns null if provided vertex is not in set', () => { + expect( + getNodeRenderers(new Set([{ vertex: { key: `not-${key}` } }]), new Map()).vectorFindColorBand(lv) + ).toBe(null); + }); + + it('returns circle with correct size and className', () => { + expect( + shallow(getNodeRenderers(new Set([lv.vertex]), new Map()).vectorFindColorBand(lv)) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/getSetOnEdge.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Graph/getSetOnEdge.test.js new file mode 100644 index 0000000000..d9ae3072b0 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/getSetOnEdge.test.js @@ -0,0 +1,54 @@ +// 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 getSetOnEdge, { baseCase, matchMiss } from './getSetOnEdge'; + +import { getEdgeId } from '../../../model/ddg/GraphModel'; +import { EViewModifier } from '../../../model/ddg/types'; + +describe('getSetOnEdge', () => { + const makeEdge = (from, to) => ({ + edge: { from, to }, + }); + const hovered = makeEdge('test', 'hovered'); + const notHovered = makeEdge('not', 'hovered'); + const miss = makeEdge('test', 'miss'); + const vms = new Map([ + [getEdgeId(hovered.edge.from, hovered.edge.to), EViewModifier.PathHovered], + [getEdgeId(notHovered.edge.from, notHovered.edge.to), EViewModifier.emphasized], + ]); + const fakeUtils = { + getGlobalId: id => id, + }; + + it('returns base case when given empty map', () => { + expect(getSetOnEdge(new Map())).toBe(baseCase); + }); + + it('returns function that returns miss if id is not in map as hovered', () => { + const setOnEdge = getSetOnEdge(vms); + + expect(setOnEdge(miss, fakeUtils)).toBe(matchMiss); + expect(setOnEdge(notHovered, fakeUtils)).toBe(matchMiss); + }); + + it('returns function that returns hovered edge class and hovered arrow if id is in map as hovered', () => { + const setOnEdge = getSetOnEdge(vms); + + expect(setOnEdge(hovered, fakeUtils)).toEqual({ + className: expect.stringContaining('Hovered'), + markerEnd: expect.stringContaining('hovered'), + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/getSetOnEdge.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/getSetOnEdge.tsx index 316cb4e032..062a2d2c39 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/getSetOnEdge.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/getSetOnEdge.tsx @@ -16,10 +16,12 @@ import { TRendererUtils } from '@jaegertracing/plexus/lib/Digraph/types'; import { TLayoutEdge } from '@jaegertracing/plexus/lib/types'; import { getEdgeId } from '../../../model/ddg/GraphModel'; + import { EViewModifier } from '../../../model/ddg/types'; -const baseCase = { className: 'Ddg--Edge' }; -const matchMiss = { className: 'Ddg--Edge', markerEnd: null }; +// exported for tests +export const baseCase = { className: 'Ddg--Edge' }; +export const matchMiss = { className: 'Ddg--Edge', markerEnd: null }; export default function getSetOnEdge(edgesViewModifiers: Map) { if (!edgesViewModifiers.size) { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.test.js index 37a2188f81..964ad864f6 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.test.js @@ -16,7 +16,9 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import { Digraph, LayoutManager } from '@jaegertracing/plexus'; -import Graph from './index'; +import Graph, { setOnEdgesContainer, setOnVectorBorderContainerWithViewModifiers } from './index'; + +import { EViewModifier } from '../../../model/ddg/types'; describe('', () => { const vertices = [...new Array(10)].map((_, i) => ({ key: `key${i}` })); @@ -46,12 +48,45 @@ describe('', () => { }); describe('render', () => { + let wrapper; + let plexusGraph; + + beforeEach(() => { + wrapper = shallow(); + plexusGraph = wrapper.find(Digraph); + }); + it('renders provided edges and vertices', () => { - const wrapper = shallow(); - const plexusGraph = wrapper.find(Digraph); expect(plexusGraph.prop('edges')).toEqual(edges); expect(plexusGraph.prop('vertices')).toEqual(vertices); expect(wrapper).toMatchSnapshot(); }); + + it('de-emphasizes non-matching edges iff edgeVMs are present', () => { + expect(plexusGraph.prop('layers')[3].setOnContainer).toBe(setOnEdgesContainer.withoutViewModifiers); + + wrapper.setProps({ edgesViewModifiers: new Map([[0, EViewModifier.Emphasized]]) }); + plexusGraph = wrapper.find(Digraph); + expect(plexusGraph.prop('layers')[3].setOnContainer).toBe(setOnEdgesContainer.withViewModifiers); + }); + + it('de-emphasizes non-matching vertices iff vertexVMs are present', () => { + expect(plexusGraph.prop('layers')[2].setOnContainer).toBe( + Digraph.propsFactories.scaleStrokeOpacityStrongest + ); + + wrapper.setProps({ verticesViewModifiers: new Map([[0, EViewModifier.Emphasized]]) }); + plexusGraph = wrapper.find(Digraph); + expect(plexusGraph.prop('layers')[2].setOnContainer).toBe(setOnVectorBorderContainerWithViewModifiers); + }); + }); + + describe('clean up', () => { + it('stops LayoutManager before unmounting', () => { + const wrapper = shallow(); + const stopAndReleaseSpy = jest.spyOn(wrapper.instance().layoutManager, 'stopAndRelease'); + wrapper.unmount(); + expect(stopAndReleaseSpy).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx index a5deb6932d..2be3c11c34 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx @@ -40,14 +40,18 @@ type TProps = { verticesViewModifiers: Map; }; +// exported for tests // The dichotomy between w/ & w/o VMs assumes that any edge VM neccesitates unmodified edges are de-emphasized -const setOnEdgesContainer: Record>> = { +export const setOnEdgesContainer: Record>> = { withViewModifiers: [{ className: 'Ddg--Edges is-withViewModifiers' }], withoutViewModifiers: [Digraph.propsFactories.scaleStrokeOpacityStrongest, { className: 'Ddg--Edges' }], }; +// exported for tests // The dichotomy between w/ & w/o VMs assumes that any vertex VM makes unmodified vertices de-emphasized -const setOnVectorBorderContainerWithViewModifiers: TSetProps> = { +export const setOnVectorBorderContainerWithViewModifiers: TSetProps< + TFromGraphStateFn +> = { className: 'DdgVectorBorders is-withViewModifiers', }; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Header/ChevronDown.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Header/ChevronDown.test.js new file mode 100644 index 0000000000..8bf7e4721a --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/Header/ChevronDown.test.js @@ -0,0 +1,37 @@ +// 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 React from 'react'; +import { shallow } from 'enzyme'; +import IoChevronDown from 'react-icons/lib/io/chevron-down'; + +import ChevronDown from './ChevronDown'; + +describe('ChevronDown', () => { + it('renders with provided className and style', () => { + const className = 'testClassName'; + const style = { + border: 'black solid 1px', + }; + const wrapper = shallow(); + + expect(wrapper.hasClass(className)).toBe(true); + expect(wrapper.find(IoChevronDown).prop('style')).toBe(style); + }); + + it('does not add `undefined` as a className when not given a className', () => { + const wrapper = shallow(); + expect(wrapper.hasClass('undefined')).toBe(false); + }); +}); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Header/LayoutSettings/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Header/LayoutSettings/index.test.js new file mode 100644 index 0000000000..3da62f824f --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/Header/LayoutSettings/index.test.js @@ -0,0 +1,102 @@ +// 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 React from 'react'; +import { Checkbox, Radio, Popover } from 'antd'; +import { shallow } from 'enzyme'; + +import LayoutSettings, { densityOptions } from '.'; +import * as track from '../../index.track'; + +import { EDdgDensity } from '../../../../model/ddg/types'; + +describe('LayoutSettings', () => { + const props = { + density: EDdgDensity.PreventPathEntanglement, + setDensity: jest.fn(), + showOperations: true, + toggleShowOperations: jest.fn(), + }; + const densityIdx = densityOptions.findIndex(({ option }) => option === props.density); + + const getWrapper = overrideProps => { + const content = shallow() + .find(Popover) + .prop('content'); + return shallow(content); + }; + let trackDensityChangeSpy; + let trackToggleShowOpSpy; + + beforeAll(() => { + trackDensityChangeSpy = jest.spyOn(track, 'trackDensityChange'); + trackToggleShowOpSpy = jest.spyOn(track, 'trackToggleShowOp'); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders each densityOption', () => { + const radios = getWrapper().find(Radio); + + expect(radios.length).toBe(densityOptions.length); + expect(Array.from(radios).findIndex(radio => radio.props.checked)).toBe(densityIdx); + }); + + it('updates density and tracks its change', () => { + const newIdx = 1; + const newDensity = densityOptions[newIdx].option; + getWrapper() + .find(Radio) + .at(newIdx) + .simulate('change', { target: { value: newDensity } }); + expect(props.setDensity).toHaveBeenCalledWith(newDensity); + expect(trackDensityChangeSpy).toHaveBeenCalledWith(props.density, newDensity, densityOptions); + }); + + it('no-ops if current density is selected', () => { + getWrapper() + .find(Radio) + .at(densityIdx) + .simulate('change', { target: { value: props.density } }); + expect(props.setDensity).not.toHaveBeenCalled(); + expect(trackDensityChangeSpy).not.toHaveBeenCalled(); + }); + + it('renders showOperations checkbox', () => { + expect( + getWrapper() + .find(Checkbox) + .prop('checked') + ).toBe(props.showOperations); + + const showOperations = !props.showOperations; + expect( + getWrapper({ showOperations }) + .find(Checkbox) + .prop('checked') + ).toBe(showOperations); + }); + + it('toggles showOperation and tracks its toggle', () => { + const checked = !props.showOperations; + getWrapper() + .find(Checkbox) + .simulate('change', { target: { checked } }); + + expect(props.toggleShowOperations).toHaveBeenCalledWith(checked); + expect(trackToggleShowOpSpy).toHaveBeenCalledWith(checked); + }); +}); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Header/LayoutSettings/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Header/LayoutSettings/index.tsx index 12ffd22313..d2c4ef6dbf 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Header/LayoutSettings/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Header/LayoutSettings/index.tsx @@ -36,7 +36,8 @@ const cssCls = (() => { return (namePart?: string) => (namePart ? `${CLASSNAME_PREFIX}--${namePart}` : CLASSNAME_PREFIX); })(); -const densityOptions = [ +// exported for tests +export const densityOptions = [ { option: EDdgDensity.MostConcise, title: 'One node per resource', diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.test.js b/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.test.js index 4bbd501a05..78cae3d562 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.test.js @@ -45,7 +45,7 @@ describe('', () => { }); it('renders without is-invalid when not required and without a value', () => { - wrapper.setProps({ required: false }); + wrapper.setProps({ required: undefined }); expect(wrapper).toMatchSnapshot(); }); @@ -67,6 +67,11 @@ describe('', () => { wrapper.setProps({ placeholder: true, value }); expect(wrapper.find(BreakableText).prop('text')).toBe(value); }); + + it('does not render default placeholder if placeholder is disabled', () => { + wrapper.setProps({ placeholder: undefined }); + expect(wrapper.find(BreakableText).prop('text')).toBe(''); + }); }); it('allows the filtered list to set values', () => { @@ -85,6 +90,28 @@ describe('', () => { expect(wrapper.state('popoverVisible')).toBe(false); }); + it('hides the popover when clicking outside of the open popover', () => { + let mouseWithin = false; + wrapper.setState({ popoverVisible: true }); + wrapper.instance().listRef = { + current: { + focusInput: () => {}, + isMouseWithin: () => mouseWithin, + }, + }; + wrapper.instance().onBodyClicked(); + expect(wrapper.state('popoverVisible')).toBe(false); + + wrapper.setState({ popoverVisible: true }); + mouseWithin = true; + wrapper.instance().onBodyClicked(); + expect(wrapper.state('popoverVisible')).toBe(true); + + wrapper.instance().listRef = {}; + wrapper.instance().onBodyClicked(); + expect(wrapper.state('popoverVisible')).toBe(true); + }); + it('controls the visibility of the popover', () => { expect(wrapper.state('popoverVisible')).toBe(false); const popover = wrapper.find(Popover); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.tsx index 3c8088e1b7..9b2912898d 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Header/NameSelector.tsx @@ -71,7 +71,7 @@ export default class NameSelector extends React.PureComponent { this.changeVisible(false); }; - onPopoverVisbileChanged = (popoverVisible: boolean) => { + onPopoverVisibleChanged = (popoverVisible: boolean) => { this.changeVisible(popoverVisible); }; @@ -92,7 +92,7 @@ export default class NameSelector extends React.PureComponent { return ( { describe('DeepDependencyGraphPageImpl', () => { const props = { - history: { - push: jest.fn(), - }, + addViewModifier: jest.fn(), + fetchDeepDependencyGraph: () => {}, + fetchServices: jest.fn(), + fetchServiceOperations: jest.fn(), graph: { getVisible: () => ({ edges: [], vertices: [], }), getHiddenUiFindMatches: () => new Set(), + getVertexVisiblePathElems: jest.fn(), getVisibleUiFindMatches: () => new Set(), + getVisWithVertices: jest.fn(), }, - fetchServices: jest.fn(), - fetchServiceOperations: jest.fn(), graphState: { model: { distanceToPathElems: new Map(), @@ -50,7 +53,11 @@ describe('DeepDependencyGraphPage', () => { state: fetchedState.DONE, viewModifiers: new Map(), }, + history: { + push: jest.fn(), + }, operationsForService: {}, + removeViewModifierFromIndices: jest.fn(), urlState: { start: 'testStart', end: 'testEnd', @@ -58,7 +65,6 @@ describe('DeepDependencyGraphPage', () => { operation: 'testOperation', visEncoding: 'testVisKey', }, - fetchDeepDependencyGraph: () => {}, }; const ddgPageImpl = new DeepDependencyGraphPageImpl(props); @@ -75,7 +81,7 @@ describe('DeepDependencyGraphPage', () => { expect(props.fetchServices).toHaveBeenCalledTimes(1); }); - it('fetches services if service is provided without operations', () => { + it('fetches operations if service is provided without operations', () => { const { service, ...urlState } = props.urlState; new DeepDependencyGraphPageImpl({ ...props, urlState }); // eslint-disable-line no-new expect(props.fetchServiceOperations).not.toHaveBeenCalled(); @@ -244,6 +250,180 @@ describe('DeepDependencyGraphPage', () => { expect(props.fetchServiceOperations).toHaveBeenCalledTimes(callCount); }); }); + + describe('showVertices', () => { + const vertices = ['vertex0', 'vertex1']; + const mockVisWithVertices = 'mockVisWithVertices'; + + beforeAll(() => { + props.graph.getVisWithVertices.mockReturnValue(mockVisWithVertices); + }); + + it('updates url with visEncoding calculated by graph', () => { + ddgPageImpl.showVertices(vertices); + expect(props.graph.getVisWithVertices).toHaveBeenLastCalledWith( + vertices, + props.urlState.visEncoding + ); + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { visEncoding: mockVisWithVertices }), + undefined + ); + }); + + it('no-ops if not given graph', () => { + const { graph: _, ...propsWithoutGraph } = props; + const ddg = new DeepDependencyGraphPageImpl(propsWithoutGraph); + const { length: callCount } = getUrlSpy.mock.calls; + ddg.showVertices(vertices); + expect(getUrlSpy.mock.calls.length).toBe(callCount); + }); + }); + + describe('setDensity', () => { + it('updates url with provided density', () => { + const density = EDdgDensity.PreventPathEntanglement; + ddgPageImpl.setDensity(density); + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { density }), + undefined + ); + }); + }); + + describe('toggleShowOperations', () => { + it('updates url with provided boolean', () => { + let showOp = true; + ddgPageImpl.toggleShowOperations(showOp); + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { showOp }), + undefined + ); + + showOp = false; + ddgPageImpl.toggleShowOperations(showOp); + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { showOp }), + undefined + ); + }); + }); + }); + + describe('view modifiers', () => { + const vertexKey = 'test vertex key'; + const visibilityIndices = ['visId0', 'visId1', 'visId2']; + const targetVM = EViewModifier.Emphasized; + + beforeAll(() => { + props.graph.getVertexVisiblePathElems.mockReturnValue( + visibilityIndices.map(visibilityIdx => ({ visibilityIdx })) + ); + }); + + beforeEach(() => { + props.addViewModifier.mockReset(); + props.graph.getVertexVisiblePathElems.mockClear(); + props.removeViewModifierFromIndices.mockReset(); + }); + + it('adds given viewModifier to specified pathElems', () => { + ddgPageImpl.setViewModifier(vertexKey, targetVM, true); + expect(props.addViewModifier).toHaveBeenLastCalledWith({ + operation: props.urlState.operation, + service: props.urlState.service, + viewModifier: targetVM, + visibilityIndices, + end: 0, + start: 0, + }); + expect(props.graph.getVertexVisiblePathElems).toHaveBeenCalledWith( + vertexKey, + props.urlState.visEncoding + ); + }); + + it('removes given viewModifier from specified pathElems', () => { + ddgPageImpl.setViewModifier(vertexKey, targetVM, false); + expect(props.removeViewModifierFromIndices).toHaveBeenCalledWith({ + operation: props.urlState.operation, + service: props.urlState.service, + viewModifier: targetVM, + visibilityIndices, + end: 0, + start: 0, + }); + expect(props.graph.getVertexVisiblePathElems).toHaveBeenCalledWith( + vertexKey, + props.urlState.visEncoding + ); + }); + + it('throws error if given absent vertexKey', () => { + props.graph.getVertexVisiblePathElems.mockReturnValueOnce(undefined); + const absentVertexKey = 'absentVertexKey'; + expect(() => + ddgPageImpl.setViewModifier(absentVertexKey, EViewModifier.emphasized, true) + ).toThrowError(new RegExp(`Invalid vertex key.*${absentVertexKey}`)); + }); + + it('no-ops if not given dispatch fn or graph or operation or service', () => { + const { addViewModifier: _add, ...propsWithoutAdd } = props; + const ddgWithoutAdd = new DeepDependencyGraphPageImpl(propsWithoutAdd); + ddgWithoutAdd.setViewModifier(vertexKey, EViewModifier.emphasized, true); + expect(props.graph.getVertexVisiblePathElems).not.toHaveBeenCalled(); + + const { removeViewModifierFromIndices: _remove, ...propsWithoutRemove } = props; + const ddgWithoutRemove = new DeepDependencyGraphPageImpl(propsWithoutRemove); + ddgWithoutRemove.setViewModifier(vertexKey, EViewModifier.emphasized, false); + expect(props.graph.getVertexVisiblePathElems).not.toHaveBeenCalled(); + + const { graph: _graph, ...propsWithoutGraph } = props; + const ddgWithoutGraph = new DeepDependencyGraphPageImpl(propsWithoutGraph); + ddgWithoutGraph.setViewModifier(vertexKey, EViewModifier.emphasized, true); + expect(props.graph.getVertexVisiblePathElems).not.toHaveBeenCalled(); + + const { + urlState: { operation: _operation, ...urlStateWithoutOperation }, + ...propsWithoutOperation + } = props; + propsWithoutOperation.urlState = urlStateWithoutOperation; + const ddgWithoutOperation = new DeepDependencyGraphPageImpl(propsWithoutGraph); + ddgWithoutOperation.setViewModifier(vertexKey, EViewModifier.emphasized, true); + expect(props.graph.getVertexVisiblePathElems).not.toHaveBeenCalled(); + + const { + urlState: { service: _service, ...urlStateWithoutService }, + ...propsWithoutService + } = props; + propsWithoutService.urlState = urlStateWithoutService; + const ddgWithoutService = new DeepDependencyGraphPageImpl(propsWithoutGraph); + ddgWithoutService.setViewModifier(vertexKey, EViewModifier.emphasized, true); + expect(props.graph.getVertexVisiblePathElems).not.toHaveBeenCalled(); + }); + }); + + describe('getVisiblePathElems', () => { + const vertexKey = 'test vertex key'; + const mockVisibleElems = 'mock visible elems'; + + beforeAll(() => { + props.graph.getVertexVisiblePathElems.mockReturnValue(mockVisibleElems); + }); + + it('returns visible pathElems', () => { + expect(ddgPageImpl.getVisiblePathElems(vertexKey)).toBe(mockVisibleElems); + expect(props.graph.getVertexVisiblePathElems).toHaveBeenLastCalledWith( + vertexKey, + props.urlState.visEncoding + ); + }); + + it('no-ops if not given graph', () => { + const { graph: _, ...propsWithoutGraph } = props; + const ddg = new DeepDependencyGraphPageImpl(propsWithoutGraph); + expect(() => ddg.getVisiblePathElems(vertexKey)).not.toThrowError(); + }); }); describe('render', () => { @@ -330,6 +510,23 @@ describe('DeepDependencyGraphPage', () => { expect(wrapper.find(Header).prop('uiFindCount')).toBe(1); expect(wrapper.find(Header).prop('hiddenUiFindMatches').size).toBe(vertices.length - 1); }); + + it('passes correct operations to Header', () => { + const wrapper = shallow( + + ); + expect(wrapper.find(Header).prop('operations')).toBe(undefined); + + const operationsForService = { + [props.urlState.service]: ['testOperation0', 'testOperation1'], + }; + wrapper.setProps({ operationsForService }); + expect(wrapper.find(Header).prop('operations')).toBe(operationsForService[props.urlState.service]); + + const { service: _, ...urlStateWithoutService } = props.urlState; + wrapper.setProps({ urlState: urlStateWithoutService }); + expect(wrapper.find(Header).prop('operations')).toBe(undefined); + }); }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 50734cce7d..6a86691192 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -108,7 +108,6 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { } componentWillReceiveProps(nextProps: TProps) { - /* istanbul ignore next */ DeepDependencyGraphPageImpl.fetchModelIfStale(nextProps); } @@ -154,8 +153,9 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { setViewModifier = (vertexKey: string, viewModifier: EViewModifier, enable: boolean) => { const { addViewModifier, graph, removeViewModifierFromIndices, urlState } = this.props; + const fn = enable ? addViewModifier : removeViewModifierFromIndices; const { service, operation, visEncoding } = urlState; - if (!graph || !service || !operation) { + if (!fn || !graph || !operation || !service) { return; } const pathElems = graph.getVertexVisiblePathElems(vertexKey, visEncoding); @@ -163,17 +163,14 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { throw new Error(`Invalid vertex key to set view modifier for: ${vertexKey}`); } const visibilityIndices = pathElems.map(pe => pe.visibilityIdx); - const fn = enable ? addViewModifier : removeViewModifierFromIndices; - if (fn) { - fn({ - operation, - service, - viewModifier, - visibilityIndices, - end: 0, - start: 0, - }); - } + fn({ + operation, + service, + viewModifier, + visibilityIndices, + end: 0, + start: 0, + }); }; showVertices = (vertices: TDdgVertex[]) => { diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.test.js b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.test.js new file mode 100644 index 0000000000..b03e2f5893 --- /dev/null +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.test.js @@ -0,0 +1,108 @@ +// 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 { makeGraph } from '.'; +import * as testResources from '../sample-paths.test.resources'; +import transformDdgData from '../transformDdgData'; +import { encode } from '../visibility-codec'; + +import { EDdgDensity, EViewModifier } from '../types'; + +describe('getDerivedViewModifiers', () => { + const hiddenLabel = 'hidden'; + const getGraph = () => + makeGraph( + transformDdgData( + testResources.wrap([ + [ + testResources.beforePayloadElem, + testResources.simplePayloadElemMaker(hiddenLabel), + testResources.focalPayloadElem, + testResources.lastPayloadElem, + ], + [ + testResources.firstPayloadElem, + testResources.focalPayloadElem, + testResources.afterPayloadElem, + testResources.midPayloadElem, + testResources.lastPayloadElem, + ], + ]), + testResources.focalPayloadElem + ), + true, + EDdgDensity.OnePerLevel + ); + const graph = getGraph(); + let hiddenKey; + graph.vertices.forEach((_vertex, key) => { + if (key.indexOf(hiddenLabel) !== -1) hiddenKey = key; + }); + const visibleIndices = graph.visIdxToPathElem + .filter(({ operation: { name } }) => name.indexOf(hiddenLabel) === -1) + .map(({ visibilityIdx }) => visibilityIdx); + const visEncoding = encode(visibleIndices); + const vms = new Map(graph.visIdxToPathElem.map((_elem, idx) => [idx, EViewModifier.Emphasized])); + + it('only includes default visible vertices and edges when not given visEncoding', () => { + let tooFarKey; + graph.vertices.forEach((_vertex, key) => { + if (key.indexOf(testResources.lastPayloadElem.service) !== -1) tooFarKey = key; + }); + const { vertices } = graph.getDerivedViewModifiers(undefined, vms); + + expect(vertices.has(tooFarKey)).toBe(false); + expect(vertices.size).toBe(graph.vertices.size - 1); + }); + + it('only includes vertices shown by given visEncoding', () => { + const { vertices } = graph.getDerivedViewModifiers(visEncoding, vms); + + expect(vertices.has(hiddenKey)).toBe(false); + expect(vertices.size).toBe(graph.vertices.size - 1); + }); + + it("adds pathHovered to visible vertices and edges in hovered elem's path", () => { + const idxWithHiddenPathMember = 0; + const { edges, vertices } = graph.getDerivedViewModifiers( + visEncoding, + new Map([[idxWithHiddenPathMember, EViewModifier.Hovered]]) + ); + expect(edges.size).toBe(1); + expect(vertices.size).toBe(3); + }); + + describe('error cases', () => { + it('errors if out of bounds visIdx has a VM', () => { + const outOfBounds = graph.visIdxToPathElem.length; + const outOfBoundsEncoding = encode([...visibleIndices, outOfBounds]); + expect(() => + graph.getDerivedViewModifiers(outOfBoundsEncoding, new Map([[outOfBounds, EViewModifier.Hovered]])) + ).toThrowError(`Invalid vis ids: ${outOfBounds}`); + }); + + it('errors if elem with VM does not have vertex', () => { + const graphMissingVertex = getGraph(); + const idxToDelete = graphMissingVertex.visIdxToPathElem.length - 1; + const elemToDelete = graphMissingVertex.visIdxToPathElem[idxToDelete]; + graphMissingVertex.pathElemToVertex.delete(elemToDelete); + expect(() => + graphMissingVertex.getDerivedViewModifiers( + visEncoding, + new Map([[idxToDelete, EViewModifier.Hovered]]) + ) + ).toThrowError(`Path elem without vertex: ${elemToDelete}`); + }); + }); +}); diff --git a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx index efd157a9f1..fea950cddf 100644 --- a/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx +++ b/packages/jaeger-ui/src/model/ddg/GraphModel/getDerivedViewModifiers.tsx @@ -63,9 +63,8 @@ export default function getDerivedViewModifiers( return; } const hoveredPe = this.visIdxToPathElem[visIdx]; - if (!hoveredPe) { - throw new Error(`Invalid vis index: ${visIdx}`); - } + /* istanbul ignore next : getKeyFromVisIdx would have thrown if visIdx was invalid */ + if (!hoveredPe) throw new Error(`Invalid vis index: ${visIdx}`); const members = hoveredPe.memberOf.members; let lastKey: string | null = null; for (let i = 0; i < members.length; i++) { diff --git a/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js b/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js index 0f31a50cee..3e1ff249dd 100644 --- a/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js +++ b/packages/jaeger-ui/src/model/ddg/sample-paths.test.resources.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -const simplePayloadElemMaker = label => ({ +export const simplePayloadElemMaker = label => ({ operation: `${label}Operation`, service: `${label}Service`, }); @@ -42,11 +42,11 @@ const pathLengthener = path => { return [...prequels, ...path, ...sequels]; }; -const firstPayloadElem = simplePayloadElemMaker('first'); -const beforePayloadElem = simplePayloadElemMaker('before'); -const midPayloadElem = simplePayloadElemMaker('mid'); -const afterPayloadElem = simplePayloadElemMaker('after'); -const lastPayloadElem = simplePayloadElemMaker('last'); +export const firstPayloadElem = simplePayloadElemMaker('first'); +export const beforePayloadElem = simplePayloadElemMaker('before'); +export const midPayloadElem = simplePayloadElemMaker('mid'); +export const afterPayloadElem = simplePayloadElemMaker('after'); +export const lastPayloadElem = simplePayloadElemMaker('last'); export const shortPath = [beforePayloadElem, focalPayloadElem]; export const simplePath = [ diff --git a/packages/jaeger-ui/src/model/ddg/visibility-codec.tsx b/packages/jaeger-ui/src/model/ddg/visibility-codec.tsx index 76dee09a85..edab5de8e4 100644 --- a/packages/jaeger-ui/src/model/ddg/visibility-codec.tsx +++ b/packages/jaeger-ui/src/model/ddg/visibility-codec.tsx @@ -30,7 +30,7 @@ const VISIBILITY_BUCKET_SIZE = 31; * Converts string csv of base36 numbers into array of visible indices. * * @param {string} encoded - base36 csv visibility encoding. - * @returns {number[]} - Visibile indices for this encoding. + * @returns {number[]} - Visible indices for this encoding. */ export const decode: (encoded: string) => number[] = memoize(10)((encoded: string): number[] => { const rv: number[] = []; @@ -74,7 +74,7 @@ function convertAbsoluteIdxToRelativeValues(absIdx: number) { /* * Converts array of visible indices into string csv of base36 numbers. * - * @param {number[]} decoded - Visibile indices for this encoding. + * @param {number[]} decoded - Visible indices for this encoding. * @returns {string} - base36 csv visibility encoding. */ export const encode = (decoded: number[]): string => {