index.spec.tsx 9.2 KB

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