]> source.dussan.org Git - sonarqube.git/blob
cbbe8669da51024f3d48da0b5bd0464b21d041d0
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2022 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 import { mount, ReactWrapper, shallow } from 'enzyme';
21 import { range, times } from 'lodash';
22 import * as React from 'react';
23 import { getSources } from '../../../../api/components';
24 import Issue from '../../../../components/issue/Issue';
25 import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
26 import {
27   mockSnippetsByComponent,
28   mockSourceLine,
29   mockSourceViewerFile
30 } from '../../../../helpers/mocks/sources';
31 import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks';
32 import { waitAndUpdate } from '../../../../helpers/testUtils';
33 import { SnippetGroup } from '../../../../types/types';
34 import ComponentSourceSnippetGroupViewer from '../ComponentSourceSnippetGroupViewer';
35 import SnippetViewer from '../SnippetViewer';
36
37 jest.mock('../../../../api/components', () => ({
38   getSources: jest.fn().mockResolvedValue([])
39 }));
40
41 beforeEach(() => {
42   jest.clearAllMocks();
43 });
44
45 it('should render correctly', () => {
46   expect(shallowRender()).toMatchSnapshot();
47 });
48
49 it('should render correctly with secondary locations', () => {
50   // issue with secondary locations but no flows
51   const issue = mockIssue(true, {
52     component: 'project:main.js',
53     flows: [],
54     textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 10 }
55   });
56
57   const snippetGroup: SnippetGroup = {
58     locations: [
59       mockFlowLocation({
60         component: issue.component,
61         textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
62       }),
63       mockFlowLocation({
64         component: issue.component,
65         textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }
66       })
67     ],
68     ...mockSnippetsByComponent('main.js', 'project', [
69       ...range(2, 17),
70       ...range(29, 39),
71       ...range(69, 79)
72     ])
73   };
74   const wrapper = shallowRender({ issue, snippetGroup });
75   expect(wrapper.state('snippets')).toHaveLength(3);
76   expect(wrapper.state('snippets')[0]).toEqual({ index: 0, start: 2, end: 16 });
77   expect(wrapper.state('snippets')[1]).toEqual({ index: 1, start: 29, end: 39 });
78   expect(wrapper.state('snippets')[2]).toEqual({ index: 2, start: 69, end: 79 });
79 });
80
81 it('should render correctly with flows', () => {
82   // issue with flows but no secondary locations
83   const issue = mockIssue(true, {
84     component: 'project:main.js',
85     secondaryLocations: [],
86     textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 10 }
87   });
88
89   const snippetGroup: SnippetGroup = {
90     locations: [
91       mockFlowLocation({
92         component: issue.component,
93         textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
94       }),
95       mockFlowLocation({
96         component: issue.component,
97         textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }
98       })
99     ],
100     ...mockSnippetsByComponent('main.js', 'project', [
101       ...range(2, 17),
102       ...range(29, 39),
103       ...range(69, 79)
104     ])
105   };
106   const wrapper = shallowRender({ issue, snippetGroup });
107   expect(wrapper.state('snippets')).toHaveLength(2);
108   expect(wrapper.state('snippets')[0]).toEqual({ index: 0, start: 29, end: 39 });
109   expect(wrapper.state('snippets')[1]).toEqual({ index: 1, start: 69, end: 79 });
110
111   // Check that locationsByLine is defined when isLastOccurenceOfPrimaryComponent
112   expect(
113     wrapper
114       .find(SnippetViewer)
115       .at(0)
116       .props().locationsByLine
117   ).not.toEqual({});
118
119   // If not, it should be an empty object:
120   const snippets = shallowRender({
121     isLastOccurenceOfPrimaryComponent: false,
122     issue,
123     snippetGroup
124   }).find(SnippetViewer);
125
126   expect(snippets.at(0).props().locationsByLine).toEqual({});
127   expect(snippets.at(1).props().locationsByLine).toEqual({});
128 });
129
130 it('should render file-level issue correctly', () => {
131   // issue with secondary locations and no primary location
132   const issue = mockIssue(true, {
133     component: 'project:main.js',
134     flows: [],
135     textRange: undefined
136   });
137
138   const wrapper = shallowRender({
139     issue,
140     snippetGroup: {
141       locations: [
142         mockFlowLocation({
143           component: issue.component,
144           textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
145         })
146       ],
147       ...mockSnippetsByComponent('main.js', 'project', range(29, 39))
148     }
149   });
150
151   expect(wrapper.find(Issue).exists()).toBe(true);
152 });
153
154 it('should expand block', async () => {
155   (getSources as jest.Mock).mockResolvedValueOnce(
156     Object.values(mockSnippetsByComponent('a', 'project', range(6, 59)).sources)
157   );
158   const issue = mockIssue(true, {
159     textRange: { startLine: 74, endLine: 74, startOffset: 5, endOffset: 10 }
160   });
161   const snippetGroup: SnippetGroup = {
162     locations: [
163       mockFlowLocation({
164         component: 'a',
165         textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }
166       }),
167       mockFlowLocation({
168         component: 'a',
169         textRange: { startLine: 107, endLine: 107, startOffset: 0, endOffset: 0 }
170       })
171     ],
172     ...mockSnippetsByComponent('a', 'project', [...range(69, 83), ...range(102, 112)])
173   };
174
175   const wrapper = shallowRender({ issue, snippetGroup });
176
177   wrapper.instance().expandBlock(0, 'up');
178   await waitAndUpdate(wrapper);
179
180   expect(getSources).toHaveBeenCalledWith({ from: 9, key: 'project:a', to: 68 });
181   expect(wrapper.state('snippets')).toHaveLength(2);
182   expect(wrapper.state('snippets')[0]).toEqual({ index: 0, start: 19, end: 83 });
183   expect(Object.keys(wrapper.state('additionalLines'))).toHaveLength(53);
184 });
185
186 it('should expand full component', async () => {
187   (getSources as jest.Mock).mockResolvedValueOnce(
188     Object.values(mockSnippetsByComponent('a', 'project', times(14)).sources)
189   );
190   const snippetGroup: SnippetGroup = {
191     locations: [
192       mockFlowLocation({
193         component: 'a',
194         textRange: { startLine: 3, endLine: 3, startOffset: 0, endOffset: 0 }
195       }),
196       mockFlowLocation({
197         component: 'a',
198         textRange: { startLine: 12, endLine: 12, startOffset: 0, endOffset: 0 }
199       })
200     ],
201     ...mockSnippetsByComponent('a', 'project', [1, 2, 3, 4, 5, 10, 11, 12, 13, 14])
202   };
203
204   const wrapper = shallowRender({ snippetGroup });
205
206   wrapper.instance().expandComponent();
207   await waitAndUpdate(wrapper);
208
209   expect(getSources).toHaveBeenCalledWith({ key: 'project:a' });
210   expect(wrapper.state('snippets')).toHaveLength(1);
211   expect(wrapper.state('snippets')[0]).toEqual({ index: -1, start: 0, end: 13 });
212 });
213
214 it('should get the right branch when expanding', async () => {
215   (getSources as jest.Mock).mockResolvedValueOnce(
216     Object.values(
217       mockSnippetsByComponent('a', 'project', [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17])
218         .sources
219     )
220   );
221   const snippetGroup: SnippetGroup = {
222     locations: [mockFlowLocation()],
223     ...mockSnippetsByComponent('a', 'project', [1, 2, 3, 4, 5, 6, 7])
224   };
225
226   const wrapper = shallowRender({
227     branchLike: mockBranch({ name: 'asdf' }),
228     snippetGroup
229   });
230
231   wrapper.instance().expandBlock(0, 'down');
232   await waitAndUpdate(wrapper);
233
234   expect(getSources).toHaveBeenCalledWith({ branch: 'asdf', from: 8, key: 'project:a', to: 67 });
235 });
236
237 it('should handle correctly open/close issue', () => {
238   const wrapper = shallowRender();
239   const sourceLine = mockSourceLine();
240   expect(wrapper.state('openIssuesByLine')).toEqual({});
241   wrapper.instance().handleOpenIssues(sourceLine);
242   expect(wrapper.state('openIssuesByLine')).toEqual({ [sourceLine.line]: true });
243   wrapper.instance().handleCloseIssues(sourceLine);
244   expect(wrapper.state('openIssuesByLine')).toEqual({ [sourceLine.line]: false });
245 });
246
247 it('should handle symbol highlighting', () => {
248   const wrapper = shallowRender();
249   expect(wrapper.state('highlightedSymbols')).toEqual([]);
250   wrapper.instance().handleSymbolClick(['foo']);
251   expect(wrapper.state('highlightedSymbols')).toEqual(['foo']);
252   wrapper.instance().handleSymbolClick(['foo']);
253   expect(wrapper.state('highlightedSymbols')).toEqual([]);
254 });
255
256 it('should correctly handle lines actions', () => {
257   const snippetGroup: SnippetGroup = {
258     locations: [
259       mockFlowLocation({
260         component: 'my-project:foo/bar.ts',
261         textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
262       }),
263       mockFlowLocation({
264         component: 'my-project:foo/bar.ts',
265         textRange: { startLine: 54, endLine: 54, startOffset: 0, endOffset: 0 }
266       })
267     ],
268     ...mockSnippetsByComponent('foo/bar.ts', 'my-project', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56])
269   };
270   const loadDuplications = jest.fn();
271   const renderDuplicationPopup = jest.fn();
272
273   const wrapper = shallowRender({
274     loadDuplications,
275     renderDuplicationPopup,
276     snippetGroup
277   });
278
279   const line = mockSourceLine();
280   wrapper
281     .find('SnippetViewer')
282     .first()
283     .prop<Function>('loadDuplications')(line);
284   expect(loadDuplications).toHaveBeenCalledWith('my-project:foo/bar.ts', line);
285
286   wrapper
287     .find('SnippetViewer')
288     .first()
289     .prop<Function>('renderDuplicationPopup')(1, 13);
290   expect(renderDuplicationPopup).toHaveBeenCalledWith(
291     mockSourceViewerFile('foo/bar.ts', 'my-project'),
292     1,
293     13
294   );
295 });
296
297 it('should render correctly line with issue', () => {
298   const issue = mockIssue(false, {
299     textRange: { endLine: 1, startLine: 1, endOffset: 1, startOffset: 0 }
300   });
301   const wrapper = shallowRender({
302     issue,
303     issuesByLine: { '1': [issue] }
304   });
305   wrapper.instance().setState({ openIssuesByLine: { '1': true } });
306   const wrapperLine = shallow(wrapper.instance().renderIssuesList(mockSourceLine({ line: 1 })));
307   expect(wrapperLine).toMatchSnapshot();
308 });
309
310 describe('getNodes', () => {
311   const snippetGroup: SnippetGroup = {
312     component: mockSourceViewerFile(),
313     locations: [],
314     sources: []
315   };
316   const wrapper = mount<ComponentSourceSnippetGroupViewer>(
317     <ComponentSourceSnippetGroupViewer
318       branchLike={mockMainBranch()}
319       highlightedLocationMessage={{ index: 0, text: '' }}
320       isLastOccurenceOfPrimaryComponent={true}
321       issue={mockIssue()}
322       issuesByLine={{}}
323       lastSnippetGroup={false}
324       loadDuplications={jest.fn()}
325       locations={[]}
326       onIssueChange={jest.fn()}
327       onIssuePopupToggle={jest.fn()}
328       onLocationSelect={jest.fn()}
329       renderDuplicationPopup={jest.fn()}
330       scroll={jest.fn()}
331       snippetGroup={snippetGroup}
332     />
333   );
334
335   it('should return undefined if any node is missing', async () => {
336     await waitAndUpdate(wrapper);
337     const rootNode = wrapper.instance().rootNodeRef;
338     mockDom(rootNode.current!);
339     expect(wrapper.instance().getNodes(0)).toBeUndefined();
340     expect(wrapper.instance().getNodes(1)).toBeUndefined();
341     expect(wrapper.instance().getNodes(2)).toBeUndefined();
342   });
343
344   it('should return elements if dom is correct', async () => {
345     await waitAndUpdate(wrapper);
346     const rootNode = wrapper.instance().rootNodeRef;
347     mockDom(rootNode.current!);
348     expect(wrapper.instance().getNodes(3)).not.toBeUndefined();
349   });
350
351   it('should enable cleaning the dom', async () => {
352     await waitAndUpdate(wrapper);
353     const rootNode = wrapper.instance().rootNodeRef;
354     mockDom(rootNode.current!);
355
356     wrapper.instance().cleanDom(3);
357     const nodes = wrapper.instance().getNodes(3);
358     expect(nodes!.wrapper.style.maxHeight).toBe('');
359     expect(nodes!.table.style.marginTop).toBe('');
360   });
361 });
362
363 describe('getHeight', () => {
364   beforeAll(() => {
365     jest.useFakeTimers();
366   });
367
368   afterAll(() => {
369     jest.runOnlyPendingTimers();
370     jest.useRealTimers();
371   });
372
373   const snippetGroup: SnippetGroup = {
374     component: mockSourceViewerFile(),
375     locations: [],
376     sources: []
377   };
378   const wrapper = mount<ComponentSourceSnippetGroupViewer>(
379     <ComponentSourceSnippetGroupViewer
380       branchLike={mockMainBranch()}
381       highlightedLocationMessage={{ index: 0, text: '' }}
382       isLastOccurenceOfPrimaryComponent={true}
383       issue={mockIssue()}
384       issuesByLine={{}}
385       lastSnippetGroup={false}
386       loadDuplications={jest.fn()}
387       locations={[]}
388       onIssueChange={jest.fn()}
389       onIssuePopupToggle={jest.fn()}
390       onLocationSelect={jest.fn()}
391       renderDuplicationPopup={jest.fn()}
392       scroll={jest.fn()}
393       snippetGroup={snippetGroup}
394     />
395   );
396
397   it('should set maxHeight to current height', async () => {
398     await waitAndUpdate(wrapper);
399
400     const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 });
401     wrapper.instance().setMaxHeight(0);
402
403     expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;');
404     expect(nodes.table.getAttribute('style')).toBeNull();
405   });
406
407   it('should set margin and then maxHeight for a nice upwards animation', async () => {
408     await waitAndUpdate(wrapper);
409
410     const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 });
411     wrapper.instance().setMaxHeight(0, undefined, true);
412
413     expect(nodes.wrapper.getAttribute('style')).toBeNull();
414     expect(nodes.table.getAttribute('style')).toBe('transition: none; margin-top: -26px;');
415
416     jest.runAllTimers();
417
418     expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;');
419     expect(nodes.table.getAttribute('style')).toBe('margin-top: 0px;');
420   });
421 });
422
423 function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props']> = {}) {
424   const snippetGroup: SnippetGroup = {
425     component: mockSourceViewerFile(),
426     locations: [],
427     sources: []
428   };
429   return shallow<ComponentSourceSnippetGroupViewer>(
430     <ComponentSourceSnippetGroupViewer
431       branchLike={mockMainBranch()}
432       highlightedLocationMessage={{ index: 0, text: '' }}
433       isLastOccurenceOfPrimaryComponent={true}
434       issue={mockIssue()}
435       issuesByLine={{}}
436       lastSnippetGroup={false}
437       loadDuplications={jest.fn()}
438       locations={[]}
439       onIssueChange={jest.fn()}
440       onIssuePopupToggle={jest.fn()}
441       onLocationSelect={jest.fn()}
442       renderDuplicationPopup={jest.fn()}
443       scroll={jest.fn()}
444       snippetGroup={snippetGroup}
445       {...props}
446     />
447   );
448 }
449
450 function mockDom(refNode: HTMLDivElement) {
451   refNode.querySelector = jest.fn(query => {
452     const index = query.split('-').pop();
453
454     switch (index) {
455       case '0':
456         return null;
457       case '1':
458         return mount(<div />).getDOMNode();
459       case '2':
460         return mount(
461           <div>
462             <div className="snippet" />
463           </div>
464         ).getDOMNode();
465       case '3':
466         return mount(
467           <div>
468             <div className="snippet">
469               <div />
470             </div>
471           </div>
472         ).getDOMNode();
473       default:
474         return null;
475     }
476   });
477 }
478
479 function mockDomForSizes(
480   componentWrapper: ReactWrapper<{}, {}, ComponentSourceSnippetGroupViewer>,
481   { wrapperHeight = 0, tableHeight = 0 }
482 ) {
483   const wrapper = mount(<div className="snippet" />).getDOMNode();
484   wrapper.getBoundingClientRect = jest.fn().mockReturnValue({ height: wrapperHeight });
485   const table = mount(<div />).getDOMNode();
486   table.getBoundingClientRect = jest.fn().mockReturnValue({ height: tableHeight });
487   componentWrapper.instance().getNodes = jest.fn().mockReturnValue({
488     wrapper,
489     table
490   });
491   return { wrapper, table };
492 }