foreman_remote_execution 4.6.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/app/controllers/api/v2/job_invocations_controller.rb +16 -1
  5. data/app/controllers/job_invocations_controller.rb +1 -1
  6. data/app/controllers/ui_job_wizard_controller.rb +21 -2
  7. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  8. data/app/graphql/types/job_invocation.rb +16 -0
  9. data/app/graphql/types/job_invocation_input.rb +13 -0
  10. data/app/graphql/types/recurrence_input.rb +8 -0
  11. data/app/graphql/types/scheduling_input.rb +6 -0
  12. data/app/graphql/types/targeting_enum.rb +7 -0
  13. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  14. data/app/helpers/remote_execution_helper.rb +9 -3
  15. data/app/lib/actions/remote_execution/run_host_job.rb +10 -1
  16. data/app/lib/actions/remote_execution/run_hosts_job.rb +58 -4
  17. data/app/mailers/rex_job_mailer.rb +15 -0
  18. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +10 -0
  19. data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +6 -0
  20. data/app/models/host_proxy_invocation.rb +4 -0
  21. data/app/models/host_status/execution_status.rb +3 -3
  22. data/app/models/job_invocation.rb +12 -5
  23. data/app/models/job_invocation_composer.rb +25 -17
  24. data/app/models/job_template.rb +1 -1
  25. data/app/models/remote_execution_feature.rb +5 -1
  26. data/app/models/remote_execution_provider.rb +18 -2
  27. data/app/models/rex_mail_notification.rb +13 -0
  28. data/app/models/targeting.rb +7 -3
  29. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  30. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  31. data/app/views/job_invocations/index.html.erb +1 -1
  32. data/app/views/job_invocations/refresh.js.erb +1 -0
  33. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  34. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  35. data/app/views/template_invocations/show.html.erb +2 -1
  36. data/app/views/templates/ssh/module_action.erb +1 -0
  37. data/app/views/templates/ssh/power_action.erb +2 -0
  38. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/2021051713291621250977_add_host_proxy_invocations.rb +12 -0
  41. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  42. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  43. data/db/seeds.d/95-mail_notifications.rb +24 -0
  44. data/foreman_remote_execution.gemspec +2 -3
  45. data/lib/foreman_remote_execution/engine.rb +114 -8
  46. data/lib/foreman_remote_execution/version.rb +1 -1
  47. data/package.json +9 -7
  48. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  49. data/test/functional/cockpit_controller_test.rb +0 -1
  50. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  51. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  52. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  53. data/test/helpers/remote_execution_helper_test.rb +0 -1
  54. data/test/unit/actions/run_host_job_test.rb +21 -0
  55. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  56. data/test/unit/concerns/host_extensions_test.rb +40 -7
  57. data/test/unit/input_template_renderer_test.rb +1 -89
  58. data/test/unit/job_invocation_composer_test.rb +18 -18
  59. data/test/unit/job_invocation_report_template_test.rb +16 -13
  60. data/test/unit/job_invocation_test.rb +1 -1
  61. data/test/unit/job_template_effective_user_test.rb +0 -4
  62. data/test/unit/remote_execution_provider_test.rb +46 -4
  63. data/test/unit/targeting_test.rb +68 -1
  64. data/webpack/JobWizard/JobWizard.js +158 -24
  65. data/webpack/JobWizard/JobWizard.scss +93 -1
  66. data/webpack/JobWizard/JobWizardConstants.js +54 -0
  67. data/webpack/JobWizard/JobWizardSelectors.js +41 -0
  68. data/webpack/JobWizard/__tests__/fixtures.js +188 -3
  69. data/webpack/JobWizard/__tests__/integration.test.js +41 -106
  70. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  71. data/webpack/JobWizard/autofill.js +38 -0
  72. data/webpack/JobWizard/index.js +7 -0
  73. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +41 -10
  74. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +90 -0
  75. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +116 -55
  76. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +354 -16
  77. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +79 -246
  78. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +5 -2
  79. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +123 -51
  80. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  81. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  82. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  83. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  84. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  85. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +100 -0
  86. data/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js +23 -0
  87. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  88. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +53 -0
  89. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  90. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  91. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  92. data/webpack/JobWizard/steps/HostsAndInputs/index.js +214 -0
  93. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  94. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  95. data/webpack/JobWizard/steps/Schedule/QueryType.js +51 -0
  96. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  97. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  98. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  99. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  100. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +125 -0
  101. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  102. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +28 -0
  103. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +106 -0
  104. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  105. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +32 -0
  106. data/webpack/JobWizard/steps/Schedule/index.js +178 -0
  107. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  108. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -0
  109. data/webpack/JobWizard/steps/form/Formatter.js +181 -0
  110. data/webpack/JobWizard/steps/form/NumberInput.js +36 -0
  111. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  112. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  113. data/webpack/JobWizard/steps/form/SelectField.js +28 -5
  114. data/webpack/JobWizard/steps/form/WizardTitle.js +14 -0
  115. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  116. data/webpack/JobWizard/submit.js +120 -0
  117. data/webpack/JobWizard/validation.js +53 -0
  118. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  119. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  120. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  121. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  122. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  123. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  124. data/webpack/helpers.js +1 -0
  125. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  126. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  127. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  128. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  129. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  130. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  131. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  132. metadata +71 -16
  133. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +0 -70
  134. data/app/models/setting/remote_execution.rb +0 -88
  135. data/test/models/orchestration/ssh_test.rb +0 -56
  136. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  137. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  138. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  139. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  140. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  141. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  142. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  143. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -0,0 +1,106 @@
1
+ import React, { useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, Checkbox } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { DateTimePicker } from '../form/DateTimePicker';
6
+ import { helpLabel } from '../form/FormHelpers';
7
+
8
+ export const StartEndDates = ({
9
+ startsAt,
10
+ setStartsAt,
11
+ startsBefore,
12
+ setStartsBefore,
13
+ ends,
14
+ setEnds,
15
+ isNeverEnds,
16
+ setIsNeverEnds,
17
+ validEnd,
18
+ setValidEnd,
19
+ isFuture,
20
+ isStartBeforeDisabled,
21
+ isEndDisabled,
22
+ }) => {
23
+ const toggleIsNeverEnds = (checked, event) => {
24
+ const value = event?.target?.checked;
25
+ setIsNeverEnds(value);
26
+ };
27
+ useEffect(() => {
28
+ if (isNeverEnds) setValidEnd(true);
29
+ else if ((!startsAt.length && !startsBefore.length) || !ends)
30
+ setValidEnd(true);
31
+ else if (
32
+ startsAt.length
33
+ ? new Date(startsAt).getTime() <= new Date(ends).getTime()
34
+ : new Date(startsBefore).getTime() <= new Date(ends).getTime()
35
+ )
36
+ setValidEnd(true);
37
+ else setValidEnd(false);
38
+ }, [startsAt, startsBefore, ends, isNeverEnds, setValidEnd]);
39
+ return (
40
+ <>
41
+ <FormGroup label={__('Starts at')} fieldId="start-at-date">
42
+ <DateTimePicker
43
+ allowEmpty={!isFuture}
44
+ ariaLabel="starts at"
45
+ dateTime={startsAt}
46
+ setDateTime={setStartsAt}
47
+ />
48
+ </FormGroup>
49
+
50
+ <FormGroup
51
+ label={__('Starts before')}
52
+ fieldId="start-before-date"
53
+ labelIcon={helpLabel(
54
+ __(
55
+ 'Indicates that the action should be cancelled if it cannot be started before this time.'
56
+ ),
57
+ 'start-before-date'
58
+ )}
59
+ >
60
+ <DateTimePicker
61
+ isDisabled={isStartBeforeDisabled}
62
+ allowEmpty={!isFuture}
63
+ ariaLabel="starts before"
64
+ dateTime={startsBefore}
65
+ setDateTime={setStartsBefore}
66
+ />
67
+ </FormGroup>
68
+ <FormGroup
69
+ label={__('Ends')}
70
+ fieldId="end-date"
71
+ helperTextInvalid={__('End time needs to be after start time')}
72
+ validated={validEnd ? 'success' : 'error'}
73
+ >
74
+ <DateTimePicker
75
+ ariaLabel="ends"
76
+ dateTime={ends}
77
+ setDateTime={setEnds}
78
+ isDisabled={isNeverEnds || isEndDisabled}
79
+ />
80
+ <Checkbox
81
+ label={__('Never ends')}
82
+ isChecked={isNeverEnds}
83
+ onChange={toggleIsNeverEnds}
84
+ id="never-ends"
85
+ name="never-ends"
86
+ />
87
+ </FormGroup>
88
+ </>
89
+ );
90
+ };
91
+
92
+ StartEndDates.propTypes = {
93
+ startsAt: PropTypes.string.isRequired,
94
+ setStartsAt: PropTypes.func.isRequired,
95
+ startsBefore: PropTypes.string.isRequired,
96
+ setStartsBefore: PropTypes.func.isRequired,
97
+ ends: PropTypes.string.isRequired,
98
+ setEnds: PropTypes.func.isRequired,
99
+ isNeverEnds: PropTypes.bool.isRequired,
100
+ setIsNeverEnds: PropTypes.func.isRequired,
101
+ validEnd: PropTypes.bool.isRequired,
102
+ setValidEnd: PropTypes.func.isRequired,
103
+ isFuture: PropTypes.bool.isRequired,
104
+ isStartBeforeDisabled: PropTypes.bool.isRequired,
105
+ isEndDisabled: PropTypes.bool.isRequired,
106
+ };
@@ -0,0 +1,402 @@
1
+ /* eslint-disable max-lines */
2
+ import React from 'react';
3
+ import { Provider } from 'react-redux';
4
+ import configureMockStore from 'redux-mock-store';
5
+ import { fireEvent, screen, render, act } from '@testing-library/react';
6
+ import * as api from 'foremanReact/redux/API';
7
+ import { JobWizard } from '../../../JobWizard';
8
+ import * as selectors from '../../../JobWizardSelectors';
9
+ import { jobTemplate, jobTemplateResponse } from '../../../__tests__/fixtures';
10
+
11
+ const lodash = require('lodash');
12
+
13
+ lodash.debounce = fn => fn;
14
+ jest.spyOn(api, 'get');
15
+ jest.spyOn(selectors, 'selectJobTemplate');
16
+ jest.spyOn(selectors, 'selectJobTemplates');
17
+ jest.spyOn(selectors, 'selectJobCategories');
18
+
19
+ const jobCategories = ['Ansible Commands', 'Puppet', 'Services'];
20
+
21
+ selectors.selectJobCategories.mockImplementation(() => jobCategories);
22
+
23
+ selectors.selectJobTemplates.mockImplementation(() => [
24
+ jobTemplate,
25
+ { ...jobTemplate, id: 2, name: 'template2' },
26
+ ]);
27
+ selectors.selectJobTemplate.mockImplementation(() => jobTemplateResponse);
28
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
29
+ if (action.key === 'JOB_CATEGORIES') {
30
+ handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
31
+ } else if (action.key === 'JOB_TEMPLATE') {
32
+ handleSuccess &&
33
+ handleSuccess({
34
+ data: jobTemplateResponse,
35
+ });
36
+ } else if (action.key === 'JOB_TEMPLATES') {
37
+ handleSuccess &&
38
+ handleSuccess({
39
+ data: { results: [jobTemplateResponse.job_template] },
40
+ });
41
+ }
42
+ return { type: 'get', ...action };
43
+ });
44
+
45
+ const mockStore = configureMockStore([]);
46
+ const store = mockStore({});
47
+ jest.useFakeTimers();
48
+
49
+ describe('Schedule', () => {
50
+ it('should save date time between steps ', async () => {
51
+ render(
52
+ <Provider store={store}>
53
+ <JobWizard />
54
+ </Provider>
55
+ );
56
+ await act(async () => {
57
+ fireEvent.click(screen.getByText('Schedule'));
58
+ });
59
+ const newStartDate = '2020/03/12';
60
+ const newStartTime = '12:03';
61
+ const newEndsDate = '2030/03/12';
62
+ const newEndsTime = '17:34';
63
+ const startsDateField = screen.getByLabelText('starts at datepicker');
64
+ const endsDateField = screen.getByLabelText('ends datepicker');
65
+
66
+ const startsTimeField = screen.getByLabelText('starts at timepicker');
67
+ const endsTimeField = screen.getByLabelText('ends timepicker');
68
+
69
+ const staticQuery = screen.getByLabelText('Static query');
70
+ const dynamicQuery = screen.getByLabelText('Dynamic query');
71
+ const purpose = screen.getByLabelText('purpose');
72
+ const newPurposeLabel = 'some fun text';
73
+ expect(staticQuery.checked).toBeTruthy();
74
+ await act(async () => {
75
+ await fireEvent.change(startsDateField, {
76
+ target: { value: newStartDate },
77
+ });
78
+ await fireEvent.change(startsTimeField, {
79
+ target: { value: newStartTime },
80
+ });
81
+ await fireEvent.change(purpose, {
82
+ target: { value: newPurposeLabel },
83
+ });
84
+ await fireEvent.change(endsDateField, { target: { value: newEndsDate } });
85
+ await fireEvent.change(endsTimeField, { target: { value: newEndsTime } });
86
+
87
+ await fireEvent.click(dynamicQuery);
88
+ jest.runAllTimers(); // to handle pf4 date picker popover useTimer
89
+ });
90
+ await act(async () => {
91
+ fireEvent.click(screen.getByText('Category and Template'));
92
+ });
93
+ expect(screen.getAllByText('Category and Template')).toHaveLength(3);
94
+
95
+ await act(async () => {
96
+ fireEvent.click(screen.getByText('Schedule'));
97
+ jest.runAllTimers();
98
+ });
99
+ expect(startsDateField.value).toBe(newStartDate);
100
+ expect(startsTimeField.value).toBe(newStartTime);
101
+ expect(endsDateField.value).toBe(newEndsDate);
102
+ expect(endsTimeField.value).toBe(newEndsTime);
103
+ expect(dynamicQuery.checked).toBeTruthy();
104
+ expect(purpose.value).toBe(newPurposeLabel);
105
+ });
106
+ it('should remove start date time on execute now', async () => {
107
+ render(
108
+ <Provider store={store}>
109
+ <JobWizard />
110
+ </Provider>
111
+ );
112
+ await act(async () => {
113
+ fireEvent.click(screen.getByText('Schedule'));
114
+ });
115
+ const executeNow = screen.getByLabelText('Execute now');
116
+ const executeFuture = screen.getByLabelText(
117
+ 'Schedule for future execution'
118
+ );
119
+ expect(executeNow.checked).toBeTruthy();
120
+ const newStartDate = '2020/03/12';
121
+ const newStartTime = '12:03';
122
+ const startsDateField = screen.getByLabelText('starts at datepicker');
123
+ const startsTimeField = screen.getByLabelText('starts at timepicker');
124
+ await act(async () => {
125
+ await fireEvent.change(startsDateField, {
126
+ target: { value: newStartDate },
127
+ });
128
+ await fireEvent.change(startsTimeField, {
129
+ target: { value: newStartTime },
130
+ });
131
+ await jest.runOnlyPendingTimers();
132
+ });
133
+ expect(startsDateField.value).toBe(newStartDate);
134
+ expect(startsTimeField.value).toBe(newStartTime);
135
+ expect(executeFuture.checked).toBeTruthy();
136
+ await act(async () => {
137
+ await fireEvent.click(executeNow);
138
+ jest.runAllTimers();
139
+ });
140
+ expect(executeNow.checked).toBeTruthy();
141
+ expect(startsDateField.value).toBe('');
142
+ expect(startsTimeField.value).toBe('');
143
+ });
144
+
145
+ it('should disable end date on never ends', async () => {
146
+ render(
147
+ <Provider store={store}>
148
+ <JobWizard />
149
+ </Provider>
150
+ );
151
+ await act(async () => {
152
+ await fireEvent.click(screen.getByText('Schedule'));
153
+ jest.runAllTimers();
154
+ });
155
+ const neverEnds = screen.getByLabelText('Never ends');
156
+ expect(neverEnds.checked).toBeFalsy();
157
+
158
+ const endsDateField = screen.getByLabelText('ends datepicker');
159
+ const endsTimeField = screen.getByLabelText('ends timepicker');
160
+ fireEvent.click(
161
+ screen.getByLabelText('Does not repeat', { selector: 'button' })
162
+ );
163
+ await act(async () => {
164
+ fireEvent.click(screen.getByText('Cronline'));
165
+ });
166
+ expect(endsDateField.disabled).toBeFalsy();
167
+ expect(endsTimeField.disabled).toBeFalsy();
168
+ await act(async () => {
169
+ fireEvent.click(neverEnds);
170
+ });
171
+ expect(neverEnds.checked).toBeTruthy();
172
+ expect(endsDateField.disabled).toBeTruthy();
173
+ expect(endsTimeField.disabled).toBeTruthy();
174
+ });
175
+
176
+ it('should change between repeat on states', async () => {
177
+ render(
178
+ <Provider store={store}>
179
+ <JobWizard />
180
+ </Provider>
181
+ );
182
+ await act(async () => {
183
+ fireEvent.click(screen.getByText('Schedule'));
184
+ jest.runAllTimers(); // to handle pf4 date picker popover useTimer
185
+ });
186
+ expect(
187
+ screen.getByPlaceholderText('Repeat N times').hasAttribute('disabled')
188
+ ).toBeTruthy();
189
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
190
+ await act(async () => {
191
+ fireEvent.click(
192
+ screen.getByLabelText('Does not repeat', { selector: 'button' })
193
+ );
194
+ });
195
+
196
+ await act(async () => {
197
+ fireEvent.click(screen.getByText('Cronline'));
198
+ });
199
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
200
+ const newRepeatTimes = '3';
201
+ const repeatNTimes = screen.getByPlaceholderText('Repeat N times');
202
+ expect(repeatNTimes.value).toBe('');
203
+ await act(async () => {
204
+ fireEvent.change(repeatNTimes, {
205
+ target: { value: newRepeatTimes },
206
+ });
207
+ });
208
+ expect(repeatNTimes.value).toBe(newRepeatTimes);
209
+
210
+ const newCronline = '1 2';
211
+ const cronline = screen.getByLabelText('cronline');
212
+ expect(cronline.value).toBe('');
213
+ await act(async () => {
214
+ fireEvent.change(cronline, {
215
+ target: { value: newCronline },
216
+ });
217
+ });
218
+ expect(cronline.value).toBe(newCronline);
219
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
220
+
221
+ await act(async () => {
222
+ fireEvent.click(screen.getByText('Category and Template'));
223
+ });
224
+ expect(screen.getAllByText('Category and Template')).toHaveLength(3);
225
+
226
+ await act(async () => {
227
+ fireEvent.click(screen.getByText('Schedule'));
228
+ jest.runAllTimers();
229
+ });
230
+ expect(screen.queryAllByText('Schedule')).toHaveLength(3);
231
+ expect(repeatNTimes.value).toBe(newRepeatTimes);
232
+ expect(cronline.value).toBe(newCronline);
233
+
234
+ fireEvent.click(screen.getByText('Cronline'));
235
+ await act(async () => {
236
+ fireEvent.click(screen.getByText('Monthly'));
237
+ });
238
+
239
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
240
+ const newDays = '1,2,3';
241
+ const days = screen.getByLabelText('days');
242
+ expect(days.value).toBe('');
243
+ await act(async () => {
244
+ fireEvent.change(days, {
245
+ target: { value: newDays },
246
+ });
247
+ fireEvent.click(repeatNTimes);
248
+ });
249
+ expect(days.value).toBe(newDays);
250
+
251
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
252
+ const newAtMonthly = '13:07';
253
+ const at = () => screen.getByLabelText('repeat-at');
254
+ expect(at().value).toBe('');
255
+ await act(async () => {
256
+ fireEvent.change(at(), {
257
+ target: { value: newAtMonthly },
258
+ });
259
+ });
260
+ expect(at().value).toBe(newAtMonthly);
261
+
262
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
263
+ fireEvent.click(screen.getByText('Monthly'));
264
+ await act(async () => {
265
+ fireEvent.click(screen.getByText('Weekly'));
266
+ });
267
+
268
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
269
+ const dayTue = screen.getByLabelText('Tue checkbox');
270
+ const daySat = screen.getByLabelText('Sat checkbox');
271
+ expect(dayTue.checked).toBe(false);
272
+ expect(daySat.checked).toBe(false);
273
+ await act(async () => {
274
+ fireEvent.click(dayTue);
275
+ fireEvent.change(dayTue, {
276
+ target: { checked: true },
277
+ });
278
+ });
279
+ await act(async () => {
280
+ fireEvent.click(daySat);
281
+ fireEvent.change(daySat, {
282
+ target: { checked: true },
283
+ });
284
+ });
285
+ expect(dayTue.checked).toBe(true);
286
+ expect(daySat.checked).toBe(true);
287
+ const newAtWeekly = '17:53';
288
+ expect(at().value).toBe(newAtMonthly);
289
+ await act(async () => {
290
+ fireEvent.change(at(), {
291
+ target: { value: newAtWeekly },
292
+ });
293
+ });
294
+ expect(at().value).toBe(newAtWeekly);
295
+
296
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
297
+ fireEvent.click(screen.getByText('Weekly'));
298
+ await act(async () => {
299
+ fireEvent.click(screen.getByText('Daily'));
300
+ });
301
+
302
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
303
+ await act(async () => {
304
+ fireEvent.change(at(), {
305
+ target: { value: '' },
306
+ });
307
+ });
308
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
309
+ const newAtDaily = '17:07';
310
+ expect(at().value).toBe('');
311
+ await act(async () => {
312
+ fireEvent.change(at(), {
313
+ target: { value: newAtDaily },
314
+ });
315
+ });
316
+ expect(at().value).toBe(newAtDaily);
317
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
318
+
319
+ fireEvent.click(screen.getByText('Daily'));
320
+ await act(async () => {
321
+ fireEvent.click(screen.getByText('Hourly'));
322
+ });
323
+
324
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
325
+ const newMinutes = '6';
326
+ const atHourly = screen.getByLabelText('repeat-at-minute-typeahead');
327
+ expect(atHourly.value).toBe('');
328
+ await act(async () => {
329
+ fireEvent.click(screen.getByLabelText('select minute toggle'));
330
+ });
331
+ await act(async () => {
332
+ fireEvent.click(screen.getByText(newMinutes));
333
+ });
334
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
335
+ expect(atHourly.value).toBe(newMinutes);
336
+ });
337
+ it('should show invalid error on start date after end', async () => {
338
+ render(
339
+ <Provider store={store}>
340
+ <JobWizard />
341
+ </Provider>
342
+ );
343
+ await act(async () => {
344
+ await fireEvent.click(screen.getByText('Schedule'));
345
+ jest.runAllTimers();
346
+ });
347
+ const neverEnds = screen.getByLabelText('Never ends');
348
+ expect(neverEnds.checked).toBeFalsy();
349
+
350
+ const startsDateField = screen.getByLabelText('starts at datepicker');
351
+ const endsDateField = screen.getByLabelText('ends datepicker');
352
+
353
+ expect(
354
+ screen.queryAllByText('End time needs to be after start time')
355
+ ).toHaveLength(0);
356
+ expect(screen.getByText('Review Details').disabled).toBeFalsy();
357
+ await act(async () => {
358
+ await fireEvent.change(startsDateField, {
359
+ target: { value: '2020/10/15' },
360
+ });
361
+ await fireEvent.change(endsDateField, {
362
+ target: { value: '2020/10/14' },
363
+ });
364
+ await jest.runOnlyPendingTimers();
365
+ });
366
+
367
+ expect(
368
+ screen.queryAllByText('End time needs to be after start time')
369
+ ).toHaveLength(1);
370
+
371
+ expect(screen.getByText('Review Details').disabled).toBeTruthy();
372
+ });
373
+ it('purpose and ends should be disabled when no reaccurence ', async () => {
374
+ render(
375
+ <Provider store={store}>
376
+ <JobWizard />
377
+ </Provider>
378
+ );
379
+ await act(async () => {
380
+ await fireEvent.click(screen.getByText('Schedule'));
381
+ jest.runAllTimers();
382
+ });
383
+
384
+ const endsDateField = screen.getByLabelText('ends datepicker');
385
+ const endsTimeField = screen.getByLabelText('ends timepicker');
386
+ const purpose = screen.getByLabelText('purpose');
387
+ expect(endsDateField.disabled).toBeTruthy();
388
+ expect(endsTimeField.disabled).toBeTruthy();
389
+ expect(purpose.disabled).toBeTruthy();
390
+ await act(async () => {
391
+ fireEvent.click(
392
+ screen.getByLabelText('Does not repeat', { selector: 'button' })
393
+ );
394
+ });
395
+ await act(async () => {
396
+ fireEvent.click(screen.getByText('Cronline'));
397
+ });
398
+ expect(endsDateField.disabled).toBeFalsy();
399
+ expect(endsTimeField.disabled).toBeFalsy();
400
+ expect(purpose.disabled).toBeFalsy();
401
+ });
402
+ });
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen, act } from '@testing-library/react';
3
+ import { StartEndDates } from '../StartEndDates';
4
+
5
+ const setEnds = jest.fn();
6
+ const setIsNeverEnds = jest.fn();
7
+ const setValid = jest.fn();
8
+ const props = {
9
+ startsAt: '',
10
+ startsBefore: '',
11
+ setStartsAt: jest.fn(),
12
+ setStartsBefore: jest.fn(),
13
+ ends: 'some-end-date',
14
+ setEnds,
15
+ setIsNeverEnds,
16
+ isNeverEnds: false,
17
+ validEnd: true,
18
+ setValidEnd: setValid,
19
+ isFuture: false,
20
+ isStartBeforeDisabled: false,
21
+ isEndDisabled: false,
22
+ };
23
+
24
+ describe('StartEndDates', () => {
25
+ it('never ends', async () => {
26
+ await act(async () => render(<StartEndDates {...props} />));
27
+ const neverEnds = screen.getByRole('checkbox', { name: 'Never ends' });
28
+ await act(async () => fireEvent.click(neverEnds));
29
+ expect(setIsNeverEnds).toBeCalledWith(true);
30
+ expect(setValid).toBeCalledWith(true);
31
+ });
32
+ });