3 * Copyright (C) 2009-2022 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
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';
27 mockSnippetsByComponent,
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';
37 jest.mock('../../../../api/components', () => ({
38 getSources: jest.fn().mockResolvedValue([])
45 it('should render correctly', () => {
46 expect(shallowRender()).toMatchSnapshot();
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',
54 textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 10 }
57 const snippetGroup: SnippetGroup = {
60 component: issue.component,
61 textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
64 component: issue.component,
65 textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }
68 ...mockSnippetsByComponent('main.js', 'project', [
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 });
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 }
89 const snippetGroup: SnippetGroup = {
92 component: issue.component,
93 textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
96 component: issue.component,
97 textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }
100 ...mockSnippetsByComponent('main.js', 'project', [
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 });
111 // Check that locationsByLine is defined when isLastOccurenceOfPrimaryComponent
116 .props().locationsByLine
119 // If not, it should be an empty object:
120 const snippets = shallowRender({
121 isLastOccurenceOfPrimaryComponent: false,
124 }).find(SnippetViewer);
126 expect(snippets.at(0).props().locationsByLine).toEqual({});
127 expect(snippets.at(1).props().locationsByLine).toEqual({});
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',
138 const wrapper = shallowRender({
143 component: issue.component,
144 textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
147 ...mockSnippetsByComponent('main.js', 'project', range(29, 39))
151 expect(wrapper.find(Issue).exists()).toBe(true);
154 it('should expand block', async () => {
155 (getSources as jest.Mock).mockResolvedValueOnce(
156 Object.values(mockSnippetsByComponent('a', 'project', range(6, 59)).sources)
158 const issue = mockIssue(true, {
159 textRange: { startLine: 74, endLine: 74, startOffset: 5, endOffset: 10 }
161 const snippetGroup: SnippetGroup = {
165 textRange: { startLine: 74, endLine: 74, startOffset: 0, endOffset: 0 }
169 textRange: { startLine: 107, endLine: 107, startOffset: 0, endOffset: 0 }
172 ...mockSnippetsByComponent('a', 'project', [...range(69, 83), ...range(102, 112)])
175 const wrapper = shallowRender({ issue, snippetGroup });
177 wrapper.instance().expandBlock(0, 'up');
178 await waitAndUpdate(wrapper);
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);
186 it('should expand full component', async () => {
187 (getSources as jest.Mock).mockResolvedValueOnce(
188 Object.values(mockSnippetsByComponent('a', 'project', times(14)).sources)
190 const snippetGroup: SnippetGroup = {
194 textRange: { startLine: 3, endLine: 3, startOffset: 0, endOffset: 0 }
198 textRange: { startLine: 12, endLine: 12, startOffset: 0, endOffset: 0 }
201 ...mockSnippetsByComponent('a', 'project', [1, 2, 3, 4, 5, 10, 11, 12, 13, 14])
204 const wrapper = shallowRender({ snippetGroup });
206 wrapper.instance().expandComponent();
207 await waitAndUpdate(wrapper);
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 });
214 it('should get the right branch when expanding', async () => {
215 (getSources as jest.Mock).mockResolvedValueOnce(
217 mockSnippetsByComponent('a', 'project', [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17])
221 const snippetGroup: SnippetGroup = {
222 locations: [mockFlowLocation()],
223 ...mockSnippetsByComponent('a', 'project', [1, 2, 3, 4, 5, 6, 7])
226 const wrapper = shallowRender({
227 branchLike: mockBranch({ name: 'asdf' }),
231 wrapper.instance().expandBlock(0, 'down');
232 await waitAndUpdate(wrapper);
234 expect(getSources).toHaveBeenCalledWith({ branch: 'asdf', from: 8, key: 'project:a', to: 67 });
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 });
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([]);
256 it('should correctly handle lines actions', () => {
257 const snippetGroup: SnippetGroup = {
260 component: 'my-project:foo/bar.ts',
261 textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
264 component: 'my-project:foo/bar.ts',
265 textRange: { startLine: 54, endLine: 54, startOffset: 0, endOffset: 0 }
268 ...mockSnippetsByComponent('foo/bar.ts', 'my-project', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56])
270 const loadDuplications = jest.fn();
271 const renderDuplicationPopup = jest.fn();
273 const wrapper = shallowRender({
275 renderDuplicationPopup,
279 const line = mockSourceLine();
281 .find('SnippetViewer')
283 .prop<Function>('loadDuplications')(line);
284 expect(loadDuplications).toHaveBeenCalledWith('my-project:foo/bar.ts', line);
287 .find('SnippetViewer')
289 .prop<Function>('renderDuplicationPopup')(1, 13);
290 expect(renderDuplicationPopup).toHaveBeenCalledWith(
291 mockSourceViewerFile('foo/bar.ts', 'my-project'),
297 it('should render correctly line with issue', () => {
298 const issue = mockIssue(false, {
299 textRange: { endLine: 1, startLine: 1, endOffset: 1, startOffset: 0 }
301 const wrapper = shallowRender({
303 issuesByLine: { '1': [issue] }
305 wrapper.instance().setState({ openIssuesByLine: { '1': true } });
306 const wrapperLine = shallow(wrapper.instance().renderIssuesList(mockSourceLine({ line: 1 })));
307 expect(wrapperLine).toMatchSnapshot();
310 describe('getNodes', () => {
311 const snippetGroup: SnippetGroup = {
312 component: mockSourceViewerFile(),
316 const wrapper = mount<ComponentSourceSnippetGroupViewer>(
317 <ComponentSourceSnippetGroupViewer
318 branchLike={mockMainBranch()}
319 highlightedLocationMessage={{ index: 0, text: '' }}
320 isLastOccurenceOfPrimaryComponent={true}
323 lastSnippetGroup={false}
324 loadDuplications={jest.fn()}
326 onIssueChange={jest.fn()}
327 onIssuePopupToggle={jest.fn()}
328 onLocationSelect={jest.fn()}
329 renderDuplicationPopup={jest.fn()}
331 snippetGroup={snippetGroup}
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();
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();
351 it('should enable cleaning the dom', async () => {
352 await waitAndUpdate(wrapper);
353 const rootNode = wrapper.instance().rootNodeRef;
354 mockDom(rootNode.current!);
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('');
363 describe('getHeight', () => {
365 jest.useFakeTimers();
369 jest.runOnlyPendingTimers();
370 jest.useRealTimers();
373 const snippetGroup: SnippetGroup = {
374 component: mockSourceViewerFile(),
378 const wrapper = mount<ComponentSourceSnippetGroupViewer>(
379 <ComponentSourceSnippetGroupViewer
380 branchLike={mockMainBranch()}
381 highlightedLocationMessage={{ index: 0, text: '' }}
382 isLastOccurenceOfPrimaryComponent={true}
385 lastSnippetGroup={false}
386 loadDuplications={jest.fn()}
388 onIssueChange={jest.fn()}
389 onIssuePopupToggle={jest.fn()}
390 onLocationSelect={jest.fn()}
391 renderDuplicationPopup={jest.fn()}
393 snippetGroup={snippetGroup}
397 it('should set maxHeight to current height', async () => {
398 await waitAndUpdate(wrapper);
400 const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 });
401 wrapper.instance().setMaxHeight(0);
403 expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;');
404 expect(nodes.table.getAttribute('style')).toBeNull();
407 it('should set margin and then maxHeight for a nice upwards animation', async () => {
408 await waitAndUpdate(wrapper);
410 const nodes = mockDomForSizes(wrapper, { wrapperHeight: 42, tableHeight: 68 });
411 wrapper.instance().setMaxHeight(0, undefined, true);
413 expect(nodes.wrapper.getAttribute('style')).toBeNull();
414 expect(nodes.table.getAttribute('style')).toBe('transition: none; margin-top: -26px;');
418 expect(nodes.wrapper.getAttribute('style')).toBe('max-height: 88px;');
419 expect(nodes.table.getAttribute('style')).toBe('margin-top: 0px;');
423 function shallowRender(props: Partial<ComponentSourceSnippetGroupViewer['props']> = {}) {
424 const snippetGroup: SnippetGroup = {
425 component: mockSourceViewerFile(),
429 return shallow<ComponentSourceSnippetGroupViewer>(
430 <ComponentSourceSnippetGroupViewer
431 branchLike={mockMainBranch()}
432 highlightedLocationMessage={{ index: 0, text: '' }}
433 isLastOccurenceOfPrimaryComponent={true}
436 lastSnippetGroup={false}
437 loadDuplications={jest.fn()}
439 onIssueChange={jest.fn()}
440 onIssuePopupToggle={jest.fn()}
441 onLocationSelect={jest.fn()}
442 renderDuplicationPopup={jest.fn()}
444 snippetGroup={snippetGroup}
450 function mockDom(refNode: HTMLDivElement) {
451 refNode.querySelector = jest.fn(query => {
452 const index = query.split('-').pop();
458 return mount(<div />).getDOMNode();
462 <div className="snippet" />
468 <div className="snippet">
479 function mockDomForSizes(
480 componentWrapper: ReactWrapper<{}, {}, ComponentSourceSnippetGroupViewer>,
481 { wrapperHeight = 0, tableHeight = 0 }
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({
491 return { wrapper, table };