index.spec.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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 {RouterFixture} from 'sentry-fixture/routerFixture';
  7. import {
  8. render,
  9. screen,
  10. userEvent,
  11. waitFor,
  12. within,
  13. } from 'sentry-test/reactTestingLibrary';
  14. import GlobalModal from 'sentry/components/globalModal';
  15. import ConfigStore from 'sentry/stores/configStore';
  16. import ModalStore from 'sentry/stores/modalStore';
  17. import {GroupStatus, IssueCategory} from 'sentry/types/group';
  18. import * as analytics from 'sentry/utils/analytics';
  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 group={group} project={project} disabled={false} event={null} />,
  54. {organization}
  55. );
  56. expect(await screen.findByRole('button', {name: 'Resolve'})).toBeInTheDocument();
  57. });
  58. });
  59. describe('subscribing', function () {
  60. let issuesApi: any;
  61. beforeEach(function () {
  62. issuesApi = MockApiClient.addMockResponse({
  63. url: '/projects/org/project/issues/',
  64. method: 'PUT',
  65. body: GroupFixture({isSubscribed: false}),
  66. });
  67. });
  68. it('can subscribe', async function () {
  69. render(
  70. <GroupActions group={group} project={project} disabled={false} event={null} />,
  71. {organization}
  72. );
  73. await userEvent.click(screen.getByRole('button', {name: 'Subscribe'}));
  74. expect(issuesApi).toHaveBeenCalledWith(
  75. expect.anything(),
  76. expect.objectContaining({
  77. data: {isSubscribed: true},
  78. })
  79. );
  80. });
  81. });
  82. describe('bookmarking', function () {
  83. let issuesApi: any;
  84. beforeEach(function () {
  85. issuesApi = MockApiClient.addMockResponse({
  86. url: '/projects/org/project/issues/',
  87. method: 'PUT',
  88. body: GroupFixture({isBookmarked: false}),
  89. });
  90. });
  91. it('can bookmark', async function () {
  92. render(
  93. <GroupActions group={group} project={project} disabled={false} event={null} />,
  94. {organization}
  95. );
  96. await userEvent.click(screen.getByLabelText('More Actions'));
  97. const bookmark = await screen.findByTestId('bookmark');
  98. await userEvent.click(bookmark);
  99. expect(issuesApi).toHaveBeenCalledWith(
  100. expect.anything(),
  101. expect.objectContaining({
  102. data: {isBookmarked: true},
  103. })
  104. );
  105. });
  106. });
  107. describe('reprocessing', function () {
  108. it('renders ReprocessAction component if org has native exception event', async function () {
  109. const event = EventStacktraceExceptionFixture({
  110. platform: 'native',
  111. });
  112. render(
  113. <GroupActions group={group} project={project} event={event} disabled={false} />,
  114. {organization}
  115. );
  116. await userEvent.click(screen.getByLabelText('More Actions'));
  117. const reprocessActionButton = await screen.findByTestId('reprocess');
  118. expect(reprocessActionButton).toBeInTheDocument();
  119. });
  120. it('open dialog by clicking on the ReprocessAction component', async function () {
  121. const event = EventStacktraceExceptionFixture({
  122. platform: 'native',
  123. });
  124. render(
  125. <GroupActions group={group} project={project} event={event} disabled={false} />,
  126. {organization}
  127. );
  128. const onReprocessEventFunc = jest.spyOn(ModalStore, 'openModal');
  129. await userEvent.click(screen.getByLabelText('More Actions'));
  130. const reprocessActionButton = await screen.findByTestId('reprocess');
  131. expect(reprocessActionButton).toBeInTheDocument();
  132. await userEvent.click(reprocessActionButton);
  133. await waitFor(() => expect(onReprocessEventFunc).toHaveBeenCalled());
  134. });
  135. });
  136. it('opens share modal from more actions dropdown', async () => {
  137. const org = {
  138. ...organization,
  139. features: ['shared-issues'],
  140. };
  141. render(
  142. <Fragment>
  143. <GlobalModal />
  144. <GroupActions group={group} project={project} disabled={false} event={null} />
  145. </Fragment>,
  146. {organization: org}
  147. );
  148. await userEvent.click(screen.getByLabelText('More Actions'));
  149. await userEvent.click(await screen.findByText('Share'));
  150. const modal = screen.getByRole('dialog');
  151. expect(within(modal).getByText('Share Issue')).toBeInTheDocument();
  152. });
  153. describe('delete', function () {
  154. it('opens delete confirm modal from more actions dropdown', async () => {
  155. const router = RouterFixture();
  156. const org = OrganizationFixture({
  157. ...organization,
  158. access: [...organization.access, 'event:admin'],
  159. });
  160. MockApiClient.addMockResponse({
  161. url: `/projects/${org.slug}/${project.slug}/issues/`,
  162. method: 'PUT',
  163. body: {},
  164. });
  165. const deleteMock = MockApiClient.addMockResponse({
  166. url: `/projects/${org.slug}/${project.slug}/issues/`,
  167. method: 'DELETE',
  168. body: {},
  169. });
  170. render(
  171. <Fragment>
  172. <GlobalModal />
  173. <GroupActions group={group} project={project} disabled={false} event={null} />
  174. </Fragment>,
  175. {router, organization: org}
  176. );
  177. await userEvent.click(screen.getByLabelText('More Actions'));
  178. await userEvent.click(await screen.findByRole('menuitemradio', {name: 'Delete'}));
  179. const modal = screen.getByRole('dialog');
  180. expect(
  181. within(modal).getByText(/Deleting this issue is permanent/)
  182. ).toBeInTheDocument();
  183. await userEvent.click(within(modal).getByRole('button', {name: 'Delete'}));
  184. expect(deleteMock).toHaveBeenCalled();
  185. expect(router.push).toHaveBeenCalledWith({
  186. pathname: `/organizations/${org.slug}/issues/`,
  187. query: {project: project.id},
  188. });
  189. });
  190. it('delete for issue platform', async () => {
  191. const org = OrganizationFixture({
  192. access: ['event:admin'], // Delete is only shown if this is present
  193. });
  194. render(
  195. <GroupActions
  196. group={issuePlatformGroup}
  197. project={project}
  198. disabled={false}
  199. event={null}
  200. />,
  201. {organization: org}
  202. );
  203. await userEvent.click(screen.getByLabelText('More Actions'));
  204. expect(await screen.findByTestId('delete-issue')).toHaveAttribute(
  205. 'aria-disabled',
  206. 'true'
  207. );
  208. expect(await screen.findByTestId('delete-and-discard')).toHaveAttribute(
  209. 'aria-disabled',
  210. 'true'
  211. );
  212. });
  213. it('delete for issue platform is enabled with feature flag', async () => {
  214. const org = OrganizationFixture({
  215. access: ['event:admin'],
  216. features: ['issue-platform-deletion-ui'],
  217. });
  218. render(
  219. <GroupActions
  220. group={issuePlatformGroup}
  221. project={project}
  222. disabled={false}
  223. event={null}
  224. />,
  225. {organization: org}
  226. );
  227. await userEvent.click(screen.getByLabelText('More Actions'));
  228. expect(await screen.findByTestId('delete-issue')).not.toHaveAttribute(
  229. 'aria-disabled'
  230. );
  231. expect(await screen.findByTestId('delete-and-discard')).toHaveAttribute(
  232. 'aria-disabled',
  233. 'true'
  234. );
  235. });
  236. });
  237. it('resolves and unresolves issue', async () => {
  238. const issuesApi = MockApiClient.addMockResponse({
  239. url: `/projects/${organization.slug}/project/issues/`,
  240. method: 'PUT',
  241. body: {...group, status: 'resolved'},
  242. });
  243. const {rerender} = render(
  244. <GroupActions group={group} project={project} disabled={false} event={null} />,
  245. {organization}
  246. );
  247. await userEvent.click(screen.getByRole('button', {name: 'Resolve'}));
  248. expect(issuesApi).toHaveBeenCalledWith(
  249. `/projects/${organization.slug}/project/issues/`,
  250. expect.objectContaining({
  251. data: {status: 'resolved', statusDetails: {}, substatus: null},
  252. })
  253. );
  254. expect(analyticsSpy).toHaveBeenCalledWith(
  255. 'issue_details.action_clicked',
  256. expect.objectContaining({
  257. action_type: 'resolved',
  258. })
  259. );
  260. rerender(
  261. <GroupActions
  262. group={{...group, status: GroupStatus.RESOLVED, statusDetails: {}}}
  263. project={project}
  264. disabled={false}
  265. event={null}
  266. />
  267. );
  268. await userEvent.click(screen.getByRole('button', {name: 'Resolved'}));
  269. expect(issuesApi).toHaveBeenCalledWith(
  270. `/projects/${organization.slug}/project/issues/`,
  271. expect.objectContaining({
  272. data: {status: 'unresolved', statusDetails: {}, substatus: 'ongoing'},
  273. })
  274. );
  275. });
  276. it('can archive issue', async () => {
  277. const issuesApi = MockApiClient.addMockResponse({
  278. url: `/projects/${organization.slug}/project/issues/`,
  279. method: 'PUT',
  280. body: {...group, status: 'resolved'},
  281. });
  282. render(
  283. <GroupActions group={group} project={project} disabled={false} event={null} />,
  284. {organization}
  285. );
  286. await userEvent.click(await screen.findByRole('button', {name: 'Archive'}));
  287. expect(issuesApi).toHaveBeenCalledWith(
  288. expect.anything(),
  289. expect.objectContaining({
  290. data: {
  291. status: 'ignored',
  292. statusDetails: {},
  293. substatus: 'archived_until_escalating',
  294. },
  295. })
  296. );
  297. expect(analyticsSpy).toHaveBeenCalledWith(
  298. 'issue_details.action_clicked',
  299. expect.objectContaining({
  300. action_substatus: 'archived_until_escalating',
  301. action_type: 'ignored',
  302. })
  303. );
  304. });
  305. });