index.spec.tsx 9.1 KB

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