externalIssueForm.spec.jsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import ExternalIssueForm from 'app/components/group/externalIssueForm';
  3. jest.mock('lodash/debounce', () => {
  4. const debounceMap = new Map();
  5. const mockDebounce =
  6. (fn, timeout) =>
  7. (...args) => {
  8. if (debounceMap.has(fn)) {
  9. clearTimeout(debounceMap.get(fn));
  10. }
  11. debounceMap.set(
  12. fn,
  13. setTimeout(() => {
  14. fn.apply(fn, args);
  15. debounceMap.delete(fn);
  16. }, timeout)
  17. );
  18. };
  19. return mockDebounce;
  20. });
  21. describe('ExternalIssueForm', () => {
  22. let group, integration, onChange, wrapper, formConfig;
  23. beforeEach(() => {
  24. MockApiClient.clearMockResponses();
  25. group = TestStubs.Group();
  26. integration = TestStubs.GitHubIntegration({externalIssues: []});
  27. onChange = jest.fn();
  28. });
  29. afterEach(() => {
  30. jest.useRealTimers();
  31. });
  32. const generateWrapper = (action = 'create') => {
  33. MockApiClient.addMockResponse(
  34. {
  35. url: `/groups/${group.id}/integrations/${integration.id}/`,
  36. body: formConfig,
  37. },
  38. {
  39. predicate: (_, options) => options?.query?.action === 'create',
  40. }
  41. );
  42. const component = mountWithTheme(
  43. <ExternalIssueForm
  44. Body={p => p.children}
  45. Header={p => p.children}
  46. group={group}
  47. integration={integration}
  48. onChange={onChange}
  49. />,
  50. TestStubs.routerContext()
  51. );
  52. component.instance().handleClick(action);
  53. return component;
  54. };
  55. describe('create', () => {
  56. // TODO: expand tests
  57. beforeEach(() => {
  58. formConfig = {
  59. createIssueConfig: [],
  60. };
  61. MockApiClient.addMockResponse({
  62. url: `/groups/${group.id}/integrations/${integration.id}/`,
  63. body: formConfig,
  64. });
  65. });
  66. it('renders', () => {
  67. wrapper = generateWrapper();
  68. expect(wrapper).toSnapshot();
  69. });
  70. });
  71. describe('link', () => {
  72. let externalIssueField, getFormConfigRequest;
  73. beforeEach(() => {
  74. externalIssueField = {
  75. name: 'externalIssue',
  76. default: '',
  77. required: true,
  78. choices: [],
  79. url: '/search',
  80. label: 'Issue',
  81. type: 'select',
  82. };
  83. formConfig = {
  84. status: 'active',
  85. name: 'scefali',
  86. domainName: 'github.com/scefali',
  87. linkIssueConfig: [
  88. {
  89. url: '/search',
  90. required: true,
  91. name: 'repo',
  92. default: 'scefali/test',
  93. updatesForm: true,
  94. choices: [
  95. ['scefali/test', 'test'],
  96. ['scefali/ZeldaBazaar', 'ZeldaBazaar'],
  97. ],
  98. type: 'select',
  99. label: 'GitHub Repository',
  100. },
  101. externalIssueField,
  102. {
  103. help: "Leave blank if you don't want to add a comment to the GitHub issue.",
  104. default: 'Default Value',
  105. required: false,
  106. label: 'Comment',
  107. type: 'textarea',
  108. name: 'comment',
  109. },
  110. ],
  111. accountType: 'User',
  112. provider: {
  113. canAdd: true,
  114. aspects: {
  115. disable_dialog: {
  116. body: 'Before deleting this integration, you must uninstall this integration from GitHub. After uninstalling, your integration will be disabled at which point you can choose to delete this integration.',
  117. actionText: 'Visit GitHub',
  118. },
  119. removal_dialog: {
  120. body: 'Deleting this integration will delete all associated repositories and commit data. This action cannot be undone. Are you sure you want to delete your integration?',
  121. actionText: 'Delete',
  122. },
  123. },
  124. features: ['commits', 'issue-basic'],
  125. canDisable: true,
  126. key: 'github',
  127. name: 'GitHub',
  128. },
  129. id: '5',
  130. };
  131. getFormConfigRequest = MockApiClient.addMockResponse(
  132. {
  133. url: `/groups/${group.id}/integrations/${integration.id}/`,
  134. body: formConfig,
  135. },
  136. {
  137. predicate: (_, options) => options?.query?.action === 'link',
  138. }
  139. );
  140. });
  141. it('renders', () => {
  142. wrapper = generateWrapper('link');
  143. expect(wrapper).toSnapshot();
  144. });
  145. it('load options', async () => {
  146. wrapper = generateWrapper('link');
  147. await tick();
  148. wrapper.update();
  149. expect(getFormConfigRequest).toHaveBeenCalled();
  150. });
  151. describe('options loaded', () => {
  152. let mockSuccessResponse;
  153. beforeEach(async () => {
  154. mockSuccessResponse = [42, 56];
  155. const mockFetchPromise = () =>
  156. new Promise(resolve => {
  157. setTimeout(() => {
  158. resolve({
  159. json: () => Promise.resolve(mockSuccessResponse),
  160. ok: true,
  161. });
  162. }, 50);
  163. });
  164. window.fetch = jest.fn().mockImplementation(mockFetchPromise);
  165. MockApiClient.addMockResponse({
  166. url: `/groups/${group.id}/integrations/${integration.id}/?action=link`,
  167. body: formConfig,
  168. });
  169. wrapper = generateWrapper('link');
  170. await tick();
  171. wrapper.update();
  172. });
  173. afterEach(() => {
  174. window.fetch.mockClear();
  175. delete window.fetch;
  176. });
  177. it('fast typing is debounced and uses trailing call when fetching data', () => {
  178. jest.useFakeTimers();
  179. wrapper.instance().getOptions(externalIssueField, 'd');
  180. wrapper.instance().getOptions(externalIssueField, 'do');
  181. wrapper.instance().getOptions(externalIssueField, 'doo');
  182. wrapper.instance().getOptions(externalIssueField, 'doOT');
  183. expect(window.fetch).toHaveBeenCalledTimes(0);
  184. jest.advanceTimersByTime(300);
  185. expect(window.fetch).toHaveBeenCalledTimes(1);
  186. expect(window.fetch).toHaveBeenCalledWith(
  187. '/search?field=externalIssue&query=doOT&repo=scefali%2Ftest'
  188. );
  189. });
  190. it('debounced function returns a promise with the options returned by fetch', async () => {
  191. const output = await wrapper.instance().getOptions(externalIssueField, 'd');
  192. expect(output).toEqual(mockSuccessResponse);
  193. });
  194. });
  195. });
  196. });