actions.spec.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import {act} from 'react-dom/test-utils';
  2. import {selectDropdownMenuItem} from 'sentry-test/dropdownMenu';
  3. import {mountWithTheme} from 'sentry-test/enzyme';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {mountGlobalModal} from 'sentry-test/modal';
  6. import {selectByLabel} from 'sentry-test/select-new';
  7. import {triggerPress} from 'sentry-test/utils';
  8. import GroupStore from 'sentry/stores/groupStore';
  9. import SelectedGroupStore from 'sentry/stores/selectedGroupStore';
  10. import {IssueListActions} from 'sentry/views/issueList/actions';
  11. describe('IssueListActions', function () {
  12. let actions;
  13. let actionsWrapper;
  14. let wrapper;
  15. afterEach(() => {
  16. jest.restoreAllMocks();
  17. });
  18. describe('Bulk', function () {
  19. describe('Total results greater than bulk limit', function () {
  20. beforeAll(function () {
  21. const {routerContext, org} = initializeOrg();
  22. SelectedGroupStore.records = {};
  23. SelectedGroupStore.add(['1', '2', '3']);
  24. wrapper = mountWithTheme(
  25. <IssueListActions
  26. api={new MockApiClient()}
  27. allResultsVisible={false}
  28. query=""
  29. queryCount={1500}
  30. organization={org}
  31. projectId="project-slug"
  32. selection={{
  33. projects: [1],
  34. environments: [],
  35. datetime: {start: null, end: null, period: null, utc: true},
  36. }}
  37. groupIds={['1', '2', '3']}
  38. onRealtimeChange={function () {}}
  39. onSelectStatsPeriod={function () {}}
  40. realtimeActive={false}
  41. statsPeriod="24h"
  42. />,
  43. routerContext
  44. );
  45. });
  46. afterAll(() => {
  47. wrapper.unmount();
  48. });
  49. it('after checking "Select all" checkbox, displays bulk select message', function () {
  50. wrapper.find('ActionsCheckbox Checkbox').simulate('change');
  51. expect(wrapper.find('SelectAllNotice')).toSnapshot();
  52. });
  53. it('can bulk select', function () {
  54. wrapper.find('SelectAllNotice').find('a').simulate('click');
  55. expect(wrapper.find('SelectAllNotice')).toSnapshot();
  56. });
  57. it('bulk resolves', async function () {
  58. const apiMock = MockApiClient.addMockResponse({
  59. url: '/organizations/org-slug/issues/',
  60. method: 'PUT',
  61. });
  62. wrapper.find('ResolveActions ResolveButton button').simulate('click');
  63. const modal = await mountGlobalModal();
  64. expect(modal.find('Modal')).toSnapshot();
  65. modal.find('Button[priority="primary"]').simulate('click');
  66. expect(apiMock).toHaveBeenCalledWith(
  67. expect.anything(),
  68. expect.objectContaining({
  69. query: {
  70. project: [1],
  71. },
  72. data: {status: 'resolved', statusDetails: {}},
  73. })
  74. );
  75. await tick();
  76. wrapper.update();
  77. });
  78. });
  79. describe('Total results less than bulk limit', function () {
  80. beforeAll(function () {
  81. SelectedGroupStore.records = {};
  82. SelectedGroupStore.add(['1', '2', '3']);
  83. wrapper = mountWithTheme(
  84. <IssueListActions
  85. api={new MockApiClient()}
  86. allResultsVisible={false}
  87. query=""
  88. queryCount={600}
  89. organization={TestStubs.Organization()}
  90. projectId="1"
  91. selection={{
  92. projects: [1],
  93. environments: [],
  94. datetime: {start: null, end: null, period: null, utc: true},
  95. }}
  96. groupIds={['1', '2', '3']}
  97. onRealtimeChange={function () {}}
  98. onSelectStatsPeriod={function () {}}
  99. realtimeActive={false}
  100. statsPeriod="24h"
  101. />
  102. );
  103. });
  104. afterAll(() => {
  105. wrapper.unmount();
  106. });
  107. it('after checking "Select all" checkbox, displays bulk select message', function () {
  108. wrapper.find('ActionsCheckbox Checkbox').simulate('change');
  109. expect(wrapper.find('SelectAllNotice')).toSnapshot();
  110. });
  111. it('can bulk select', function () {
  112. wrapper.find('SelectAllNotice').find('a').simulate('click');
  113. expect(wrapper.find('SelectAllNotice')).toSnapshot();
  114. });
  115. it('bulk resolves', async function () {
  116. const apiMock = MockApiClient.addMockResponse({
  117. url: '/organizations/org-slug/issues/',
  118. method: 'PUT',
  119. });
  120. wrapper.find('ResolveActions ResolveButton button').simulate('click');
  121. const modal = await mountGlobalModal();
  122. expect(modal.find('Modal')).toSnapshot();
  123. modal.find('Button[priority="primary"]').simulate('click');
  124. expect(apiMock).toHaveBeenCalledWith(
  125. expect.anything(),
  126. expect.objectContaining({
  127. query: {
  128. project: [1],
  129. },
  130. data: {status: 'resolved', statusDetails: {}},
  131. })
  132. );
  133. await tick();
  134. wrapper.update();
  135. });
  136. });
  137. describe('Selected on page', function () {
  138. beforeAll(function () {
  139. SelectedGroupStore.records = {};
  140. SelectedGroupStore.add(['1', '2', '3']);
  141. wrapper = mountWithTheme(
  142. <IssueListActions
  143. api={new MockApiClient()}
  144. allResultsVisible
  145. query=""
  146. queryCount={15}
  147. organization={TestStubs.Organization()}
  148. projectId="1"
  149. selection={{
  150. projects: [1],
  151. environments: [],
  152. datetime: {start: null, end: null, period: null, utc: true},
  153. }}
  154. groupIds={['1', '2', '3', '6', '9']}
  155. onRealtimeChange={function () {}}
  156. onSelectStatsPeriod={function () {}}
  157. realtimeActive={false}
  158. statsPeriod="24h"
  159. />
  160. );
  161. });
  162. afterAll(() => {
  163. wrapper.unmount();
  164. });
  165. it('resolves selected items', function () {
  166. const apiMock = MockApiClient.addMockResponse({
  167. url: '/organizations/org-slug/issues/',
  168. method: 'PUT',
  169. });
  170. jest
  171. .spyOn(SelectedGroupStore, 'getSelectedIds')
  172. .mockImplementation(() => new Set(['3', '6', '9']));
  173. wrapper
  174. .find('IssueListActions')
  175. .setState({allInQuerySelected: false, anySelected: true});
  176. wrapper.find('ResolveActions ResolveButton button').first().simulate('click');
  177. expect(apiMock).toHaveBeenCalledWith(
  178. expect.anything(),
  179. expect.objectContaining({
  180. query: {
  181. id: ['3', '6', '9'],
  182. project: [1],
  183. },
  184. data: {status: 'resolved', statusDetails: {}},
  185. })
  186. );
  187. });
  188. it('ignores selected items', async function () {
  189. const apiMock = MockApiClient.addMockResponse({
  190. url: '/organizations/org-slug/issues/',
  191. method: 'PUT',
  192. });
  193. jest
  194. .spyOn(SelectedGroupStore, 'getSelectedIds')
  195. .mockImplementation(() => new Set(['1']));
  196. wrapper
  197. .find('IssueListActions')
  198. .setState({allInQuerySelected: false, anySelected: true});
  199. await selectDropdownMenuItem({
  200. wrapper,
  201. specifiers: {prefix: 'IgnoreActions'},
  202. triggerSelector: 'DropdownTrigger',
  203. itemKey: ['until-affect', 'until-affect-custom'],
  204. });
  205. const modal = await mountGlobalModal();
  206. modal
  207. .find('CustomIgnoreCountModal input[label="Number of users"]')
  208. .simulate('change', {target: {value: 300}});
  209. selectByLabel(modal, 'per week', {
  210. name: 'window',
  211. });
  212. modal.find('Button[priority="primary"]').simulate('click');
  213. expect(apiMock).toHaveBeenCalledWith(
  214. expect.anything(),
  215. expect.objectContaining({
  216. query: {
  217. id: ['1'],
  218. project: [1],
  219. },
  220. data: {
  221. status: 'ignored',
  222. statusDetails: {
  223. ignoreUserCount: 300,
  224. ignoreUserWindow: 10080,
  225. },
  226. },
  227. })
  228. );
  229. });
  230. });
  231. });
  232. describe('actionSelectedGroups()', function () {
  233. beforeEach(function () {
  234. jest.spyOn(SelectedGroupStore, 'deselectAll');
  235. actionsWrapper = mountWithTheme(
  236. <IssueListActions
  237. api={new MockApiClient()}
  238. query=""
  239. organization={TestStubs.Organization()}
  240. projectId="1"
  241. selection={{
  242. projects: [1],
  243. environments: [],
  244. datetime: {start: null, end: null, period: null, utc: true},
  245. }}
  246. groupIds={['1', '2', '3']}
  247. onRealtimeChange={function () {}}
  248. onSelectStatsPeriod={function () {}}
  249. realtimeActive={false}
  250. statsPeriod="24h"
  251. />
  252. );
  253. actions = actionsWrapper.instance();
  254. });
  255. afterEach(() => {
  256. actionsWrapper.unmount();
  257. });
  258. describe('for all items', function () {
  259. it("should invoke the callback with 'undefined' and deselect all", function () {
  260. const callback = jest.fn();
  261. actions.state.allInQuerySelected = true;
  262. actions.actionSelectedGroups(callback);
  263. expect(callback).toHaveBeenCalledWith(undefined);
  264. expect(callback).toHaveBeenCalledTimes(1);
  265. expect(SelectedGroupStore.deselectAll).toHaveBeenCalledTimes(1);
  266. // all selected is reset
  267. expect(actions.state.allInQuerySelected).toBe(false);
  268. });
  269. });
  270. describe('for page-selected items', function () {
  271. it('should invoke the callback with an array of selected items and deselect all', function () {
  272. jest
  273. .spyOn(SelectedGroupStore, 'getSelectedIds')
  274. .mockImplementation(() => new Set(['1', '2', '3']));
  275. actions.state.allInQuerySelected = false;
  276. const callback = jest.fn();
  277. actions.actionSelectedGroups(callback);
  278. expect(callback).toHaveBeenCalledWith(['1', '2', '3']);
  279. expect(callback).toHaveBeenCalledTimes(1);
  280. expect(SelectedGroupStore.deselectAll).toHaveBeenCalledTimes(1);
  281. });
  282. });
  283. });
  284. describe('multiple groups from different project', function () {
  285. beforeEach(function () {
  286. jest
  287. .spyOn(SelectedGroupStore, 'getSelectedIds')
  288. .mockImplementation(() => new Set(['1', '2', '3']));
  289. wrapper = mountWithTheme(
  290. <IssueListActions
  291. api={new MockApiClient()}
  292. query=""
  293. organization={TestStubs.Organization()}
  294. groupIds={['1', '2', '3']}
  295. selection={{
  296. projects: [],
  297. environments: [],
  298. datetime: {start: null, end: null, period: null, utc: true},
  299. }}
  300. onRealtimeChange={function () {}}
  301. onSelectStatsPeriod={function () {}}
  302. realtimeActive={false}
  303. statsPeriod="24h"
  304. />
  305. );
  306. });
  307. afterEach(() => {
  308. wrapper.unmount();
  309. });
  310. it('should disable resolve dropdown but not resolve action', function () {
  311. const resolve = wrapper.find('ResolveActions').first();
  312. expect(resolve.props().disabled).toBe(false);
  313. expect(resolve.props().disableDropdown).toBe(true);
  314. });
  315. it('should disable merge button', function () {
  316. expect(
  317. wrapper.find('button[aria-label="Merge Selected Issues"]').props()[
  318. 'aria-disabled'
  319. ]
  320. ).toBe(true);
  321. });
  322. });
  323. describe('mark reviewed', function () {
  324. let issuesApiMock;
  325. beforeEach(() => {
  326. SelectedGroupStore.records = {};
  327. const organization = TestStubs.Organization();
  328. wrapper = mountWithTheme(
  329. <IssueListActions
  330. api={new MockApiClient()}
  331. query=""
  332. organization={organization}
  333. groupIds={['1', '2', '3']}
  334. selection={{
  335. projects: [],
  336. environments: [],
  337. datetime: {start: null, end: null, period: null, utc: true},
  338. }}
  339. onRealtimeChange={function () {}}
  340. onSelectStatsPeriod={function () {}}
  341. realtimeActive={false}
  342. statsPeriod="24h"
  343. queryCount={100}
  344. displayCount="3 of 3"
  345. />
  346. );
  347. MockApiClient.addMockResponse({
  348. url: '/organizations/org-slug/projects/',
  349. body: [TestStubs.Project({slug: 'earth', platform: 'javascript'})],
  350. });
  351. issuesApiMock = MockApiClient.addMockResponse({
  352. url: '/organizations/org-slug/issues/',
  353. method: 'PUT',
  354. });
  355. });
  356. afterEach(() => {
  357. wrapper.unmount();
  358. });
  359. it('acknowledges group', async function () {
  360. await act(async () => {
  361. wrapper.find('IssueListActions').setState({anySelected: true});
  362. await tick();
  363. wrapper.update();
  364. });
  365. SelectedGroupStore.add(['1', '2', '3']);
  366. SelectedGroupStore.toggleSelectAll();
  367. const inbox = {
  368. date_added: '2020-11-24T13:17:42.248751Z',
  369. reason: 0,
  370. reason_details: null,
  371. };
  372. GroupStore.loadInitialData([
  373. TestStubs.Group({id: '1', inbox}),
  374. TestStubs.Group({id: '2', inbox}),
  375. TestStubs.Group({id: '2', inbox}),
  376. ]);
  377. await tick();
  378. wrapper.find('button[aria-label="Mark Reviewed"]').simulate('click');
  379. expect(issuesApiMock).toHaveBeenCalledWith(
  380. expect.anything(),
  381. expect.objectContaining({
  382. data: {inbox: false},
  383. })
  384. );
  385. });
  386. it('mark reviewed disabled for group that is already reviewed', async function () {
  387. await act(async () => {
  388. wrapper.find('IssueListActions').setState({anySelected: true});
  389. await tick();
  390. wrapper.update();
  391. });
  392. SelectedGroupStore.add(['1']);
  393. SelectedGroupStore.toggleSelectAll();
  394. GroupStore.loadInitialData([TestStubs.Group({id: '1', inbox: null})]);
  395. await tick();
  396. expect(
  397. wrapper.find('button[aria-label="Mark Reviewed"]').props()['aria-disabled']
  398. ).toBe(true);
  399. });
  400. });
  401. describe('sort', function () {
  402. let onSortChange;
  403. afterEach(() => {
  404. wrapper.unmount();
  405. });
  406. beforeEach(function () {
  407. const organization = TestStubs.Organization();
  408. onSortChange = jest.fn();
  409. wrapper = mountWithTheme(
  410. <IssueListActions
  411. api={new MockApiClient()}
  412. query=""
  413. organization={organization}
  414. groupIds={['1', '2', '3']}
  415. selection={{
  416. projects: [],
  417. environments: [],
  418. datetime: {start: null, end: null, period: null, utc: true},
  419. }}
  420. onRealtimeChange={function () {}}
  421. onSelectStatsPeriod={function () {}}
  422. onSortChange={onSortChange}
  423. realtimeActive={false}
  424. statsPeriod="24h"
  425. queryCount={100}
  426. displayCount="3 of 3"
  427. sort="date"
  428. />
  429. );
  430. });
  431. it('calls onSortChange with new sort value', async function () {
  432. await act(async () => {
  433. triggerPress(wrapper.find('IssueListSortOptions button'));
  434. await tick();
  435. wrapper.update();
  436. });
  437. wrapper.find('Option').at(3).simulate('click');
  438. expect(onSortChange).toHaveBeenCalledWith('freq');
  439. });
  440. });
  441. });