index.spec.tsx 9.0 KB

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