externalIssueForm.spec.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import styled from '@emotion/styled';
  2. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  3. import {makeCloseButton} from 'sentry/components/globalModal/components';
  4. import ExternalIssueForm from 'sentry/components/group/externalIssueForm';
  5. jest.mock('lodash/debounce', () => {
  6. const debounceMap = new Map();
  7. const mockDebounce =
  8. (fn, timeout) =>
  9. (...args) => {
  10. if (debounceMap.has(fn)) {
  11. clearTimeout(debounceMap.get(fn));
  12. }
  13. debounceMap.set(
  14. fn,
  15. setTimeout(() => {
  16. fn.apply(fn, args);
  17. debounceMap.delete(fn);
  18. }, timeout)
  19. );
  20. };
  21. return mockDebounce;
  22. });
  23. describe('ExternalIssueForm', () => {
  24. let group, integration, formConfig;
  25. const onChange = jest.fn();
  26. beforeEach(() => {
  27. MockApiClient.clearMockResponses();
  28. group = TestStubs.Group();
  29. integration = TestStubs.GitHubIntegration({externalIssues: []});
  30. });
  31. afterEach(() => {
  32. jest.useRealTimers();
  33. jest.clearAllMocks();
  34. });
  35. const renderComponent = async (action = 'Create') => {
  36. MockApiClient.addMockResponse({
  37. url: `/organizations/org-slug/issues/${group.id}/integrations/${integration.id}/`,
  38. body: formConfig,
  39. match: [MockApiClient.matchQuery({action: 'create'})],
  40. });
  41. const styledWrapper = styled(c => c.children);
  42. const wrapper = render(
  43. <ExternalIssueForm
  44. Body={styledWrapper()}
  45. Footer={styledWrapper()}
  46. organization={TestStubs.Organization()}
  47. Header={c => <span>{c.children}</span>}
  48. group={group}
  49. integration={integration}
  50. onChange={onChange}
  51. CloseButton={makeCloseButton(() => {})}
  52. closeModal={() => {}}
  53. />
  54. );
  55. await userEvent.click(screen.getByText(action));
  56. return wrapper;
  57. };
  58. describe('create', () => {
  59. // TODO: expand tests
  60. beforeEach(() => {
  61. formConfig = {
  62. createIssueConfig: [],
  63. };
  64. MockApiClient.addMockResponse({
  65. url: `/organizations/org-slug/issues/${group.id}/integrations/${integration.id}/`,
  66. body: formConfig,
  67. });
  68. });
  69. it('renders', async () => {
  70. await renderComponent();
  71. });
  72. });
  73. describe('link', () => {
  74. let externalIssueField, getFormConfigRequest;
  75. beforeEach(() => {
  76. externalIssueField = {
  77. name: 'externalIssue',
  78. default: '',
  79. required: true,
  80. choices: [],
  81. url: '/search',
  82. label: 'Issue',
  83. type: 'select',
  84. };
  85. formConfig = {
  86. status: 'active',
  87. name: 'scefali',
  88. domainName: 'github.com/scefali',
  89. linkIssueConfig: [
  90. {
  91. url: '/search',
  92. required: true,
  93. name: 'repo',
  94. default: 'scefali/test',
  95. updatesForm: true,
  96. choices: [
  97. ['scefali/test', 'test'],
  98. ['scefali/ZeldaBazaar', 'ZeldaBazaar'],
  99. ],
  100. type: 'select',
  101. label: 'GitHub Repository',
  102. },
  103. externalIssueField,
  104. {
  105. help: "Leave blank if you don't want to add a comment to the GitHub issue.",
  106. default: 'Default Value',
  107. required: false,
  108. label: 'Comment',
  109. type: 'textarea',
  110. name: 'comment',
  111. },
  112. ],
  113. accountType: 'User',
  114. provider: {
  115. canAdd: true,
  116. aspects: {
  117. disable_dialog: {
  118. 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.',
  119. actionText: 'Visit GitHub',
  120. },
  121. removal_dialog: {
  122. 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?',
  123. actionText: 'Delete',
  124. },
  125. },
  126. features: ['commits', 'issue-basic'],
  127. canDisable: true,
  128. key: 'github',
  129. name: 'GitHub',
  130. },
  131. id: '5',
  132. };
  133. getFormConfigRequest = MockApiClient.addMockResponse({
  134. url: `/organizations/org-slug/issues/${group.id}/integrations/${integration.id}/`,
  135. body: formConfig,
  136. match: [MockApiClient.matchQuery({action: 'link'})],
  137. });
  138. });
  139. it('renders and loads options', async () => {
  140. await renderComponent('Link');
  141. expect(getFormConfigRequest).toHaveBeenCalled();
  142. });
  143. describe('options loaded', () => {
  144. beforeEach(() => {
  145. MockApiClient.addMockResponse({
  146. url: `/organizations/org-slug/issues/${group.id}/integrations/${integration.id}/?action=link`,
  147. body: formConfig,
  148. });
  149. });
  150. it('fast typing is debounced and uses trailing call when fetching data', async () => {
  151. const searchResponse = MockApiClient.addMockResponse({
  152. url: '/search?field=externalIssue&query=faster&repo=scefali%2Ftest',
  153. body: [
  154. {
  155. label: '#1337 ref(js): Convert Form to a FC',
  156. value: 1337,
  157. },
  158. {
  159. label: '#2345 perf: Make it faster',
  160. value: 2345,
  161. },
  162. ],
  163. });
  164. await renderComponent('Link');
  165. jest.useFakeTimers();
  166. const textbox = screen.getByRole('textbox', {name: 'Issue'});
  167. await userEvent.click(textbox, {delay: null});
  168. await userEvent.type(textbox, 'faster', {delay: null});
  169. expect(searchResponse).not.toHaveBeenCalled();
  170. jest.advanceTimersByTime(300);
  171. expect(searchResponse).toHaveBeenCalledTimes(1);
  172. expect(await screen.findByText('#2345 perf: Make it faster')).toBeInTheDocument();
  173. });
  174. });
  175. });
  176. });