index.spec.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import {Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {
  4. render,
  5. screen,
  6. userEvent,
  7. waitFor,
  8. within,
  9. } from 'sentry-test/reactTestingLibrary';
  10. import GlobalModal from 'sentry/components/globalModal';
  11. import ConfigStore from 'sentry/stores/configStore';
  12. import ModalStore from 'sentry/stores/modalStore';
  13. import {IssueCategory} from 'sentry/types';
  14. import * as analytics from 'sentry/utils/analytics';
  15. import GroupActions from 'sentry/views/issueDetails/actions';
  16. const project = TestStubs.Project({
  17. id: '2448',
  18. name: 'project name',
  19. slug: 'project',
  20. });
  21. const group = TestStubs.Group({
  22. id: '1337',
  23. pluginActions: [],
  24. pluginIssues: [],
  25. issueCategory: IssueCategory.ERROR,
  26. project,
  27. });
  28. const organization = TestStubs.Organization({
  29. id: '4660',
  30. slug: 'org',
  31. features: ['reprocessing-v2'],
  32. });
  33. describe('GroupActions', function () {
  34. const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics');
  35. beforeEach(function () {
  36. ConfigStore.init();
  37. jest.spyOn(ConfigStore, 'get').mockImplementation(() => []);
  38. });
  39. afterEach(function () {
  40. MockApiClient.clearMockResponses();
  41. jest.clearAllMocks();
  42. });
  43. describe('render()', function () {
  44. it('renders correctly', function () {
  45. render(
  46. <GroupActions
  47. group={group}
  48. project={project}
  49. organization={organization}
  50. disabled={false}
  51. />
  52. );
  53. });
  54. });
  55. describe('subscribing', function () {
  56. let issuesApi: any;
  57. beforeEach(function () {
  58. issuesApi = MockApiClient.addMockResponse({
  59. url: '/projects/org/project/issues/',
  60. method: 'PUT',
  61. body: TestStubs.Group({isSubscribed: false}),
  62. });
  63. });
  64. it('can subscribe', async function () {
  65. render(
  66. <GroupActions
  67. group={group}
  68. project={project}
  69. organization={organization}
  70. disabled={false}
  71. />
  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: TestStubs.Group({isBookmarked: false}),
  89. });
  90. });
  91. it('can bookmark', async function () {
  92. render(
  93. <GroupActions
  94. group={group}
  95. project={project}
  96. organization={organization}
  97. disabled={false}
  98. />
  99. );
  100. await userEvent.click(screen.getByLabelText('More Actions'));
  101. const bookmark = await screen.findByTestId('bookmark');
  102. await userEvent.click(bookmark);
  103. expect(issuesApi).toHaveBeenCalledWith(
  104. expect.anything(),
  105. expect.objectContaining({
  106. data: {isBookmarked: true},
  107. })
  108. );
  109. });
  110. });
  111. describe('reprocessing', function () {
  112. it('renders ReprocessAction component if org has feature flag reprocessing-v2 and native exception event', async function () {
  113. const event = TestStubs.EventStacktraceException({
  114. platform: 'native',
  115. });
  116. render(
  117. <GroupActions
  118. group={group}
  119. project={project}
  120. organization={organization}
  121. event={event}
  122. disabled={false}
  123. />
  124. );
  125. await userEvent.click(screen.getByLabelText('More Actions'));
  126. const reprocessActionButton = await screen.findByTestId('reprocess');
  127. expect(reprocessActionButton).toBeInTheDocument();
  128. });
  129. it('open dialog by clicking on the ReprocessAction component', async function () {
  130. const event = TestStubs.EventStacktraceException({
  131. platform: 'native',
  132. });
  133. render(
  134. <GroupActions
  135. group={group}
  136. project={project}
  137. organization={organization}
  138. event={event}
  139. disabled={false}
  140. />
  141. );
  142. const onReprocessEventFunc = jest.spyOn(ModalStore, 'openModal');
  143. await userEvent.click(screen.getByLabelText('More Actions'));
  144. const reprocessActionButton = await screen.findByTestId('reprocess');
  145. expect(reprocessActionButton).toBeInTheDocument();
  146. await userEvent.click(reprocessActionButton);
  147. await waitFor(() => expect(onReprocessEventFunc).toHaveBeenCalled());
  148. });
  149. });
  150. it('opens share modal from more actions dropdown', async () => {
  151. const org = {
  152. ...organization,
  153. features: ['shared-issues'],
  154. };
  155. const updateMock = MockApiClient.addMockResponse({
  156. url: `/projects/${org.slug}/${project.slug}/issues/`,
  157. method: 'PUT',
  158. body: {},
  159. });
  160. render(
  161. <Fragment>
  162. <GlobalModal />
  163. <GroupActions
  164. group={group}
  165. project={project}
  166. organization={org}
  167. disabled={false}
  168. />
  169. </Fragment>,
  170. {organization: org}
  171. );
  172. await userEvent.click(screen.getByLabelText('More Actions'));
  173. await userEvent.click(await screen.findByText('Share'));
  174. const modal = screen.getByRole('dialog');
  175. expect(within(modal).getByText('Share Issue')).toBeInTheDocument();
  176. expect(updateMock).toHaveBeenCalled();
  177. });
  178. it('opens delete confirm modal from more actions dropdown', async () => {
  179. const org = {
  180. ...organization,
  181. access: [...organization.access, 'event:admin'],
  182. };
  183. MockApiClient.addMockResponse({
  184. url: `/projects/${org.slug}/${project.slug}/issues/`,
  185. method: 'PUT',
  186. body: {},
  187. });
  188. const deleteMock = MockApiClient.addMockResponse({
  189. url: `/projects/${org.slug}/${project.slug}/issues/`,
  190. method: 'DELETE',
  191. body: {},
  192. });
  193. render(
  194. <Fragment>
  195. <GlobalModal />
  196. <GroupActions
  197. group={group}
  198. project={project}
  199. organization={org}
  200. disabled={false}
  201. />
  202. </Fragment>,
  203. {organization: org}
  204. );
  205. await userEvent.click(screen.getByLabelText('More Actions'));
  206. await userEvent.click(await screen.findByRole('menuitemradio', {name: 'Delete'}));
  207. const modal = screen.getByRole('dialog');
  208. expect(
  209. within(modal).getByText(/Deleting this issue is permanent/)
  210. ).toBeInTheDocument();
  211. await userEvent.click(within(modal).getByRole('button', {name: 'Delete'}));
  212. expect(deleteMock).toHaveBeenCalled();
  213. expect(browserHistory.push).toHaveBeenCalledWith({
  214. pathname: `/organizations/${organization.slug}/issues/`,
  215. query: {project: project.id},
  216. });
  217. });
  218. it('resolves and unresolves issue', async () => {
  219. const issuesApi = MockApiClient.addMockResponse({
  220. url: `/projects/${organization.slug}/project/issues/`,
  221. method: 'PUT',
  222. body: {...group, status: 'resolved'},
  223. });
  224. const {rerender} = render(
  225. <GroupActions
  226. group={group}
  227. project={project}
  228. organization={organization}
  229. disabled={false}
  230. />,
  231. {organization}
  232. );
  233. await userEvent.click(screen.getByRole('button', {name: 'Resolve'}));
  234. expect(issuesApi).toHaveBeenCalledWith(
  235. `/projects/${organization.slug}/project/issues/`,
  236. expect.objectContaining({
  237. data: {status: 'resolved', statusDetails: {}, substatus: null},
  238. })
  239. );
  240. expect(analyticsSpy).toHaveBeenCalledWith(
  241. 'issue_details.action_clicked',
  242. expect.objectContaining({
  243. action_type: 'resolved',
  244. })
  245. );
  246. rerender(
  247. <GroupActions
  248. group={{...group, status: 'resolved'}}
  249. project={project}
  250. organization={organization}
  251. disabled={false}
  252. />
  253. );
  254. await userEvent.click(screen.getByRole('button', {name: 'Resolved'}));
  255. expect(issuesApi).toHaveBeenCalledWith(
  256. `/projects/${organization.slug}/project/issues/`,
  257. expect.objectContaining({
  258. data: {status: 'unresolved', statusDetails: {}, substatus: 'ongoing'},
  259. })
  260. );
  261. });
  262. it('can archive issue', async () => {
  263. const org = {...organization, features: ['escalating-issues']};
  264. const issuesApi = MockApiClient.addMockResponse({
  265. url: `/projects/${organization.slug}/project/issues/`,
  266. method: 'PUT',
  267. body: {...group, status: 'resolved'},
  268. });
  269. render(
  270. <GroupActions
  271. group={group}
  272. project={project}
  273. organization={org}
  274. disabled={false}
  275. />,
  276. {organization: org}
  277. );
  278. await userEvent.click(await screen.findByRole('button', {name: 'Archive'}));
  279. expect(issuesApi).toHaveBeenCalledWith(
  280. expect.anything(),
  281. expect.objectContaining({
  282. data: {
  283. status: 'ignored',
  284. statusDetails: {},
  285. substatus: 'archived_until_escalating',
  286. },
  287. })
  288. );
  289. expect(analyticsSpy).toHaveBeenCalledWith(
  290. 'issue_details.action_clicked',
  291. expect.objectContaining({
  292. action_substatus: 'archived_until_escalating',
  293. action_type: 'ignored',
  294. })
  295. );
  296. });
  297. });