index.spec.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import {Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {Group as GroupFixture} from 'sentry-fixture/group';
  4. import {Organization} from 'sentry-fixture/organization';
  5. import {Project as 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 GroupActions from 'sentry/views/issueDetails/actions';
  19. const project = ProjectFixture({
  20. id: '2448',
  21. name: 'project name',
  22. slug: 'project',
  23. });
  24. const group = GroupFixture({
  25. id: '1337',
  26. pluginActions: [],
  27. pluginIssues: [],
  28. issueCategory: IssueCategory.ERROR,
  29. project,
  30. });
  31. const organization = Organization({
  32. id: '4660',
  33. slug: 'org',
  34. features: ['reprocessing-v2'],
  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', function () {
  48. render(
  49. <GroupActions
  50. group={group}
  51. project={project}
  52. organization={organization}
  53. disabled={false}
  54. />
  55. );
  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 feature flag reprocessing-v2 and native exception event', async function () {
  116. const event = TestStubs.EventStacktraceException({
  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 = TestStubs.EventStacktraceException({
  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 = Organization({
  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/${organization.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 org = {...organization, features: ['escalating-issues']};
  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={org}
  277. disabled={false}
  278. />,
  279. {organization: org}
  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. });