ticketRuleModal.spec.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import styled from '@emotion/styled';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  4. import selectEvent from 'sentry-test/selectEvent';
  5. import {addSuccessMessage} from 'sentry/actionCreators/indicator';
  6. import {makeCloseButton} from 'sentry/components/globalModal/components';
  7. import type {IssueAlertRuleAction} from 'sentry/types/alerts';
  8. import type {IssueConfigField} from 'sentry/types/integrations';
  9. import TicketRuleModal from 'sentry/views/alerts/rules/issue/ticketRuleModal';
  10. jest.unmock('sentry/utils/recreateRoute');
  11. jest.mock('sentry/actionCreators/indicator');
  12. jest.mock('sentry/actionCreators/onboardingTasks');
  13. describe('ProjectAlerts -> TicketRuleModal', function () {
  14. const closeModal = jest.fn();
  15. const modalElements = {
  16. Header: p => p.children,
  17. Body: p => p.children,
  18. Footer: p => p.children,
  19. };
  20. afterEach(function () {
  21. closeModal.mockReset();
  22. MockApiClient.clearMockResponses();
  23. });
  24. const doSubmit = async () =>
  25. await userEvent.click(screen.getByRole('button', {name: 'Apply Changes'}));
  26. const submitSuccess = async () => {
  27. await doSubmit();
  28. expect(addSuccessMessage).toHaveBeenCalled();
  29. expect(closeModal).toHaveBeenCalled();
  30. };
  31. const addMockConfigsAPICall = (otherField = {}) => {
  32. return MockApiClient.addMockResponse({
  33. url: '/organizations/org-slug/integrations/1/?ignored=Sprint',
  34. method: 'GET',
  35. body: {
  36. createIssueConfig: [
  37. {
  38. name: 'project',
  39. label: 'Jira Project',
  40. choices: [['10000', 'TEST']],
  41. default: '10000',
  42. type: 'select',
  43. updatesForm: true,
  44. },
  45. {
  46. name: 'issuetype',
  47. label: 'Issue Type',
  48. default: '10001',
  49. type: 'select',
  50. choices: [
  51. ['10001', 'Improvement'],
  52. ['10002', 'Task'],
  53. ['10003', 'Sub-task'],
  54. ['10004', 'New Feature'],
  55. ['10005', 'Bug'],
  56. ['10000', 'Epic'],
  57. ],
  58. updatesForm: true,
  59. required: true,
  60. },
  61. otherField,
  62. ],
  63. },
  64. });
  65. };
  66. const renderComponent = (
  67. props: Partial<IssueAlertRuleAction> = {},
  68. otherField: IssueConfigField = {
  69. label: 'Reporter',
  70. required: true,
  71. choices: [['a', 'a']],
  72. type: 'select',
  73. name: 'reporter',
  74. }
  75. ) => {
  76. const {organization, router} = initializeOrg();
  77. addMockConfigsAPICall(otherField);
  78. const body = styled(c => c.children);
  79. return render(
  80. <TicketRuleModal
  81. {...modalElements}
  82. CloseButton={makeCloseButton(() => {})}
  83. closeModal={closeModal}
  84. Body={body()}
  85. Footer={body()}
  86. formFields={{}}
  87. link=""
  88. ticketType=""
  89. instance={{...(props.data || {}), integration: 1}}
  90. index={0}
  91. onSubmitAction={() => {}}
  92. organization={organization}
  93. />,
  94. {router}
  95. );
  96. };
  97. describe('Create Rule', function () {
  98. it('should render the Ticket Rule modal', function () {
  99. renderComponent();
  100. expect(screen.getByRole('button', {name: 'Apply Changes'})).toBeInTheDocument();
  101. expect(screen.getByRole('textbox', {name: 'Title'})).toBeInTheDocument();
  102. expect(screen.getByRole('textbox', {name: 'Description'})).toBeInTheDocument();
  103. });
  104. it('should save the modal data when "Apply Changes" is clicked with valid data', async function () {
  105. renderComponent();
  106. await selectEvent.select(screen.getByRole('textbox', {name: 'Reporter'}), 'a');
  107. await submitSuccess();
  108. });
  109. it('submit button shall be disabled if form is incomplete', async function () {
  110. // This doesn't test anything TicketRules specific but I'm leaving it here as an example.
  111. renderComponent();
  112. expect(screen.getByRole('button', {name: 'Apply Changes'})).toBeDisabled();
  113. await userEvent.hover(screen.getByRole('button', {name: 'Apply Changes'}));
  114. expect(
  115. await screen.findByText('Required fields must be filled out')
  116. ).toBeInTheDocument();
  117. });
  118. it('should reload fields when an "updatesForm" field changes', async function () {
  119. renderComponent();
  120. await selectEvent.select(screen.getByRole('textbox', {name: 'Reporter'}), 'a');
  121. addMockConfigsAPICall({
  122. label: 'Assignee',
  123. required: true,
  124. choices: [['b', 'b']],
  125. type: 'select',
  126. name: 'assignee',
  127. });
  128. await selectEvent.select(screen.getByRole('textbox', {name: 'Issue Type'}), 'Epic');
  129. await selectEvent.select(screen.getByRole('textbox', {name: 'Assignee'}), 'b');
  130. await submitSuccess();
  131. });
  132. it('should ignore error checking when default is empty array', async function () {
  133. renderComponent(undefined, {
  134. label: 'Labels',
  135. required: false,
  136. choices: [['bug', `bug`]],
  137. default: undefined,
  138. type: 'select',
  139. multiple: true,
  140. name: 'labels',
  141. });
  142. expect(
  143. screen.queryAllByText(`Could not fetch saved option for Labels. Please reselect.`)
  144. ).toHaveLength(0);
  145. await selectEvent.select(screen.getByRole('textbox', {name: 'Issue Type'}), 'Epic');
  146. await selectEvent.select(screen.getByRole('textbox', {name: 'Labels'}), 'bug');
  147. await submitSuccess();
  148. });
  149. it('should persist single select values when the modal is reopened', async function () {
  150. renderComponent({data: {reporter: 'a'}});
  151. await submitSuccess();
  152. });
  153. it('should persist multi select values when the modal is reopened', async function () {
  154. renderComponent(
  155. {data: {components: ['a', 'c']}},
  156. {
  157. name: 'components',
  158. label: 'Components',
  159. default: undefined,
  160. type: 'select',
  161. multiple: true,
  162. required: true,
  163. choices: [
  164. ['a', 'a'],
  165. ['b', 'b'],
  166. ['c', 'c'],
  167. ],
  168. }
  169. );
  170. await submitSuccess();
  171. });
  172. it('should not persist value when unavailable in new choices', async function () {
  173. renderComponent({data: {reporter: 'a'}});
  174. addMockConfigsAPICall({
  175. label: 'Reporter',
  176. required: true,
  177. choices: [['b', 'b']],
  178. type: 'select',
  179. name: 'reporter',
  180. ignorePriorChoices: true,
  181. });
  182. // Switch Issue Type so we refetch the config and update Reporter choices
  183. await selectEvent.select(screen.getByRole('textbox', {name: 'Issue Type'}), 'Epic');
  184. await expect(
  185. selectEvent.select(screen.getByRole('textbox', {name: 'Reporter'}), 'a')
  186. ).rejects.toThrow();
  187. await selectEvent.select(screen.getByRole('textbox', {name: 'Reporter'}), 'b');
  188. await submitSuccess();
  189. });
  190. it('should persist non-choice value when the modal is reopened', async function () {
  191. const textField: IssueConfigField = {
  192. label: 'Text Field',
  193. required: true,
  194. type: 'string',
  195. name: 'textField',
  196. };
  197. renderComponent({data: {textField: 'foo'}}, textField);
  198. expect(screen.getByRole('textbox', {name: 'Text Field'})).toHaveValue('foo');
  199. await submitSuccess();
  200. });
  201. it('should get async options from URL', async function () {
  202. renderComponent();
  203. addMockConfigsAPICall({
  204. label: 'Assignee',
  205. required: true,
  206. url: 'http://example.com',
  207. type: 'select',
  208. name: 'assignee',
  209. });
  210. await selectEvent.select(screen.getByRole('textbox', {name: 'Issue Type'}), 'Epic');
  211. // Component makes 1 request per character typed.
  212. let txt = '';
  213. for (const char of 'Joe') {
  214. txt += char;
  215. MockApiClient.addMockResponse({
  216. url: `http://example.com?field=assignee&issuetype=10001&project=10000&query=${txt}`,
  217. method: 'GET',
  218. body: [{label: 'Joe', value: 'Joe'}],
  219. });
  220. }
  221. const menu = screen.getByRole('textbox', {name: 'Assignee'});
  222. await selectEvent.openMenu(menu);
  223. await userEvent.type(menu, 'Joe{Escape}');
  224. await selectEvent.select(menu, 'Joe');
  225. await submitSuccess();
  226. });
  227. });
  228. });