sentryAppExternalIssueForm.spec.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import selectEvent from 'react-select-event';
  2. import {SentryApp} from 'sentry-fixture/sentryApp';
  3. import {SentryAppInstallation} from 'sentry-fixture/sentryAppInstallation';
  4. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  5. import SentryAppExternalIssueForm from 'sentry/components/group/sentryAppExternalIssueForm';
  6. import {addQueryParamsToExistingUrl} from 'sentry/utils/queryString';
  7. describe('SentryAppExternalIssueForm', () => {
  8. const group = TestStubs.Group({
  9. title: 'ApiError: Broken',
  10. shortId: 'SEN123',
  11. permalink: 'https://sentry.io/organizations/sentry/issues/123/?project=1',
  12. });
  13. const component = TestStubs.SentryAppComponent();
  14. const sentryApp = SentryApp();
  15. const sentryAppInstallation = SentryAppInstallation({});
  16. const submitUrl = `/sentry-app-installations/${sentryAppInstallation.uuid}/external-issue-actions/`;
  17. let externalIssueRequest;
  18. beforeEach(() => {
  19. externalIssueRequest = MockApiClient.addMockResponse({
  20. url: submitUrl,
  21. method: 'POST',
  22. body: {},
  23. });
  24. });
  25. afterEach(() => {
  26. MockApiClient.clearMockResponses();
  27. });
  28. describe('create', () => {
  29. it('can create a new issue', async () => {
  30. render(
  31. <SentryAppExternalIssueForm
  32. event={TestStubs.Event()}
  33. onSubmitSuccess={jest.fn()}
  34. group={group}
  35. sentryAppInstallation={sentryAppInstallation}
  36. appName={sentryApp.name}
  37. config={component.schema.create}
  38. action="create"
  39. />
  40. );
  41. // renders each required_fields field
  42. expect(component.schema.create.required_fields).toHaveLength(3);
  43. for (const field of component.schema.create.required_fields) {
  44. expect(screen.getByRole('textbox', {name: field.label})).toBeInTheDocument();
  45. }
  46. // Prevents submission if required fields are not set
  47. await userEvent.clear(screen.getByRole('textbox', {name: 'Title'}));
  48. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  49. expect(externalIssueRequest).not.toHaveBeenCalled();
  50. selectEvent.openMenu(screen.getByRole('textbox', {name: 'Numbers'}));
  51. await userEvent.type(screen.getByRole('textbox', {name: 'Numbers'}), '1');
  52. await userEvent.click(screen.getByText('one'));
  53. // Sets required fields
  54. await userEvent.type(
  55. screen.getByRole('textbox', {name: 'Title'}),
  56. 'ApiError: Broken'
  57. );
  58. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  59. expect(externalIssueRequest).toHaveBeenCalledWith(
  60. submitUrl,
  61. expect.objectContaining({
  62. data: {
  63. action: 'create',
  64. description:
  65. 'Sentry Issue: [SEN123](https://sentry.io/organizations/sentry/issues/123/?project=1&referrer=Sample%20App)',
  66. groupId: '1',
  67. numbers: 'number_1',
  68. title: 'ApiError: Broken',
  69. uri: '',
  70. },
  71. method: 'POST',
  72. })
  73. );
  74. });
  75. it('renders prepopulated defaults', () => {
  76. render(
  77. <SentryAppExternalIssueForm
  78. event={TestStubs.Event()}
  79. onSubmitSuccess={jest.fn()}
  80. group={group}
  81. sentryAppInstallation={sentryAppInstallation}
  82. appName={sentryApp.name}
  83. config={component.schema.create}
  84. action="create"
  85. />
  86. );
  87. expect(screen.getByRole('textbox', {name: 'Title'})).toHaveValue(`${group.title}`);
  88. const url = addQueryParamsToExistingUrl(group.permalink, {
  89. referrer: sentryApp.name,
  90. });
  91. expect(screen.getByRole('textbox', {name: 'Description'})).toHaveValue(
  92. `Sentry Issue: [${group.shortId}](${url})`
  93. );
  94. });
  95. });
  96. describe('link', () => {
  97. it('can link an issue', async () => {
  98. render(
  99. <SentryAppExternalIssueForm
  100. event={TestStubs.Event()}
  101. onSubmitSuccess={jest.fn()}
  102. group={group}
  103. sentryAppInstallation={sentryAppInstallation}
  104. appName={sentryApp.name}
  105. config={component.schema.link}
  106. action="link"
  107. />
  108. );
  109. // renders each required_fields field
  110. expect(component.schema.link.required_fields).toHaveLength(1);
  111. for (const field of component.schema.link.required_fields) {
  112. expect(screen.getByRole('textbox', {name: field.label})).toBeInTheDocument();
  113. }
  114. await userEvent.type(screen.getByRole('textbox', {name: 'Issue'}), 'my issue');
  115. await userEvent.click(screen.getByRole('button', {name: 'Save Changes'}));
  116. expect(externalIssueRequest).toHaveBeenCalledWith(
  117. submitUrl,
  118. expect.objectContaining({
  119. data: {
  120. action: 'link',
  121. groupId: '1',
  122. issue: 'my issue',
  123. uri: '',
  124. },
  125. method: 'POST',
  126. })
  127. );
  128. });
  129. });
  130. });
  131. describe('SentryAppExternalIssueForm Async Field', () => {
  132. const component = TestStubs.SentryAppComponentAsync();
  133. const group = TestStubs.Group({
  134. title: 'ApiError: Broken',
  135. shortId: 'SEN123',
  136. permalink: 'https://sentry.io/organizations/sentry/issues/123/?project=1',
  137. });
  138. const sentryApp = SentryApp();
  139. const sentryAppInstallation = SentryAppInstallation({});
  140. afterEach(() => {
  141. MockApiClient.clearMockResponses();
  142. });
  143. it('renders each required_fields field', async function () {
  144. const mockGetOptions = MockApiClient.addMockResponse({
  145. method: 'GET',
  146. url: '/sentry-app-installations/d950595e-cba2-46f6-8a94-b79e42806f98/external-requests/',
  147. body: {
  148. choices: [
  149. [1, 'Issue 1'],
  150. [2, 'Issue 2'],
  151. ],
  152. },
  153. });
  154. render(
  155. <SentryAppExternalIssueForm
  156. event={TestStubs.Event()}
  157. onSubmitSuccess={jest.fn()}
  158. group={group}
  159. sentryAppInstallation={sentryAppInstallation}
  160. appName={sentryApp.name}
  161. config={component.schema.create}
  162. action="create"
  163. />
  164. );
  165. selectEvent.openMenu(screen.getByText('Numbers'));
  166. await userEvent.type(screen.getByRole('textbox'), 'I');
  167. expect(mockGetOptions).toHaveBeenCalled();
  168. expect(await screen.findByText('Issue 1')).toBeInTheDocument();
  169. expect(await screen.findByText('Issue 2')).toBeInTheDocument();
  170. });
  171. });
  172. describe('SentryAppExternalIssueForm Dependent fields', () => {
  173. const group = TestStubs.Group({
  174. title: 'ApiError: Broken',
  175. shortId: 'SEN123',
  176. permalink: 'https://sentry.io/organizations/sentry/issues/123/?project=1',
  177. });
  178. const sentryApp = SentryApp();
  179. const sentryAppInstallation = SentryAppInstallation({});
  180. const component = TestStubs.SentryAppComponentDependent();
  181. afterEach(() => {
  182. MockApiClient.clearMockResponses();
  183. });
  184. it('load options for field that has dependencies when the dependent option is selected', async () => {
  185. const url = `/sentry-app-installations/${sentryAppInstallation.uuid}/external-requests/`;
  186. MockApiClient.addMockResponse({
  187. method: 'GET',
  188. url,
  189. body: {
  190. choices: [
  191. ['A', 'project A'],
  192. ['B', 'project B'],
  193. ],
  194. },
  195. match: [MockApiClient.matchQuery({uri: '/integrations/sentry/projects'})],
  196. });
  197. const boardMock = MockApiClient.addMockResponse({
  198. method: 'GET',
  199. url,
  200. body: {
  201. choices: [
  202. ['R', 'board R'],
  203. ['S', 'board S'],
  204. ],
  205. },
  206. match: [
  207. MockApiClient.matchQuery({
  208. uri: '/integrations/sentry/boards',
  209. dependentData: JSON.stringify({project_id: 'A'}),
  210. }),
  211. ],
  212. });
  213. render(
  214. <SentryAppExternalIssueForm
  215. event={TestStubs.Event()}
  216. onSubmitSuccess={jest.fn()}
  217. group={group}
  218. sentryAppInstallation={sentryAppInstallation}
  219. appName={sentryApp.name}
  220. config={component.schema.create}
  221. action="create"
  222. />
  223. );
  224. await userEvent.type(screen.getByRole('textbox', {name: 'Project'}), 'p');
  225. expect(await screen.findByText('project A')).toBeInTheDocument();
  226. expect(screen.getByText('project B')).toBeInTheDocument();
  227. // project select should be disabled and we shouldn't fetch the options yet
  228. expect(screen.getByRole('textbox', {name: 'Board'})).toBeDisabled();
  229. expect(boardMock).not.toHaveBeenCalled();
  230. // when we set the value for project we should get the values for the board
  231. await selectEvent.select(screen.getByRole('textbox', {name: 'Project'}), 'project A');
  232. expect(boardMock).toHaveBeenCalled();
  233. expect(screen.getByRole('textbox', {name: 'Board'})).toBeEnabled();
  234. await userEvent.type(screen.getByRole('textbox', {name: 'Board'}), 'b');
  235. expect(await screen.findByText('board R')).toBeInTheDocument();
  236. expect(screen.getByText('board S')).toBeInTheDocument();
  237. });
  238. });