groupingStore.spec.jsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. import GroupingStore from 'app/stores/groupingStore';
  2. import {Client} from 'app/api';
  3. describe('Grouping Store', function() {
  4. let trigger;
  5. beforeAll(function() {
  6. Client.mockAsync = true;
  7. });
  8. afterAll(function() {
  9. Client.mockAsync = false;
  10. });
  11. beforeEach(function() {
  12. trigger = jest.spyOn(GroupingStore, 'trigger');
  13. Client.clearMockResponses();
  14. Client.addMockResponse({
  15. url: '/issues/groupId/hashes/',
  16. body: [
  17. {
  18. latestEvent: {
  19. eventID: 'event-1',
  20. },
  21. state: 'locked',
  22. id: '1',
  23. },
  24. {
  25. latestEvent: {
  26. eventID: 'event-2',
  27. },
  28. state: 'unlocked',
  29. id: '2',
  30. },
  31. {
  32. latestEvent: {
  33. eventID: 'event-3',
  34. },
  35. state: 'unlocked',
  36. id: '3',
  37. },
  38. {
  39. latestEvent: {
  40. eventID: 'event-4',
  41. },
  42. state: 'unlocked',
  43. id: '4',
  44. },
  45. {
  46. latestEvent: {
  47. eventID: 'event-5',
  48. },
  49. state: 'locked',
  50. id: '5',
  51. },
  52. ],
  53. });
  54. Client.addMockResponse({
  55. url: '/issues/groupId/similar/',
  56. body: [
  57. [
  58. {
  59. id: '274',
  60. },
  61. {
  62. 'exception:stacktrace:pairs': 0.375,
  63. 'exception:stacktrace:application-chunks': 0.175,
  64. 'message:message:character-shingles': 0.775,
  65. },
  66. ],
  67. [
  68. {
  69. id: '275',
  70. },
  71. {'exception:stacktrace:pairs': 1.0},
  72. ],
  73. [
  74. {
  75. id: '216',
  76. },
  77. {
  78. 'exception:stacktrace:application-chunks': 0.000235,
  79. 'exception:stacktrace:pairs': 0.001488,
  80. },
  81. ],
  82. [
  83. {
  84. id: '217',
  85. },
  86. {
  87. 'exception:message:character-shingles': null,
  88. 'exception:stacktrace:application-chunks': 0.25,
  89. 'exception:stacktrace:pairs': 0.25,
  90. 'message:message:character-shingles': 0.7,
  91. },
  92. ],
  93. ],
  94. });
  95. });
  96. afterEach(function() {
  97. trigger.mockReset();
  98. });
  99. describe('onFetch()', function() {
  100. it('initially gets called with correct state values', function() {
  101. GroupingStore.onFetch([]);
  102. expect(trigger).toHaveBeenCalled();
  103. expect(trigger).toHaveBeenCalledWith(
  104. expect.objectContaining({
  105. error: false,
  106. filteredSimilarItems: [],
  107. loading: true,
  108. mergeState: new Map(),
  109. mergedItems: [],
  110. mergedLinks: '',
  111. similarItems: [],
  112. similarLinks: '',
  113. unmergeState: new Map(),
  114. })
  115. );
  116. });
  117. it('fetches list of similar items', async function() {
  118. await GroupingStore.onFetch([
  119. {dataKey: 'similar', endpoint: '/issues/groupId/similar/'},
  120. ]);
  121. expect(trigger).toHaveBeenCalled();
  122. let calls = trigger.mock.calls;
  123. let arg = calls[calls.length - 1][0];
  124. expect(arg.filteredSimilarItems).toHaveLength(1);
  125. expect(arg.similarItems).toHaveLength(3);
  126. expect(arg).toMatchObject({
  127. loading: false,
  128. error: false,
  129. mergeState: new Map(),
  130. mergedItems: [],
  131. similarItems: [
  132. {
  133. isBelowThreshold: false,
  134. issue: {
  135. id: '274',
  136. },
  137. },
  138. {
  139. isBelowThreshold: false,
  140. issue: {
  141. id: '275',
  142. },
  143. },
  144. {
  145. isBelowThreshold: false,
  146. issue: {
  147. id: '217',
  148. },
  149. },
  150. ],
  151. filteredSimilarItems: [
  152. {
  153. isBelowThreshold: true,
  154. issue: {
  155. id: '216',
  156. },
  157. },
  158. ],
  159. unmergeState: new Map(),
  160. });
  161. expect(arg).toMatchSnapshot();
  162. });
  163. it('unsuccessfully fetches list of similar items', function() {
  164. Client.clearMockResponses();
  165. Client.addMockResponse({
  166. url: '/issues/groupId/similar/',
  167. statusCode: 500,
  168. body: {message: 'failed'},
  169. });
  170. let promise = GroupingStore.onFetch([
  171. {dataKey: 'similar', endpoint: '/issues/groupId/similar/'},
  172. ]);
  173. expect(trigger).toHaveBeenCalled();
  174. let calls = trigger.mock.calls;
  175. return promise.then(() => {
  176. let arg = calls[calls.length - 1][0];
  177. expect(arg).toMatchObject({
  178. loading: false,
  179. error: true,
  180. mergeState: new Map(),
  181. mergedItems: [],
  182. unmergeState: new Map(),
  183. });
  184. });
  185. });
  186. it('ignores null scores in aggregate', async function() {
  187. await GroupingStore.onFetch([
  188. {dataKey: 'similar', endpoint: '/issues/groupId/similar/'},
  189. ]);
  190. expect(trigger).toHaveBeenCalled();
  191. let calls = trigger.mock.calls;
  192. let arg = calls[calls.length - 1][0];
  193. let item = arg.similarItems.find(({issue}) => issue.id === '217');
  194. expect(item.aggregate.exception).toBe(0.25);
  195. expect(item.aggregate.message).toBe(0.7);
  196. });
  197. it('fetches list of hashes', function() {
  198. let promise = GroupingStore.onFetch([
  199. {dataKey: 'merged', endpoint: '/issues/groupId/hashes/'},
  200. ]);
  201. expect(trigger).toHaveBeenCalled();
  202. let calls = trigger.mock.calls;
  203. return promise.then(() => {
  204. let arg = calls[calls.length - 1][0];
  205. expect(arg.mergedItems).toHaveLength(5);
  206. expect(arg).toMatchObject({
  207. loading: false,
  208. error: false,
  209. similarItems: [],
  210. filteredSimilarItems: [],
  211. mergeState: new Map(),
  212. unmergeState: new Map([
  213. ['1', {busy: true}],
  214. ['2', {busy: false}],
  215. ['3', {busy: false}],
  216. ['4', {busy: false}],
  217. ['5', {busy: true}],
  218. ]),
  219. });
  220. });
  221. });
  222. it('unsuccessfully fetches list of hashes items', function() {
  223. Client.clearMockResponses();
  224. Client.addMockResponse({
  225. url: '/issues/groupId/hashes/',
  226. statusCode: 500,
  227. body: {message: 'failed'},
  228. });
  229. let promise = GroupingStore.onFetch([
  230. {dataKey: 'merged', endpoint: '/issues/groupId/hashes/'},
  231. ]);
  232. expect(trigger).toHaveBeenCalled();
  233. let calls = trigger.mock.calls;
  234. return promise.then(() => {
  235. let arg = calls[calls.length - 1][0];
  236. expect(arg).toMatchObject({
  237. loading: false,
  238. error: true,
  239. mergeState: new Map(),
  240. mergedItems: [],
  241. unmergeState: new Map(),
  242. });
  243. });
  244. });
  245. });
  246. describe('Similar Issues list (to be merged)', function() {
  247. let mergeList;
  248. let mergeState;
  249. beforeEach(function() {
  250. mergeList = new Set();
  251. mergeState = new Map();
  252. return GroupingStore.onFetch([
  253. {dataKey: 'similar', endpoint: '/issues/groupId/similar/'},
  254. ]);
  255. });
  256. describe('onToggleMerge (checkbox state)', function() {
  257. // Attempt to check first item but its "locked" so should not be able to do anything
  258. it('can check and uncheck item', function() {
  259. GroupingStore.onToggleMerge('1');
  260. mergeList.add('1');
  261. mergeState.set('1', {checked: true});
  262. expect(GroupingStore.mergeList).toEqual(mergeList);
  263. expect(GroupingStore.mergeState).toEqual(mergeState);
  264. // Uncheck
  265. GroupingStore.onToggleMerge('1');
  266. mergeList.delete('1');
  267. mergeState.set('1', {checked: false});
  268. // Check all
  269. GroupingStore.onToggleMerge('1');
  270. GroupingStore.onToggleMerge('2');
  271. GroupingStore.onToggleMerge('3');
  272. mergeList.add('1');
  273. mergeList.add('2');
  274. mergeList.add('3');
  275. mergeState.set('1', {checked: true});
  276. mergeState.set('2', {checked: true});
  277. mergeState.set('3', {checked: true});
  278. expect(GroupingStore.mergeList).toEqual(mergeList);
  279. expect(GroupingStore.mergeState).toEqual(mergeState);
  280. expect(trigger).toHaveBeenLastCalledWith({
  281. mergeDisabled: false,
  282. mergeList,
  283. mergeState,
  284. });
  285. });
  286. });
  287. describe('onMerge', function() {
  288. beforeEach(function() {
  289. jest.spyOn(Client.prototype, 'merge');
  290. Client.clearMockResponses();
  291. Client.addMockResponse({
  292. method: 'PUT',
  293. url: '/projects/orgId/projectId/issues/',
  294. });
  295. });
  296. afterEach(function() {});
  297. it('disables rows to be merged', async function() {
  298. trigger.mockReset();
  299. GroupingStore.onToggleMerge('1');
  300. mergeList.add('1');
  301. mergeState.set('1', {checked: true});
  302. expect(trigger).toHaveBeenLastCalledWith({
  303. mergeDisabled: false,
  304. mergeList,
  305. mergeState,
  306. });
  307. trigger.mockReset();
  308. // Everything is sync so trigger will have been called multiple times
  309. let promise = GroupingStore.onMerge({
  310. params: {
  311. orgId: 'orgId',
  312. projectId: 'projectId',
  313. groupId: 'groupId',
  314. },
  315. });
  316. mergeState.set('1', {checked: true, busy: true});
  317. expect(trigger).toHaveBeenCalledWith({
  318. mergeDisabled: true,
  319. mergeList,
  320. mergeState,
  321. });
  322. await promise;
  323. expect(Client.prototype.merge).toHaveBeenCalledWith(
  324. {
  325. orgId: 'orgId',
  326. projectId: 'projectId',
  327. itemIds: ['1', 'groupId'],
  328. query: undefined,
  329. },
  330. {
  331. error: expect.any(Function),
  332. success: expect.any(Function),
  333. complete: expect.any(Function),
  334. }
  335. );
  336. // Should be removed from mergeList after merged
  337. mergeList.delete('1');
  338. mergeState.set('1', {checked: false, busy: true});
  339. expect(trigger).toHaveBeenLastCalledWith({
  340. mergeDisabled: false,
  341. mergeList,
  342. mergeState,
  343. });
  344. });
  345. it('keeps rows in "busy" state and unchecks after successfully adding to merge queue', async function() {
  346. GroupingStore.onToggleMerge('1');
  347. mergeList.add('1');
  348. mergeState.set('1', {checked: true});
  349. // Expect checked
  350. expect(trigger).toHaveBeenCalledWith({
  351. mergeDisabled: false,
  352. mergeList,
  353. mergeState,
  354. });
  355. trigger.mockReset();
  356. // Start unmerge
  357. let promise = GroupingStore.onMerge({
  358. params: {
  359. orgId: 'orgId',
  360. projectId: 'projectId',
  361. groupId: 'groupId',
  362. },
  363. });
  364. mergeState.set('1', {checked: true, busy: true});
  365. // Expect checked to remain the same, but is now busy
  366. expect(trigger).toHaveBeenCalledWith({
  367. mergeDisabled: true,
  368. mergeList,
  369. mergeState,
  370. });
  371. await promise;
  372. mergeState.set('1', {checked: false, busy: true});
  373. // After promise, reset checked to false, but keep busy
  374. expect(trigger).toHaveBeenLastCalledWith({
  375. mergeDisabled: false,
  376. mergeList: new Set(),
  377. mergeState,
  378. });
  379. });
  380. it('resets busy state and has same items checked after error when trying to merge', async function() {
  381. Client.clearMockResponses();
  382. Client.addMockResponse({
  383. method: 'PUT',
  384. url: '/projects/orgId/projectId/issues/',
  385. statusCode: 500,
  386. body: {},
  387. });
  388. GroupingStore.onToggleMerge('1');
  389. mergeList.add('1');
  390. mergeState.set('1', {checked: true});
  391. let promise = GroupingStore.onMerge({
  392. params: {
  393. orgId: 'orgId',
  394. projectId: 'projectId',
  395. groupId: 'groupId',
  396. },
  397. });
  398. mergeState.set('1', {checked: true, busy: true});
  399. expect(trigger).toHaveBeenCalledWith({
  400. mergeDisabled: true,
  401. mergeList,
  402. mergeState,
  403. });
  404. await promise;
  405. // Error state
  406. mergeState.set('1', {checked: true, busy: false});
  407. expect(trigger).toHaveBeenLastCalledWith({
  408. mergeDisabled: false,
  409. mergeList,
  410. mergeState,
  411. });
  412. });
  413. });
  414. });
  415. describe('Hashes list (to be unmerged)', function() {
  416. let unmergeList;
  417. let unmergeState;
  418. beforeEach(async function() {
  419. unmergeList = new Map();
  420. unmergeState = new Map();
  421. await GroupingStore.onFetch([
  422. {dataKey: 'merged', endpoint: '/issues/groupId/hashes/'},
  423. ]);
  424. trigger.mockClear();
  425. unmergeState = new Map([...GroupingStore.unmergeState]);
  426. });
  427. describe('onToggleUnmerge (checkbox state for hashes)', function() {
  428. // Attempt to check first item but its "locked" so should not be able to do anything
  429. it('can not check locked item', function() {
  430. GroupingStore.onToggleUnmerge('1');
  431. expect(GroupingStore.unmergeList).toEqual(unmergeList);
  432. expect(GroupingStore.unmergeState).toEqual(unmergeState);
  433. expect(trigger).not.toHaveBeenCalled();
  434. });
  435. it('can check and uncheck unlocked items', function() {
  436. // Check
  437. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  438. unmergeList.set('2', 'event-2');
  439. unmergeState.set('2', {busy: false, checked: true});
  440. expect(GroupingStore.unmergeList).toEqual(unmergeList);
  441. expect(GroupingStore.unmergeState).toEqual(unmergeState);
  442. // Uncheck
  443. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  444. unmergeList.delete('2');
  445. unmergeState.set('2', {busy: false, checked: false});
  446. expect(GroupingStore.unmergeList).toEqual(unmergeList);
  447. expect(GroupingStore.unmergeState).toEqual(unmergeState);
  448. // Check
  449. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  450. unmergeList.set('2', 'event-2');
  451. unmergeState.set('2', {busy: false, checked: true});
  452. expect(GroupingStore.unmergeList).toEqual(unmergeList);
  453. expect(GroupingStore.unmergeState).toEqual(unmergeState);
  454. expect(trigger).toHaveBeenLastCalledWith({
  455. enableFingerprintCompare: false,
  456. unmergeLastCollapsed: false,
  457. unmergeDisabled: false,
  458. unmergeList,
  459. unmergeState,
  460. });
  461. });
  462. it('should have Compare button enabled only when two fingerprints are checked', function() {
  463. expect(GroupingStore.enableFingerprintCompare).toBe(false);
  464. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  465. GroupingStore.onToggleUnmerge(['3', 'event-3']);
  466. expect(GroupingStore.enableFingerprintCompare).toBe(true);
  467. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  468. expect(GroupingStore.enableFingerprintCompare).toBe(false);
  469. });
  470. it('selecting all available checkboxes should disable the unmerge button and re-enable when unchecking', function() {
  471. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  472. GroupingStore.onToggleUnmerge(['3', 'event-3']);
  473. GroupingStore.onToggleUnmerge(['4', 'event-4']);
  474. unmergeList.set('2', 'event-2');
  475. unmergeList.set('3', 'event-3');
  476. unmergeList.set('4', 'event-4');
  477. unmergeState.set('2', {busy: false, checked: true});
  478. unmergeState.set('3', {busy: false, checked: true});
  479. unmergeState.set('4', {busy: false, checked: true});
  480. expect(GroupingStore.unmergeList).toEqual(unmergeList);
  481. expect(GroupingStore.unmergeState).toEqual(unmergeState);
  482. expect(GroupingStore.unmergeDisabled).toBe(true);
  483. // Unchecking
  484. GroupingStore.onToggleUnmerge(['4', 'event-4']);
  485. unmergeList.delete('4');
  486. unmergeState.set('4', {busy: false, checked: false});
  487. expect(GroupingStore.unmergeList).toEqual(unmergeList);
  488. expect(GroupingStore.unmergeState).toEqual(unmergeState);
  489. expect(GroupingStore.unmergeDisabled).toBe(false);
  490. expect(trigger).toHaveBeenLastCalledWith({
  491. enableFingerprintCompare: true,
  492. unmergeLastCollapsed: false,
  493. unmergeDisabled: false,
  494. unmergeList,
  495. unmergeState,
  496. });
  497. });
  498. });
  499. describe('onUnmerge', function() {
  500. beforeAll(function() {
  501. GroupingStore.init();
  502. });
  503. beforeEach(function() {
  504. Client.clearMockResponses();
  505. Client.addMockResponse({
  506. method: 'DELETE',
  507. url: '/issues/groupId/hashes/',
  508. });
  509. });
  510. afterEach(function() {});
  511. it('can not toggle unmerge for a locked item', function() {
  512. // Event 1 is locked
  513. GroupingStore.onToggleUnmerge(['1', 'event-1']);
  514. unmergeState.set('1', {busy: true});
  515. // trigger does NOT get called because an item returned via API is in a "locked" state
  516. expect(trigger).not.toHaveBeenCalled();
  517. GroupingStore.onUnmerge({
  518. groupId: 'groupId',
  519. });
  520. expect(trigger).toHaveBeenCalledWith({
  521. enableFingerprintCompare: false,
  522. unmergeLastCollapsed: false,
  523. unmergeDisabled: true,
  524. unmergeList,
  525. unmergeState,
  526. });
  527. });
  528. it('disables rows to be merged', async function() {
  529. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  530. unmergeList.set('2', 'event-2');
  531. unmergeState.set('2', {checked: true, busy: false});
  532. // trigger does NOT get called because an item returned via API is in a "locked" state
  533. expect(trigger).toHaveBeenCalledWith({
  534. enableFingerprintCompare: false,
  535. unmergeLastCollapsed: false,
  536. unmergeDisabled: false,
  537. unmergeList,
  538. unmergeState,
  539. });
  540. let promise = GroupingStore.onUnmerge({
  541. groupId: 'groupId',
  542. });
  543. unmergeState.set('2', {checked: false, busy: true});
  544. expect(trigger).toHaveBeenCalledWith({
  545. enableFingerprintCompare: false,
  546. unmergeLastCollapsed: false,
  547. unmergeDisabled: true,
  548. unmergeList,
  549. unmergeState,
  550. });
  551. await promise;
  552. // Success
  553. unmergeState.set('2', {checked: false, busy: true});
  554. unmergeList.delete('2');
  555. expect(trigger).toHaveBeenLastCalledWith({
  556. enableFingerprintCompare: false,
  557. unmergeLastCollapsed: false,
  558. unmergeDisabled: false,
  559. unmergeList,
  560. unmergeState,
  561. });
  562. });
  563. it('keeps rows in "busy" state and unchecks after successfully adding to unmerge queue', async function() {
  564. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  565. unmergeList.set('2', 'event-2');
  566. unmergeState.set('2', {checked: true, busy: false});
  567. let promise = GroupingStore.onUnmerge({
  568. groupId: 'groupId',
  569. });
  570. unmergeState.set('2', {checked: false, busy: true});
  571. expect(trigger).toHaveBeenCalledWith({
  572. enableFingerprintCompare: false,
  573. unmergeLastCollapsed: false,
  574. unmergeDisabled: true,
  575. unmergeList,
  576. unmergeState,
  577. });
  578. await promise;
  579. expect(trigger).toHaveBeenLastCalledWith({
  580. enableFingerprintCompare: false,
  581. unmergeLastCollapsed: false,
  582. unmergeDisabled: false,
  583. unmergeList: new Map(),
  584. unmergeState,
  585. });
  586. });
  587. it('resets busy state and has same items checked after error when trying to merge', async function() {
  588. Client.clearMockResponses();
  589. Client.addMockResponse({
  590. method: 'DELETE',
  591. url: '/issues/groupId/hashes/',
  592. statusCode: 500,
  593. body: {},
  594. });
  595. GroupingStore.onToggleUnmerge(['2', 'event-2']);
  596. unmergeList.set('2', 'event-2');
  597. let promise = GroupingStore.onUnmerge({
  598. groupId: 'groupId',
  599. });
  600. unmergeState.set('2', {checked: false, busy: true});
  601. expect(trigger).toHaveBeenCalledWith({
  602. enableFingerprintCompare: false,
  603. unmergeLastCollapsed: false,
  604. unmergeDisabled: true,
  605. unmergeList,
  606. unmergeState,
  607. });
  608. await promise;
  609. unmergeState.set('2', {checked: true, busy: false});
  610. expect(trigger).toHaveBeenLastCalledWith({
  611. enableFingerprintCompare: false,
  612. unmergeLastCollapsed: false,
  613. unmergeDisabled: false,
  614. unmergeList,
  615. unmergeState,
  616. });
  617. });
  618. });
  619. });
  620. });