index.spec.tsx 9.3 KB

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