index.spec.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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.ProjectDetails({
  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. const wrapper = render(
  46. <GroupActions
  47. group={group}
  48. project={project}
  49. organization={organization}
  50. disabled={false}
  51. />
  52. );
  53. expect(wrapper.container).toSnapshot();
  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 = {
  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({data: {status: 'resolved', statusDetails: {}}})
  238. );
  239. expect(analyticsSpy).toHaveBeenCalledWith(
  240. 'issue_details.action_clicked',
  241. expect.objectContaining({
  242. action_type: 'resolved',
  243. })
  244. );
  245. rerender(
  246. <GroupActions
  247. group={{...group, status: 'resolved'}}
  248. project={project}
  249. organization={organization}
  250. disabled={false}
  251. />
  252. );
  253. await userEvent.click(screen.getByRole('button', {name: 'Resolved'}));
  254. expect(issuesApi).toHaveBeenCalledWith(
  255. `/projects/${organization.slug}/project/issues/`,
  256. expect.objectContaining({data: {status: 'unresolved', statusDetails: {}}})
  257. );
  258. });
  259. it('can archive issue', async () => {
  260. const org = {...organization, features: ['escalating-issues']};
  261. const issuesApi = MockApiClient.addMockResponse({
  262. url: `/projects/${organization.slug}/project/issues/`,
  263. method: 'PUT',
  264. body: {...group, status: 'resolved'},
  265. });
  266. render(
  267. <GroupActions
  268. group={group}
  269. project={project}
  270. organization={org}
  271. disabled={false}
  272. />,
  273. {organization: org}
  274. );
  275. await userEvent.click(await screen.findByRole('button', {name: 'Archive'}));
  276. expect(issuesApi).toHaveBeenCalledWith(
  277. expect.anything(),
  278. expect.objectContaining({
  279. data: {
  280. status: 'ignored',
  281. statusDetails: {},
  282. substatus: 'archived_until_escalating',
  283. },
  284. })
  285. );
  286. expect(analyticsSpy).toHaveBeenCalledWith(
  287. 'issue_details.action_clicked',
  288. expect.objectContaining({
  289. action_substatus: 'archived_until_escalating',
  290. action_type: 'ignored',
  291. })
  292. );
  293. });
  294. });