diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer')
15 files changed, 136 insertions, 646 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index 08a9d12d6aa..bbdff07c952 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -33,7 +33,6 @@ import { FlowLocation, Issue as TypeIssue, IssuesByLine, - LinearIssueLocation, Snippet, SnippetGroup, SourceLine, @@ -45,6 +44,7 @@ import { createSnippets, expandSnippet, EXPAND_BY_LINES, + getPrimaryLocation, linesForSnippets, MERGE_DISTANCE } from './utils'; @@ -70,7 +70,6 @@ interface Props { index: number, line: number ) => React.ReactNode; - scroll?: (element: HTMLElement, offset: number) => void; snippetGroup: SnippetGroup; } @@ -83,13 +82,16 @@ interface State { export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> { mounted = false; - rootNodeRef = React.createRef<HTMLDivElement>(); - state: State = { - additionalLines: {}, - highlightedSymbols: [], - loading: false, - snippets: [] - }; + + constructor(props: Props) { + super(props); + this.state = { + additionalLines: {}, + highlightedSymbols: [], + loading: false, + snippets: [] + }; + } componentDidMount() { this.mounted = true; @@ -106,76 +108,13 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone const snippets = createSnippets({ component: snippetGroup.component.key, issue, - locations: snippetGroup.locations + locations: + snippetGroup.locations.length === 0 ? [getPrimaryLocation(issue)] : snippetGroup.locations }); this.setState({ snippets }); } - getNodes(index: number): { wrapper: HTMLElement; table: HTMLElement } | undefined { - const root = this.rootNodeRef.current; - if (!root) { - return undefined; - } - const element = root.querySelector(`#snippet-wrapper-${index}`); - if (!element) { - return undefined; - } - const wrapper = element.querySelector<HTMLElement>('.snippet'); - if (!wrapper) { - return undefined; - } - const table = wrapper.firstChild as HTMLElement; - if (!table) { - return undefined; - } - - return { wrapper, table }; - } - - /* - * Clean after animation - */ - cleanDom(index: number) { - const nodes = this.getNodes(index); - - if (!nodes) { - return; - } - - const { wrapper, table } = nodes; - - table.style.marginTop = ''; - wrapper.style.maxHeight = ''; - } - - setMaxHeight(index: number, value?: number, up = false) { - const nodes = this.getNodes(index); - - if (!nodes) { - return; - } - - const { wrapper, table } = nodes; - - const maxHeight = value !== undefined ? value : table.getBoundingClientRect().height; - - if (up) { - const startHeight = wrapper.getBoundingClientRect().height; - table.style.transition = 'none'; - table.style.marginTop = `${startHeight - maxHeight}px`; - - // animate! - setTimeout(() => { - table.style.transition = ''; - table.style.marginTop = '0px'; - wrapper.style.maxHeight = `${maxHeight + 20}px`; - }, 0); - } else { - wrapper.style.maxHeight = `${maxHeight + 20}px`; - } - } - expandBlock = (snippetIndex: number, direction: ExpandDirection): Promise<void> => { const { branchLike, snippetGroup } = this.props; const { key } = snippetGroup.component; @@ -208,56 +147,22 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone return lineMap; }, {}) ) - .then(newLinesMapped => this.animateBlockExpansion(snippetIndex, direction, newLinesMapped)); - }; - - animateBlockExpansion( - snippetIndex: number, - direction: ExpandDirection, - newLinesMapped: Dict<SourceLine> - ): Promise<void> { - if (this.mounted) { - const { snippets } = this.state; - - const newSnippets = expandSnippet({ - direction, - snippetIndex, - snippets - }); - - const deletedSnippets = newSnippets.filter(s => s.toDelete); - - // set max-height to current height for CSS transitions - deletedSnippets.forEach(s => this.setMaxHeight(s.index)); - this.setMaxHeight(snippetIndex); - - return new Promise(resolve => { - this.setState( - ({ additionalLines, snippets }) => { - const combinedLines = { ...additionalLines, ...newLinesMapped }; - return { - additionalLines: combinedLines, - snippets - }; - }, - () => { - // Set max-height 0 to trigger CSS transitions - deletedSnippets.forEach(s => { - this.setMaxHeight(s.index, 0); - }); - this.setMaxHeight(snippetIndex, undefined, direction === 'up'); - - // Wait for transition to finish before updating dom - setTimeout(() => { - this.setState({ snippets: newSnippets.filter(s => !s.toDelete) }, resolve); - this.cleanDom(snippetIndex); - }, 200); - } - ); + .then(newLinesMapped => { + const newSnippets = expandSnippet({ + direction, + snippetIndex, + snippets + }); + + this.setState(({ additionalLines }) => { + const combinedLines = { ...additionalLines, ...newLinesMapped }; + return { + additionalLines: combinedLines, + snippets: newSnippets.filter(s => !s.toDelete) + }; + }); }); - } - return Promise.resolve(); - } + }; expandComponent = () => { const { branchLike, snippetGroup } = this.props; @@ -356,41 +261,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone ); }; - renderSnippet({ - index, - lastSnippetOfLastGroup, - locationsByLine, - snippet - }: { - index: number; - lastSnippetOfLastGroup: boolean; - locationsByLine: { [line: number]: LinearIssueLocation[] }; - snippet: SourceLine[]; - }) { - return ( - <SnippetViewer - renderAdditionalChildInLine={this.renderIssuesList} - component={this.props.snippetGroup.component} - duplications={this.props.duplications} - duplicationsByLine={this.props.duplicationsByLine} - expandBlock={this.expandBlock} - handleSymbolClick={this.handleSymbolClick} - highlightedLocationMessage={this.props.highlightedLocationMessage} - highlightedSymbols={this.state.highlightedSymbols} - index={index} - issue={this.props.issue} - lastSnippetOfLastGroup={lastSnippetOfLastGroup} - loadDuplications={this.loadDuplications} - locations={this.props.locations} - locationsByLine={locationsByLine} - onLocationSelect={this.props.onLocationSelect} - renderDuplicationPopup={this.renderDuplicationPopup} - scroll={this.props.scroll} - snippet={snippet} - /> - ); - } - render() { const { branchLike, @@ -421,7 +291,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true; return ( - <div className="component-source-container" ref={this.rootNodeRef}> + <> <IssueSourceViewerHeader branchLike={branchLike} expandable={!fullyShown && isFile(snippetGroup.component.q)} @@ -429,28 +299,39 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone onExpand={this.expandComponent} sourceViewerFile={snippetGroup.component} /> + {issue.component === snippetGroup.component.key && issue.textRange === undefined && ( - <div className="padded-top padded-left padded-right"> - <Issue - issue={issue} - onChange={this.props.onIssueChange} - onPopupToggle={this.props.onIssuePopupToggle} - openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined} - selected={true} - /> - </div> + <Issue + issue={issue} + onChange={this.props.onIssueChange} + onPopupToggle={this.props.onIssuePopupToggle} + openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined} + selected={true} + /> )} {snippetLines.map((snippet, index) => ( - <div id={`snippet-wrapper-${snippets[index].index}`} key={snippets[index].index}> - {this.renderSnippet({ - snippet, - index: snippets[index].index, - locationsByLine: includeIssueLocation ? locations : {}, - lastSnippetOfLastGroup: lastSnippetGroup && index === snippets.length - 1 - })} - </div> + <SnippetViewer + key={snippets[index].index} + renderAdditionalChildInLine={this.renderIssuesList} + component={this.props.snippetGroup.component} + duplications={this.props.duplications} + duplicationsByLine={this.props.duplicationsByLine} + expandBlock={this.expandBlock} + handleSymbolClick={this.handleSymbolClick} + highlightedLocationMessage={this.props.highlightedLocationMessage} + highlightedSymbols={this.state.highlightedSymbols} + index={index} + issue={this.props.issue} + lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1} + loadDuplications={this.loadDuplications} + locations={this.props.locations} + locationsByLine={includeIssueLocation ? locations : {}} + onLocationSelect={this.props.onLocationSelect} + renderDuplicationPopup={this.renderDuplicationPopup} + snippet={snippet} + /> ))} - </div> + </> ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx index 0457ce290df..85ab58c3812 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx @@ -51,7 +51,7 @@ import { SourceViewerFile } from '../../../types/types'; import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer'; -import { getPrimaryLocation, groupLocationsByComponent } from './utils'; +import { groupLocationsByComponent } from './utils'; interface Props { branchLike: BranchLike | undefined; @@ -63,7 +63,6 @@ interface Props { onIssueSelect: (issueKey: string) => void; onLoaded?: () => void; onLocationSelect: (index: number) => void; - scroll?: (element: HTMLElement) => void; selectedFlowIndex: number | undefined; } @@ -226,9 +225,8 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop const issuesByComponent = issuesByComponentAndLine(this.props.issues); const locationsByComponent = groupLocationsByComponent(issue, locations, components); - const lastOccurenceOfPrimaryComponent = findLastIndex( - locationsByComponent, - ({ component }) => component.key === issue.component + const lastOccurenceOfPrimaryComponent = findLastIndex(locationsByComponent, ({ component }) => + component ? component.key === issue.component : true ); if (components[issue.component] === undefined) { @@ -236,7 +234,7 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop } return ( - <div> + <> {locationsByComponent.map((snippetGroup, i) => { return ( <SourceViewerContext.Provider @@ -260,45 +258,12 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop onIssuePopupToggle={this.handleIssuePopupToggle} onLocationSelect={this.props.onLocationSelect} renderDuplicationPopup={this.renderDuplicationPopup} - scroll={this.props.scroll} snippetGroup={snippetGroup} /> </SourceViewerContext.Provider> ); })} - - {locationsByComponent.length === 0 && ( - <SourceViewerContext.Provider - value={{ - branchLike: this.props.branchLike, - file: components[issue.component].component - }}> - <ComponentSourceSnippetGroupViewer - branchLike={this.props.branchLike} - duplications={duplications} - duplicationsByLine={duplicationsByLine} - highlightedLocationMessage={this.props.highlightedLocationMessage} - issue={issue} - issuePopup={this.state.issuePopup} - issuesByLine={issuesByComponent[issue.component] || {}} - isLastOccurenceOfPrimaryComponent={true} - lastSnippetGroup={true} - loadDuplications={this.fetchDuplications} - locations={[]} - onIssueChange={this.props.onIssueChange} - onIssueSelect={this.props.onIssueSelect} - onIssuePopupToggle={this.handleIssuePopupToggle} - onLocationSelect={this.props.onLocationSelect} - renderDuplicationPopup={this.renderDuplicationPopup} - scroll={this.props.scroll} - snippetGroup={{ - locations: [getPrimaryLocation(issue)], - ...components[issue.component] - }} - /> - </SourceViewerContext.Provider> - )} - </div> + </> ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css index 4f70cddf426..b06083adb3d 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.css @@ -17,10 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -.source-viewer-header-slim { +.issue-source-viewer-header { padding: 4px 10px; border: 1px solid var(--gray80); background-color: var(--barBackgroundColor); align-items: center; min-height: 25px; + position: sticky; + z-index: 100; + top: 0; + margin-top: 8px; + margin-bottom: -1px; +} + +.issue-source-viewer-header:first-child { + margin-top: 0; } diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx index 69b4465cbe1..7bbee7fcc1e 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx @@ -65,7 +65,10 @@ export default function IssueSourceViewerHeader(props: Props) { const isProjectRoot = q === ComponentQualifier.Project; return ( - <div className="source-viewer-header-slim display-flex-row display-flex-space-between"> + <div + className="issue-source-viewer-header display-flex-row display-flex-space-between" + role="separator" + aria-label={sourceViewerFile.path}> <div className="display-flex-center flex-1"> {displayProjectName && ( <div className="spacer-right"> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css index b3b151ce897..69e8cc7756f 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.css @@ -18,11 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ .snippet { - margin: var(--gridSize) 0; border: 1px solid var(--gray80); overflow-x: auto; - overflow-y: hidden; - transition: max-height 0.2s; +} + +.snippet + .snippet { + margin-top: 8px; } .snippet > div { diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx index 9981db89700..1e21f308b1a 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx @@ -28,7 +28,6 @@ import { optimizeLocationMessage } from '../../../components/SourceViewer/helpers/lines'; import { translate } from '../../../helpers/l10n'; -import { scrollHorizontally } from '../../../helpers/scrolling'; import { Duplication, ExpandDirection, @@ -60,53 +59,12 @@ interface Props { onLocationSelect: (index: number) => void; renderAdditionalChildInLine?: (line: SourceLine) => React.ReactNode | undefined; renderDuplicationPopup: (index: number, line: number) => React.ReactNode; - scroll?: (element: HTMLElement, offset?: number) => void; snippet: SourceLine[]; } export default class SnippetViewer extends React.PureComponent<Props> { - snippetNodeRef: React.RefObject<HTMLDivElement>; - - constructor(props: Props) { - super(props); - this.snippetNodeRef = React.createRef(); - } - - doScroll = (element: HTMLElement) => { - if (this.props.scroll) { - this.props.scroll(element); - } - const parent = this.snippetNodeRef.current as Element; - - if (parent) { - const offset = parent.getBoundingClientRect().width / 2; - - scrollHorizontally(element, { - leftOffset: offset, - rightOffset: offset, - parent - }); - } - }; - - scrollToLastExpandedRow = () => { - if (this.props.scroll) { - const snippetNode = this.snippetNodeRef.current as Element; - if (!snippetNode) { - return; - } - const rows = snippetNode.querySelectorAll('tr'); - const lastRow = rows[rows.length - 1]; - this.props.scroll(lastRow, 100); - } - }; - expandBlock = (direction: ExpandDirection) => () => - this.props.expandBlock(this.props.index, direction).then(() => { - if (direction === 'down') { - this.scrollToLastExpandedRow(); - } - }); + this.props.expandBlock(this.props.index, direction); renderLine({ displayDuplications, @@ -169,7 +127,6 @@ export default class SnippetViewer extends React.PureComponent<Props> { openIssues={false} previousLine={index > 0 ? snippet[index - 1] : undefined} renderDuplicationPopup={this.props.renderDuplicationPopup} - scroll={this.doScroll} secondaryIssueLocations={secondaryIssueLocations} verticalBuffer={verticalBuffer}> {this.props.renderAdditionalChildInLine && this.props.renderAdditionalChildInLine(line)} @@ -203,7 +160,7 @@ export default class SnippetViewer extends React.PureComponent<Props> { Boolean(this.props.loadDuplications) && snippet.some(s => !!s.duplicated); return ( - <div className="source-viewer-code snippet" ref={this.snippetNodeRef}> + <div className="source-viewer-code snippet"> <div> {snippet[0].line > 1 && ( <div className="expand-block expand-block-above"> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx index 12d34757886..0d92fa6b622 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { mount, ReactWrapper, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import { range, times } from 'lodash'; import * as React from 'react'; import { getSources } from '../../../../api/components'; @@ -38,23 +38,6 @@ jest.mock('../../../../api/components', () => ({ getSources: jest.fn().mockResolvedValue([]) })); -/* - * Quick & dirty fix to make the tests pass - * this whole thing should be replaced by RTL tests! - */ -jest.mock('react-router-dom', () => { - const routerDom = jest.requireActual('react-router-dom'); - - function Link() { - return <div>Link</div>; - } - - return { - ...routerDom, - Link - }; -}); - beforeEach(() => { jest.clearAllMocks(); }); @@ -301,121 +284,6 @@ it('should correctly handle lines actions', () => { ); }); -describe('getNodes', () => { - const snippetGroup: SnippetGroup = { - component: mockSourceViewerFile(), - locations: [], - sources: [] - }; - const wrapper = mount<ComponentSourceSnippetGroupViewer>( - <ComponentSourceSnippetGroupViewer - branchLike={mockMainBranch()} - highlightedLocationMessage={{ index: 0, text: '' }} - isLastOccurenceOfPrimaryComponent={true} - issue={mockIssue()} - issuesByLine={{}} - lastSnippetGroup={false} - loadDuplications={jest.fn()} - locations={[]} - onIssueChange={jest.fn()} - onIssueSelect={jest.fn()} - onIssuePopupToggle={jest.fn()} - onLocationSelect={jest.fn()} - renderDuplicationPopup={jest.fn()} - scroll={jest.fn()} - snippetGroup={snippetGroup} - /> - ); - - it('should return undefined if any node is missing', async () => { - await waitAndUpdate(wrapper); - const rootNode = wrapper.instance().rootNodeRef; - mockDom(rootNode.current!); - expect(wrapper.instance().getNodes(0)).toBeUndefined(); - expect(wrapper.instance().getNodes(1)).toBeUndefined(); - expect(wrapper.instance().getNodes(2)).toBeUndefined(); - }); - - it('should return elements if dom is correct', async () => { - await waitAndUpdate(wrapper); - const rootNode = wrapper.instance().rootNodeRef; - mockDom(rootNode.current!); - expect(wrapper.instance().getNodes(3)).not.toBeUndefined(); - }); - - it('should enable cleaning the dom', async () => { - await waitAndUpdate(wrapper); - const rootNode = wrapper.instance().rootNodeRef; - mockDom(rootNode.current!); - - wrapper.instance().cleanDom(3); - const nodes = wrapper.instance().getNodes(3); - expect(nodes!.wrapper.style.maxHeight).toBe(''); - expect(nodes!.table.style.marginTop).toBe(''); - }); -}); - -describe('getHeight', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); - }); - - const snippetGroup: SnippetGroup = { - component: mockSourceViewerFile(), - locations: [], - sources: [] - }; - const wrapper = mount<ComponentSourceSnippetGroupViewer>( - <ComponentSourceSnippetGroupViewer - branchLike={mockMainBranch()} - highlightedLocationMessage={{ index: 0, text: '' }} - isLastOccurenceOfPrimaryComponent={true} - issue={mockIssue()} - issuesByLine={{}} - lastSnippetGroup={false} - loadDuplications={jest.fn()} - locations={[]} - onIssueChange={jest.fn()} - onIssueSelect={jest.fn()} - onIssuePopupToggle={jest.fn()} - onLocationSelect={jest.fn()} - renderDuplicationPopup={jest.fn()} - scroll={jest.fn()} - snippetGroup={snippetGroup} - /> - ); - - it('should set maxHeight to current height', async () => { - await waitAndUpdate(wrapper); - - const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 }); - wrapper.instance().setMaxHeight(0); - - expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;'); - expect(nodes.table.getAttribute('style')).toBeNull(); - }); - - it('should set margin and then maxHeight for a nice upwards animation', async () => { - await waitAndUpdate(wrapper); - - const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 }); - wrapper.instance().setMaxHeight(0, undefined, true); - - expect(nodes.wrapper.getAttribute('style')).toBeNull(); - expect(nodes.table.getAttribute('style')).toBe('transition: none; margin-top: -26px;'); - - jest.runAllTimers(); - - expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;'); - expect(nodes.table.getAttribute('style')).toBe('margin-top: 0px;'); - }); -}); - function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props']> = {}) { const snippetGroup: SnippetGroup = { component: mockSourceViewerFile(), @@ -437,53 +305,8 @@ function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props'] onIssuePopupToggle={jest.fn()} onLocationSelect={jest.fn()} renderDuplicationPopup={jest.fn()} - scroll={jest.fn()} snippetGroup={snippetGroup} {...props} /> ); } - -function mockDom(refNode: HTMLDivElement) { - refNode.querySelector = jest.fn(query => { - const index = query.split('-').pop(); - - switch (index) { - case '0': - return null; - case '1': - return mount(<div />).getDOMNode(); - case '2': - return mount( - <div> - <div className="snippet" /> - </div> - ).getDOMNode(); - case '3': - return mount( - <div> - <div className="snippet"> - <div /> - </div> - </div> - ).getDOMNode(); - default: - return null; - } - }); -} - -function mockDomForSizes( - componentWrapper: ReactWrapper<{}, {}, ComponentSourceSnippetGroupViewer>, - { wrapperHeight = 0, tableHeight = 0 } -) { - const wrapper = mount(<div className="snippet" />).getDOMNode(); - wrapper.getBoundingClientRect = jest.fn().mockReturnValue({ height: wrapperHeight }); - const table = mount(<div />).getDOMNode(); - table.getBoundingClientRect = jest.fn().mockReturnValue({ height: tableHeight }); - componentWrapper.instance().getNodes = jest.fn().mockReturnValue({ - wrapper, - table - }); - return { wrapper, table }; -} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx index 9f6472f08e3..1425e69291c 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewer-test.tsx @@ -28,6 +28,7 @@ import { } from '../../../../helpers/mocks/sources'; import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer'; import CrossComponentSourceViewer from '../CrossComponentSourceViewer'; jest.mock('../../../../api/issues', () => { @@ -102,10 +103,10 @@ it('should handle duplication popup', async () => { const wrapper = shallowRender(); await waitAndUpdate(wrapper); - wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('loadDuplications')( - 'foo', - mockSourceLine() - ); + wrapper + .find(ComponentSourceSnippetGroupViewer) + .props() + .loadDuplications('foo', mockSourceLine()); await waitAndUpdate(wrapper); expect(getDuplications).toHaveBeenCalledWith({ key: 'foo' }); @@ -114,11 +115,10 @@ it('should handle duplication popup', async () => { expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] }); expect( - wrapper.find('ComponentSourceSnippetGroupViewer').prop<Function>('renderDuplicationPopup')( - mockSourceViewerFile(), - 0, - 16 - ) + wrapper + .find(ComponentSourceSnippetGroupViewer) + .props() + .renderDuplicationPopup(mockSourceViewerFile(), 0, 16) ).toMatchSnapshot(); }); @@ -127,14 +127,17 @@ function shallowRender(props: Partial<CrossComponentSourceViewer['props']> = {}) <CrossComponentSourceViewer branchLike={undefined} highlightedLocationMessage={undefined} - issue={mockIssue(true, { key: '1' })} + issue={mockIssue(true, { + key: '1', + component: 'project:main.js', + textRange: { startLine: 1, endLine: 2, startOffset: 0, endOffset: 15 } + })} issues={[]} - locations={[mockFlowLocation()]} + locations={[mockFlowLocation({ component: 'project:main.js' })]} onIssueChange={jest.fn()} onLoaded={jest.fn()} onIssueSelect={jest.fn()} onLocationSelect={jest.fn()} - scroll={jest.fn()} selectedFlowIndex={0} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx index 614c63fc735..1a3c685d6f8 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx @@ -17,11 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import { range } from 'lodash'; import * as React from 'react'; import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/mocks/sources'; -import { scrollHorizontally } from '../../../../helpers/scrolling'; import { mockIssue } from '../../../../helpers/testMocks'; import SnippetViewer from '../SnippetViewer'; @@ -115,29 +114,6 @@ it('should correctly handle expansion', () => { expect(expandBlock).toHaveBeenCalledWith(2, 'down'); }); -it('should handle scrolling', () => { - const scroll = jest.fn(); - const wrapper = mountRender({ scroll }); - - const element = {} as HTMLElement; - - wrapper.instance().doScroll(element); - - expect(scroll).toHaveBeenCalledWith(element); - - expect(scrollHorizontally).toHaveBeenCalled(); - expect((scrollHorizontally as jest.Mock).mock.calls[0][0]).toBe(element); -}); - -it('should handle scrolling to expanded row', () => { - const scroll = jest.fn(); - const wrapper = mountRender({ scroll }); - - wrapper.instance().scrollToLastExpandedRow(); - - expect(scroll).toHaveBeenCalled(); -}); - function shallowRender(props: Partial<SnippetViewer['props']> = {}) { return shallow<SnippetViewer>( <SnippetViewer @@ -156,34 +132,8 @@ function shallowRender(props: Partial<SnippetViewer['props']> = {}) { locationsByLine={{}} onLocationSelect={jest.fn()} renderDuplicationPopup={jest.fn()} - scroll={jest.fn()} snippet={[]} {...props} /> ); } - -function mountRender(props: Partial<SnippetViewer['props']> = {}) { - return mount<SnippetViewer>( - <SnippetViewer - component={mockSourceViewerFile()} - duplications={undefined} - duplicationsByLine={undefined} - expandBlock={jest.fn()} - handleSymbolClick={jest.fn()} - highlightedLocationMessage={{ index: 0, text: '' }} - highlightedSymbols={[]} - index={0} - issue={mockIssue()} - lastSnippetOfLastGroup={false} - loadDuplications={jest.fn()} - locations={[]} - locationsByLine={{}} - onLocationSelect={jest.fn()} - renderDuplicationPopup={jest.fn()} - scroll={jest.fn()} - snippet={[mockSourceLine()]} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap index 9825cb22d5c..98d86611f24 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/ComponentSourceSnippetGroupViewer-test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -<div - className="component-source-container" -> +<Fragment> <IssueSourceViewerHeader branchLike={ Object { @@ -37,5 +35,5 @@ exports[`should render correctly 1`] = ` } } /> -</div> +</Fragment> `; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap index f587717fbbc..489fd62f4f0 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewer-test.tsx.snap @@ -22,30 +22,13 @@ exports[`should render correctly 1`] = ` `; exports[`should render correctly 2`] = ` -<div> +<Fragment> <ContextProvider key="1-0-0" value={ Object { "branchLike": undefined, - "file": Object { - "canMarkAsFavorite": true, - "fav": false, - "key": "project:main.js", - "longName": "main.js", - "measures": Object { - "coverage": "85.2", - "duplicationDensity": "1.0", - "issues": "12", - "lines": "56", - }, - "name": "main.js", - "path": "main.js", - "project": "project", - "projectName": "MyProject", - "q": "FIL", - "uuid": "foo-bar", - }, + "file": Object {}, } } > @@ -55,7 +38,7 @@ exports[`should render correctly 2`] = ` issue={ Object { "actions": Array [], - "component": "main.js", + "component": "project:main.js", "componentLongName": "main.js", "componentQualifier": "FIL", "componentUuid": "foo1234", @@ -143,9 +126,9 @@ exports[`should render correctly 2`] = ` "severity": "MAJOR", "status": "OPEN", "textRange": Object { - "endLine": 26, + "endLine": 2, "endOffset": 15, - "startLine": 25, + "startLine": 1, "startOffset": 0, }, "transitions": Array [], @@ -158,7 +141,7 @@ exports[`should render correctly 2`] = ` locations={ Array [ Object { - "component": "main.js", + "component": "project:main.js", "index": 0, "textRange": Object { "endLine": 2, @@ -174,30 +157,12 @@ exports[`should render correctly 2`] = ` onIssueSelect={[MockFunction]} onLocationSelect={[MockFunction]} renderDuplicationPopup={[Function]} - scroll={[MockFunction]} snippetGroup={ Object { - "component": Object { - "canMarkAsFavorite": true, - "fav": false, - "key": "project:main.js", - "longName": "main.js", - "measures": Object { - "coverage": "85.2", - "duplicationDensity": "1.0", - "issues": "12", - "lines": "56", - }, - "name": "main.js", - "path": "main.js", - "project": "project", - "projectName": "MyProject", - "q": "FIL", - "uuid": "foo-bar", - }, + "component": Object {}, "locations": Array [ Object { - "component": "main.js", + "component": "project:main.js", "index": 0, "textRange": Object { "endLine": 2, @@ -207,28 +172,16 @@ exports[`should render correctly 2`] = ` }, }, ], - "sources": Object { - "16": Object { - "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 16, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - }, - }, + "sources": Array [], } } /> </ContextProvider> -</div> +</Fragment> `; exports[`should render correctly: no component found 1`] = ` -<div> +<Fragment> <ContextProvider key="unknown-0-0" value={ @@ -350,7 +303,6 @@ exports[`should render correctly: no component found 1`] = ` onIssueSelect={[MockFunction]} onLocationSelect={[MockFunction]} renderDuplicationPopup={[Function]} - scroll={[MockFunction]} snippetGroup={ Object { "component": Object {}, @@ -365,24 +317,7 @@ exports[`should render correctly: no component found 1`] = ` value={ Object { "branchLike": undefined, - "file": Object { - "canMarkAsFavorite": true, - "fav": false, - "key": "project:main.js", - "longName": "main.js", - "measures": Object { - "coverage": "85.2", - "duplicationDensity": "1.0", - "issues": "12", - "lines": "56", - }, - "name": "main.js", - "path": "main.js", - "project": "project", - "projectName": "MyProject", - "q": "FIL", - "uuid": "foo-bar", - }, + "file": Object {}, } } > @@ -495,7 +430,7 @@ exports[`should render correctly: no component found 1`] = ` locations={ Array [ Object { - "component": "main.js", + "component": "project:main.js", "index": 0, "textRange": Object { "endLine": 2, @@ -511,30 +446,12 @@ exports[`should render correctly: no component found 1`] = ` onIssueSelect={[MockFunction]} onLocationSelect={[MockFunction]} renderDuplicationPopup={[Function]} - scroll={[MockFunction]} snippetGroup={ Object { - "component": Object { - "canMarkAsFavorite": true, - "fav": false, - "key": "project:main.js", - "longName": "main.js", - "measures": Object { - "coverage": "85.2", - "duplicationDensity": "1.0", - "issues": "12", - "lines": "56", - }, - "name": "main.js", - "path": "main.js", - "project": "project", - "projectName": "MyProject", - "q": "FIL", - "uuid": "foo-bar", - }, + "component": Object {}, "locations": Array [ Object { - "component": "main.js", + "component": "project:main.js", "index": 0, "textRange": Object { "endLine": 2, @@ -544,22 +461,10 @@ exports[`should render correctly: no component found 1`] = ` }, }, ], - "sources": Object { - "16": Object { - "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;", - "coverageStatus": "covered", - "coveredConditions": 2, - "duplicated": false, - "isNew": true, - "line": 16, - "scmAuthor": "simon.brandhof@sonarsource.com", - "scmDate": "2018-12-11T10:48:39+0100", - "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", - }, - }, + "sources": Array [], } } /> </ContextProvider> -</div> +</Fragment> `; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap index 0b6e42a0fb3..fb3dd212fc1 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/IssueSourceViewerHeader-test.tsx.snap @@ -2,7 +2,9 @@ exports[`should render correctly 1`] = ` <div - className="source-viewer-header-slim display-flex-row display-flex-space-between" + aria-label="foo/bar.ts" + className="issue-source-viewer-header display-flex-row display-flex-space-between" + role="separator" > <div className="display-flex-center flex-1" @@ -82,7 +84,9 @@ exports[`should render correctly 1`] = ` exports[`should render correctly: no link to project 1`] = ` <div - className="source-viewer-header-slim display-flex-row display-flex-space-between" + aria-label="foo/bar.ts" + className="issue-source-viewer-header display-flex-row display-flex-space-between" + role="separator" > <div className="display-flex-center flex-1" @@ -157,7 +161,9 @@ exports[`should render correctly: no link to project 1`] = ` exports[`should render correctly: no project name 1`] = ` <div - className="source-viewer-header-slim display-flex-row display-flex-space-between" + aria-label="foo/bar.ts" + className="issue-source-viewer-header display-flex-row display-flex-space-between" + role="separator" > <div className="display-flex-center flex-1" @@ -221,7 +227,9 @@ exports[`should render correctly: no project name 1`] = ` exports[`should render correctly: project root 1`] = ` <div - className="source-viewer-header-slim display-flex-row display-flex-space-between" + aria-label="foo/bar.ts" + className="issue-source-viewer-header display-flex-row display-flex-space-between" + role="separator" > <div className="display-flex-center flex-1" diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap index 085138cd9cf..0ee54422c94 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/SnippetViewer-test.tsx.snap @@ -55,7 +55,6 @@ exports[`should render correctly 1`] = ` onSymbolClick={[MockFunction]} openIssues={false} renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -107,7 +106,6 @@ exports[`should render correctly 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -159,7 +157,6 @@ exports[`should render correctly 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -235,7 +232,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` onSymbolClick={[MockFunction]} openIssues={false} renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -287,7 +283,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -339,7 +334,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -391,7 +385,6 @@ exports[`should render correctly when at the bottom of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -456,7 +449,6 @@ exports[`should render correctly when at the top of the file 1`] = ` onSymbolClick={[MockFunction]} openIssues={false} renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -508,7 +500,6 @@ exports[`should render correctly when at the top of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -560,7 +551,6 @@ exports[`should render correctly when at the top of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -612,7 +602,6 @@ exports[`should render correctly when at the top of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -664,7 +653,6 @@ exports[`should render correctly when at the top of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -716,7 +704,6 @@ exports[`should render correctly when at the top of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -768,7 +755,6 @@ exports[`should render correctly when at the top of the file 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -845,7 +831,6 @@ exports[`should render correctly with no SCM 1`] = ` onSymbolClick={[MockFunction]} openIssues={false} renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -898,7 +883,6 @@ exports[`should render correctly with no SCM 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> @@ -951,7 +935,6 @@ exports[`should render correctly with no SCM 1`] = ` } } renderDuplicationPopup={[MockFunction]} - scroll={[Function]} secondaryIssueLocations={Array []} verticalBuffer={0} /> diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts index dadf1c31958..bc9896d68a6 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts @@ -23,7 +23,7 @@ import { createSnippets, expandSnippet, groupLocationsByComponent } from '../uti describe('groupLocationsByComponent', () => { it('should handle empty args', () => { - expect(groupLocationsByComponent(mockIssue(), [], {})).toEqual([]); + expect(groupLocationsByComponent(mockIssue(), [], {})).toEqual([{ locations: [] }]); }); it('should group correctly', () => { diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts index d41b61cbe0d..7476c471fb1 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts @@ -181,6 +181,10 @@ export function groupLocationsByComponent( currentGroup.locations.push(loc); }); + if (groups.length === 0) { + groups.push({ locations: [], ...components[issue.component] }); + } + return groups; } |