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