externalIssueList.spec.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import {EventFixture} from 'sentry-fixture/event';
  2. import {GitHubIntegrationFixture} from 'sentry-fixture/githubIntegration';
  3. import {GroupFixture} from 'sentry-fixture/group';
  4. import {JiraIntegrationFixture} from 'sentry-fixture/jiraIntegration';
  5. import {OrganizationFixture} from 'sentry-fixture/organization';
  6. import {PlatformExternalIssueFixture} from 'sentry-fixture/platformExternalIssue';
  7. import {ProjectFixture} from 'sentry-fixture/project';
  8. import {SentryAppComponentFixture} from 'sentry-fixture/sentryAppComponent';
  9. import {SentryAppInstallationFixture} from 'sentry-fixture/sentryAppInstallation';
  10. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  11. import SentryAppComponentsStore from 'sentry/stores/sentryAppComponentsStore';
  12. import SentryAppInstallationStore from 'sentry/stores/sentryAppInstallationsStore';
  13. import {ExternalIssueList} from './externalIssueList';
  14. describe('ExternalIssueList', () => {
  15. const organization = OrganizationFixture();
  16. const event = EventFixture();
  17. const group = GroupFixture();
  18. const project = ProjectFixture();
  19. beforeEach(() => {
  20. MockApiClient.clearMockResponses();
  21. SentryAppComponentsStore.init();
  22. });
  23. it('should allow unlinking integration external issues', async () => {
  24. MockApiClient.addMockResponse({
  25. url: `/organizations/${organization.slug}/issues/1/external-issues/`,
  26. body: [],
  27. });
  28. const issueKey = 'Test-Sentry/github-test#13';
  29. MockApiClient.addMockResponse({
  30. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`,
  31. body: [
  32. GitHubIntegrationFixture({
  33. status: 'active',
  34. externalIssues: [
  35. {
  36. id: '321',
  37. key: issueKey,
  38. url: 'https://github.com/Test-Sentry/github-test/issues/13',
  39. title: 'SyntaxError: XYZ',
  40. description: 'something else, sorry',
  41. displayName: '',
  42. },
  43. ],
  44. }),
  45. ],
  46. });
  47. const unlinkMock = MockApiClient.addMockResponse({
  48. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/1/`,
  49. query: {externalIssue: '321'},
  50. method: 'DELETE',
  51. });
  52. render(<ExternalIssueList event={event} group={group} project={project} />);
  53. expect(await screen.findByRole('button', {name: issueKey})).toBeInTheDocument();
  54. await userEvent.hover(screen.getByRole('button', {name: issueKey}));
  55. // Integrations are refetched, remove the external issue from the object
  56. const refetchMock = MockApiClient.addMockResponse({
  57. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`,
  58. body: [
  59. GitHubIntegrationFixture({
  60. status: 'active',
  61. externalIssues: [],
  62. }),
  63. ],
  64. });
  65. await userEvent.click(await screen.findByRole('button', {name: 'Unlink issue'}));
  66. await waitFor(() => {
  67. expect(screen.queryByRole('button', {name: issueKey})).not.toBeInTheDocument();
  68. });
  69. expect(unlinkMock).toHaveBeenCalledTimes(1);
  70. expect(refetchMock).toHaveBeenCalledTimes(1);
  71. });
  72. it('should allow unlinking sentry app issues', async () => {
  73. MockApiClient.addMockResponse({
  74. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`,
  75. body: [],
  76. });
  77. MockApiClient.addMockResponse({
  78. url: `/organizations/${organization.slug}/issues/1/external-issues/`,
  79. body: [
  80. PlatformExternalIssueFixture({
  81. id: '1',
  82. issueId: '1',
  83. serviceType: 'clickup',
  84. displayName: 'ClickUp: hello#1',
  85. webUrl: 'https://app.clickup.com/t/1',
  86. }),
  87. ],
  88. });
  89. const unlinkMock = MockApiClient.addMockResponse({
  90. url: `/issues/1/external-issues/1/`,
  91. method: 'DELETE',
  92. });
  93. const component = SentryAppComponentFixture({
  94. sentryApp: {
  95. ...SentryAppComponentFixture().sentryApp,
  96. slug: 'clickup',
  97. name: 'Clickup',
  98. },
  99. });
  100. SentryAppComponentsStore.loadComponents([component]);
  101. SentryAppInstallationStore.load([
  102. SentryAppInstallationFixture({
  103. app: component.sentryApp,
  104. }),
  105. ]);
  106. render(<ExternalIssueList event={event} group={group} project={project} />);
  107. expect(
  108. await screen.findByRole('button', {name: 'ClickUp: hello#1'})
  109. ).toBeInTheDocument();
  110. await userEvent.hover(screen.getByRole('button', {name: 'ClickUp: hello#1'}));
  111. await userEvent.click(await screen.findByRole('button', {name: 'Unlink issue'}));
  112. await waitFor(() => {
  113. expect(
  114. screen.queryByRole('button', {name: 'ClickUp: hello#1'})
  115. ).not.toBeInTheDocument();
  116. });
  117. expect(unlinkMock).toHaveBeenCalledTimes(1);
  118. });
  119. it('should combine multiple integration configurations into a single dropdown', async () => {
  120. MockApiClient.addMockResponse({
  121. url: `/organizations/${organization.slug}/issues/${group.id}/external-issues/`,
  122. body: [],
  123. });
  124. MockApiClient.addMockResponse({
  125. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`,
  126. body: [
  127. GitHubIntegrationFixture({
  128. status: 'active',
  129. externalIssues: [],
  130. name: 'GitHub sentry',
  131. }),
  132. GitHubIntegrationFixture({
  133. id: '2',
  134. status: 'active',
  135. externalIssues: [],
  136. name: 'GitHub codecov',
  137. }),
  138. ],
  139. });
  140. render(<ExternalIssueList event={event} group={group} project={project} />);
  141. expect(await screen.findByRole('button', {name: 'GitHub'})).toBeInTheDocument();
  142. await userEvent.click(await screen.findByRole('button', {name: 'GitHub'}));
  143. // Both items are listed inside the dropdown
  144. expect(
  145. await screen.findByRole('menuitemradio', {name: /GitHub sentry/})
  146. ).toBeInTheDocument();
  147. expect(
  148. await screen.findByRole('menuitemradio', {name: /GitHub codecov/})
  149. ).toBeInTheDocument();
  150. });
  151. it('should render empty state when no integrations', async () => {
  152. MockApiClient.addMockResponse({
  153. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`,
  154. body: [],
  155. });
  156. MockApiClient.addMockResponse({
  157. url: `/organizations/${organization.slug}/issues/${group.id}/external-issues/`,
  158. body: [],
  159. });
  160. render(<ExternalIssueList event={event} group={group} project={project} />);
  161. expect(
  162. await screen.findByText('Track this issue in Jira, GitHub, etc.')
  163. ).toBeInTheDocument();
  164. });
  165. it('should render dropdown items with subtext correctly', async () => {
  166. MockApiClient.addMockResponse({
  167. url: `/organizations/${organization.slug}/issues/${group.id}/external-issues/`,
  168. body: [],
  169. });
  170. MockApiClient.addMockResponse({
  171. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`,
  172. body: [
  173. JiraIntegrationFixture({
  174. id: '1',
  175. status: 'active',
  176. externalIssues: [],
  177. name: 'Jira Integration 1',
  178. domainName: 'hello.com',
  179. }),
  180. JiraIntegrationFixture({
  181. id: '2',
  182. status: 'active',
  183. externalIssues: [],
  184. name: 'Jira',
  185. domainName: 'example.com',
  186. }),
  187. ],
  188. });
  189. render(<ExternalIssueList event={event} group={group} project={project} />);
  190. expect(await screen.findByRole('button', {name: 'Jira'})).toBeInTheDocument();
  191. await userEvent.click(await screen.findByRole('button', {name: 'Jira'}));
  192. // Item with different name and subtext should show both
  193. const menuItem = await screen.findByRole('menuitemradio', {
  194. name: /Jira Integration 1/,
  195. });
  196. expect(menuItem).toHaveTextContent('hello.com');
  197. // Item with name matching integration name should only show subtext
  198. expect(screen.getByRole('menuitemradio', {name: 'example.com'})).toBeInTheDocument();
  199. });
  200. });