You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ComponentSourceSnippetGroupViewer-test.tsx 16KB

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