groupingStore.spec.jsx 21 KB

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