index.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import {Fragment} from 'react';
  2. import {EventStacktraceExceptionFixture} from 'sentry-fixture/eventStacktraceException';
  3. import {GroupFixture} from 'sentry-fixture/group';
  4. import {OrganizationFixture} from 'sentry-fixture/organization';
  5. import {ProjectFixture} from 'sentry-fixture/project';
  6. import {
  7. render,
  8. screen,
  9. userEvent,
  10. waitFor,
  11. within,
  12. } from 'sentry-test/reactTestingLibrary';
  13. import GlobalModal from 'sentry/components/globalModal';
  14. import ConfigStore from 'sentry/stores/configStore';
  15. import ModalStore from 'sentry/stores/modalStore';
  16. import {GroupStatus, IssueCategory} from 'sentry/types/group';
  17. import * as analytics from 'sentry/utils/analytics';
  18. import {browserHistory} from 'sentry/utils/browserHistory';
  19. import GroupActions from 'sentry/views/issueDetails/actions';
  20. const project = ProjectFixture({
  21. id: '2448',
  22. name: 'project name',
  23. slug: 'project',
  24. });
  25. const group = GroupFixture({
  26. id: '1337',
  27. pluginActions: [],
  28. pluginIssues: [],
  29. issueCategory: IssueCategory.ERROR,
  30. project,
  31. });
  32. const issuePlatformGroup = GroupFixture({
  33. id: '1338',
  34. issueCategory: IssueCategory.PERFORMANCE,
  35. project,
  36. });
  37. const organization = OrganizationFixture({
  38. id: '4660',
  39. slug: 'org',
  40. });
  41. describe('GroupActions', function () {
  42. const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics');
  43. beforeEach(function () {
  44. ConfigStore.init();
  45. });
  46. afterEach(function () {
  47. MockApiClient.clearMockResponses();
  48. jest.clearAllMocks();
  49. });
  50. describe('render()', function () {
  51. it('renders correctly', async function () {
  52. render(
  53. <GroupActions
  54. group={group}
  55. project={project}
  56. organization={organization}
  57. disabled={false}
  58. event={null}
  59. />
  60. );
  61. expect(await screen.findByRole('button', {name: 'Resolve'})).toBeInTheDocument();
  62. });
  63. });
  64. describe('subscribing', function () {
  65. let issuesApi: any;
  66. beforeEach(function () {
  67. issuesApi = MockApiClient.addMockResponse({
  68. url: '/projects/org/project/issues/',
  69. method: 'PUT',
  70. body: GroupFixture({isSubscribed: false}),
  71. });
  72. });
  73. it('can subscribe', async function () {
  74. render(
  75. <GroupActions
  76. group={group}
  77. project={project}
  78. organization={organization}
  79. disabled={false}
  80. event={null}
  81. />
  82. );
  83. await userEvent.click(screen.getByRole('button', {name: 'Subscribe'}));
  84. expect(issuesApi).toHaveBeenCalledWith(
  85. expect.anything(),
  86. expect.objectContaining({
  87. data: {isSubscribed: true},
  88. })
  89. );
  90. });
  91. });
  92. describe('bookmarking', function () {
  93. let issuesApi: any;
  94. beforeEach(function () {
  95. issuesApi = MockApiClient.addMockResponse({
  96. url: '/projects/org/project/issues/',
  97. method: 'PUT',
  98. body: GroupFixture({isBookmarked: false}),
  99. });
  100. });
  101. it('can bookmark', async function () {
  102. render(
  103. <GroupActions
  104. group={group}
  105. project={project}
  106. organization={organization}
  107. disabled={false}
  108. event={null}
  109. />
  110. );
  111. await userEvent.click(screen.getByLabelText('More Actions'));
  112. const bookmark = await screen.findByTestId('bookmark');
  113. await userEvent.click(bookmark);
  114. expect(issuesApi).toHaveBeenCalledWith(
  115. expect.anything(),
  116. expect.objectContaining({
  117. data: {isBookmarked: true},
  118. })
  119. );
  120. });
  121. });
  122. describe('reprocessing', function () {
  123. it('renders ReprocessAction component if org has native exception event', async function () {
  124. const event = EventStacktraceExceptionFixture({
  125. platform: 'native',
  126. });
  127. render(
  128. <GroupActions
  129. group={group}
  130. project={project}
  131. organization={organization}
  132. event={event}
  133. disabled={false}
  134. />
  135. );
  136. await userEvent.click(screen.getByLabelText('More Actions'));
  137. const reprocessActionButton = await screen.findByTestId('reprocess');
  138. expect(reprocessActionButton).toBeInTheDocument();
  139. });
  140. it('open dialog by clicking on the ReprocessAction component', async function () {
  141. const event = EventStacktraceExceptionFixture({
  142. platform: 'native',
  143. });
  144. render(
  145. <GroupActions
  146. group={group}
  147. project={project}
  148. organization={organization}
  149. event={event}
  150. disabled={false}
  151. />
  152. );
  153. const onReprocessEventFunc = jest.spyOn(ModalStore, 'openModal');
  154. await userEvent.click(screen.getByLabelText('More Actions'));
  155. const reprocessActionButton = await screen.findByTestId('reprocess');
  156. expect(reprocessActionButton).toBeInTheDocument();
  157. await userEvent.click(reprocessActionButton);
  158. await waitFor(() => expect(onReprocessEventFunc).toHaveBeenCalled());
  159. });
  160. });
  161. it('opens share modal from more actions dropdown', async () => {
  162. const org = {
  163. ...organization,
  164. features: ['shared-issues'],
  165. };
  166. const updateMock = MockApiClient.addMockResponse({
  167. url: `/projects/${org.slug}/${project.slug}/issues/`,
  168. method: 'PUT',
  169. body: {},
  170. });
  171. render(
  172. <Fragment>
  173. <GlobalModal />
  174. <GroupActions
  175. group={group}
  176. project={project}
  177. organization={org}
  178. disabled={false}
  179. event={null}
  180. />
  181. </Fragment>,
  182. {organization: org}
  183. );
  184. await userEvent.click(screen.getByLabelText('More Actions'));
  185. await userEvent.click(await screen.findByText('Share'));
  186. const modal = screen.getByRole('dialog');
  187. expect(within(modal).getByText('Share Issue')).toBeInTheDocument();
  188. expect(updateMock).toHaveBeenCalled();
  189. });
  190. describe('delete', function () {
  191. it('opens delete confirm modal from more actions dropdown', async () => {
  192. const org = OrganizationFixture({
  193. ...organization,
  194. access: [...organization.access, 'event:admin'],
  195. });
  196. MockApiClient.addMockResponse({
  197. url: `/projects/${org.slug}/${project.slug}/issues/`,
  198. method: 'PUT',
  199. body: {},
  200. });
  201. const deleteMock = MockApiClient.addMockResponse({
  202. url: `/projects/${org.slug}/${project.slug}/issues/`,
  203. method: 'DELETE',
  204. body: {},
  205. });
  206. render(
  207. <Fragment>
  208. <GlobalModal />
  209. <GroupActions
  210. group={group}
  211. project={project}
  212. organization={org}
  213. disabled={false}
  214. event={null}
  215. />
  216. </Fragment>,
  217. {organization: org}
  218. );
  219. await userEvent.click(screen.getByLabelText('More Actions'));
  220. await userEvent.click(await screen.findByRole('menuitemradio', {name: 'Delete'}));
  221. const modal = screen.getByRole('dialog');
  222. expect(
  223. within(modal).getByText(/Deleting this issue is permanent/)
  224. ).toBeInTheDocument();
  225. await userEvent.click(within(modal).getByRole('button', {name: 'Delete'}));
  226. expect(deleteMock).toHaveBeenCalled();
  227. expect(browserHistory.push).toHaveBeenCalledWith({
  228. pathname: `/organizations/${org.slug}/issues/`,
  229. query: {project: project.id},
  230. });
  231. });
  232. it('delete for issue platform', async () => {
  233. const org = OrganizationFixture({
  234. access: ['event:admin'], // Delete is only shown if this is present
  235. });
  236. render(
  237. <GroupActions
  238. group={issuePlatformGroup}
  239. project={project}
  240. organization={org}
  241. disabled={false}
  242. event={null}
  243. />,
  244. {organization: org}
  245. );
  246. await userEvent.click(screen.getByLabelText('More Actions'));
  247. expect(await screen.findByTestId('delete-issue')).toHaveAttribute(
  248. 'aria-disabled',
  249. 'true'
  250. );
  251. expect(await screen.findByTestId('delete-and-discard')).toHaveAttribute(
  252. 'aria-disabled',
  253. 'true'
  254. );
  255. });
  256. it('delete for issue platform is enabled with feature flag', async () => {
  257. const org = OrganizationFixture({
  258. access: ['event:admin'],
  259. features: ['issue-platform-deletion-ui'],
  260. });
  261. render(
  262. <GroupActions
  263. group={issuePlatformGroup}
  264. project={project}
  265. organization={org}
  266. disabled={false}
  267. event={null}
  268. />,
  269. {organization: org}
  270. );
  271. await userEvent.click(screen.getByLabelText('More Actions'));
  272. expect(await screen.findByTestId('delete-issue')).not.toHaveAttribute(
  273. 'aria-disabled'
  274. );
  275. expect(await screen.findByTestId('delete-and-discard')).toHaveAttribute(
  276. 'aria-disabled',
  277. 'true'
  278. );
  279. });
  280. });
  281. it('resolves and unresolves issue', async () => {
  282. const issuesApi = MockApiClient.addMockResponse({
  283. url: `/projects/${organization.slug}/project/issues/`,
  284. method: 'PUT',
  285. body: {...group, status: 'resolved'},
  286. });
  287. const {rerender} = render(
  288. <GroupActions
  289. group={group}
  290. project={project}
  291. organization={organization}
  292. disabled={false}
  293. event={null}
  294. />,
  295. {organization}
  296. );
  297. await userEvent.click(screen.getByRole('button', {name: 'Resolve'}));
  298. expect(issuesApi).toHaveBeenCalledWith(
  299. `/projects/${organization.slug}/project/issues/`,
  300. expect.objectContaining({
  301. data: {status: 'resolved', statusDetails: {}, substatus: null},
  302. })
  303. );
  304. expect(analyticsSpy).toHaveBeenCalledWith(
  305. 'issue_details.action_clicked',
  306. expect.objectContaining({
  307. action_type: 'resolved',
  308. })
  309. );
  310. rerender(
  311. <GroupActions
  312. group={{...group, status: GroupStatus.RESOLVED, statusDetails: {}}}
  313. project={project}
  314. organization={organization}
  315. disabled={false}
  316. event={null}
  317. />
  318. );
  319. await userEvent.click(screen.getByRole('button', {name: 'Resolved'}));
  320. expect(issuesApi).toHaveBeenCalledWith(
  321. `/projects/${organization.slug}/project/issues/`,
  322. expect.objectContaining({
  323. data: {status: 'unresolved', statusDetails: {}, substatus: 'ongoing'},
  324. })
  325. );
  326. });
  327. it('can archive issue', async () => {
  328. const issuesApi = MockApiClient.addMockResponse({
  329. url: `/projects/${organization.slug}/project/issues/`,
  330. method: 'PUT',
  331. body: {...group, status: 'resolved'},
  332. });
  333. render(
  334. <GroupActions
  335. group={group}
  336. project={project}
  337. organization={organization}
  338. disabled={false}
  339. event={null}
  340. />,
  341. {organization}
  342. );
  343. await userEvent.click(await screen.findByRole('button', {name: 'Archive'}));
  344. expect(issuesApi).toHaveBeenCalledWith(
  345. expect.anything(),
  346. expect.objectContaining({
  347. data: {
  348. status: 'ignored',
  349. statusDetails: {},
  350. substatus: 'archived_until_escalating',
  351. },
  352. })
  353. );
  354. expect(analyticsSpy).toHaveBeenCalledWith(
  355. 'issue_details.action_clicked',
  356. expect.objectContaining({
  357. action_substatus: 'archived_until_escalating',
  358. action_type: 'ignored',
  359. })
  360. );
  361. });
  362. });