sentryAppExternalIssueForm.spec.jsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import {Group} from 'fixtures/js-stubs/group';
  2. import {SentryApp} from 'fixtures/js-stubs/sentryApp';
  3. import {SentryAppComponent} from 'fixtures/js-stubs/sentryAppComponent';
  4. import {mountWithTheme} from 'sentry-test/enzyme';
  5. import {changeInputValue, selectByValue} from 'sentry-test/select-new';
  6. import {Client} from 'sentry/api';
  7. import SentryAppExternalIssueForm from 'sentry/components/group/sentryAppExternalIssueForm';
  8. import {addQueryParamsToExistingUrl} from 'sentry/utils/queryString';
  9. const optionLabelSelector = label => `[label="${label}"]`;
  10. describe('SentryAppExternalIssueForm', () => {
  11. let wrapper;
  12. let group;
  13. let sentryApp;
  14. let sentryAppInstallation;
  15. let component;
  16. let submitUrl;
  17. let externalIssueRequest;
  18. beforeEach(() => {
  19. group = Group({
  20. title: 'ApiError: Broken',
  21. shortId: 'SEN123',
  22. permalink: 'https://sentry.io/organizations/sentry/issues/123/?project=1',
  23. });
  24. component = SentryAppComponent();
  25. sentryApp = SentryApp();
  26. sentryAppInstallation = SentryAppInstallation({sentryApp});
  27. submitUrl = `/sentry-app-installations/${sentryAppInstallation.uuid}/external-issue-actions/`;
  28. externalIssueRequest = Client.addMockResponse({
  29. url: submitUrl,
  30. method: 'POST',
  31. body: {},
  32. });
  33. });
  34. describe('create', () => {
  35. beforeEach(() => {
  36. wrapper = mountWithTheme(
  37. <SentryAppExternalIssueForm
  38. group={group}
  39. sentryAppInstallation={sentryAppInstallation}
  40. appName={sentryApp.name}
  41. config={component.schema.create}
  42. action="create"
  43. api={new Client()}
  44. />
  45. );
  46. });
  47. it('renders each required_fields field', () => {
  48. component.schema.create.required_fields.forEach(field => {
  49. expect(wrapper.exists(`#${field.name}`)).toBe(true);
  50. });
  51. });
  52. it('does not submit form if required fields are not set', () => {
  53. wrapper.find('form').simulate('submit');
  54. expect(externalIssueRequest).not.toHaveBeenCalled();
  55. });
  56. it('submits to the New External Issue endpoint', () => {
  57. selectByValue(wrapper, 'number_1', {name: 'numbers', control: true});
  58. wrapper.find('form').simulate('submit');
  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. },
  70. method: 'POST',
  71. })
  72. );
  73. });
  74. it('renders prepopulated defaults', () => {
  75. const titleField = wrapper.find('Input#title');
  76. const descriptionField = wrapper.find('TextArea#description');
  77. const url = addQueryParamsToExistingUrl(group.permalink, {
  78. referrer: sentryApp.name,
  79. });
  80. expect(titleField.prop('value')).toEqual(`${group.title}`);
  81. expect(descriptionField.prop('value')).toEqual(
  82. `Sentry Issue: [${group.shortId}](${url})`
  83. );
  84. });
  85. });
  86. describe('link', () => {
  87. beforeEach(() => {
  88. wrapper = mountWithTheme(
  89. <SentryAppExternalIssueForm
  90. group={group}
  91. sentryAppInstallation={sentryAppInstallation}
  92. appName={sentryApp.name}
  93. config={component.schema.link}
  94. action="link"
  95. api={new Client()}
  96. />
  97. );
  98. });
  99. it('renders each required_fields field', () => {
  100. component.schema.link.required_fields.forEach(field => {
  101. expect(wrapper.exists(`#${field.name}`)).toBe(true);
  102. });
  103. });
  104. it('submits to the New External Issue endpoint', () => {
  105. wrapper
  106. .find('input[name="issue"]')
  107. .simulate('change', {target: {value: 'my issue'}});
  108. wrapper.find('form').simulate('submit');
  109. expect(externalIssueRequest).toHaveBeenCalledWith(
  110. submitUrl,
  111. expect.objectContaining({
  112. data: {
  113. action: 'link',
  114. groupId: '1',
  115. issue: 'my issue',
  116. },
  117. method: 'POST',
  118. })
  119. );
  120. });
  121. });
  122. });
  123. describe('SentryAppExternalIssueForm Async Field', () => {
  124. let wrapper;
  125. let group;
  126. let sentryApp;
  127. let sentryAppInstallation;
  128. const component = SentryAppComponentAsync();
  129. beforeEach(() => {
  130. group = Group({
  131. title: 'ApiError: Broken',
  132. shortId: 'SEN123',
  133. permalink: 'https://sentry.io/organizations/sentry/issues/123/?project=1',
  134. });
  135. sentryApp = SentryApp();
  136. sentryAppInstallation = SentryAppInstallation({sentryApp});
  137. });
  138. afterEach(() => {
  139. Client.clearMockResponses();
  140. });
  141. describe('renders', () => {
  142. it('renders each required_fields field', async function () {
  143. const mockGetOptions = Client.addMockResponse({
  144. method: 'GET',
  145. url: '/sentry-app-installations/d950595e-cba2-46f6-8a94-b79e42806f98/external-requests/',
  146. body: {
  147. choices: [
  148. [1, 'Issue 1'],
  149. [2, 'Issue 2'],
  150. ],
  151. },
  152. });
  153. wrapper = mountWithTheme(
  154. <SentryAppExternalIssueForm
  155. group={group}
  156. sentryAppInstallation={sentryAppInstallation}
  157. appName={sentryApp.name}
  158. config={component.schema.create}
  159. action="create"
  160. api={new Client()}
  161. />
  162. );
  163. const thisInput = wrapper.find('input').at(0);
  164. changeInputValue(thisInput, 'I');
  165. await tick();
  166. wrapper.update();
  167. expect(mockGetOptions).toHaveBeenCalled();
  168. expect(wrapper.find(optionLabelSelector('Issue 1')).exists()).toBe(true);
  169. expect(wrapper.find(optionLabelSelector('Issue 2')).exists()).toBe(true);
  170. });
  171. });
  172. });
  173. describe('SentryAppExternalIssueForm Dependent fields', () => {
  174. let wrapper;
  175. let group;
  176. let sentryApp;
  177. let sentryAppInstallation;
  178. const component = SentryAppComponentDependent();
  179. beforeEach(() => {
  180. group = Group({
  181. title: 'ApiError: Broken',
  182. shortId: 'SEN123',
  183. permalink: 'https://sentry.io/organizations/sentry/issues/123/?project=1',
  184. });
  185. sentryApp = SentryApp();
  186. sentryAppInstallation = SentryAppInstallation({sentryApp});
  187. wrapper = mountWithTheme(
  188. <SentryAppExternalIssueForm
  189. group={group}
  190. sentryAppInstallation={sentryAppInstallation}
  191. appName={sentryApp.name}
  192. config={component.schema.create}
  193. action="create"
  194. api={new Client()}
  195. />
  196. );
  197. });
  198. afterEach(() => {
  199. Client.clearMockResponses();
  200. });
  201. describe('create', () => {
  202. it('load options for field that has dependencies when the dependent option is selected', async () => {
  203. const url = `/sentry-app-installations/${sentryAppInstallation.uuid}/external-requests/`;
  204. Client.addMockResponse({
  205. method: 'GET',
  206. url,
  207. body: {
  208. choices: [
  209. ['A', 'project A'],
  210. ['B', 'project B'],
  211. ],
  212. },
  213. match: [MockApiClient.matchQuery({uri: '/integrations/sentry/projects'})],
  214. });
  215. const boardMock = Client.addMockResponse({
  216. method: 'GET',
  217. url,
  218. body: {
  219. choices: [
  220. ['R', 'board R'],
  221. ['S', 'board S'],
  222. ],
  223. },
  224. match: [
  225. MockApiClient.matchQuery({
  226. uri: '/integrations/sentry/boards',
  227. dependentData: JSON.stringify({project_id: 'A'}),
  228. }),
  229. ],
  230. });
  231. const projectInput = wrapper.find('[data-test-id="project_id"] input').at(0);
  232. changeInputValue(projectInput, 'p');
  233. await tick();
  234. wrapper.update();
  235. expect(wrapper.find(optionLabelSelector('project A')).exists()).toBe(true);
  236. expect(wrapper.find(optionLabelSelector('project B')).exists()).toBe(true);
  237. // project select should be disabled and we shouldn't fetch the options yet
  238. expect(wrapper.find('[data-test-id="board_id"] input').at(0).prop('disabled')).toBe(
  239. true
  240. );
  241. expect(boardMock).not.toHaveBeenCalled();
  242. // when we set the value for project we should get the values for the board
  243. selectByValue(wrapper, 'A', {name: 'project_id'});
  244. await tick();
  245. wrapper.update();
  246. expect(boardMock).toHaveBeenCalled();
  247. expect(wrapper.find('[data-test-id="board_id"] input').at(0).prop('disabled')).toBe(
  248. false
  249. );
  250. const boardInput = wrapper.find('[data-test-id="board_id"] input').at(0);
  251. changeInputValue(boardInput, 'b');
  252. await tick();
  253. wrapper.update();
  254. expect(wrapper.find(optionLabelSelector('board R')).exists()).toBe(true);
  255. expect(wrapper.find(optionLabelSelector('board S')).exists()).toBe(true);
  256. });
  257. });
  258. });