foreman_remote_execution 4.5.6 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) 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/ui_job_wizard_controller.rb +16 -4
  6. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  7. data/app/graphql/types/job_invocation.rb +16 -0
  8. data/app/graphql/types/job_invocation_input.rb +13 -0
  9. data/app/graphql/types/recurrence_input.rb +8 -0
  10. data/app/graphql/types/scheduling_input.rb +6 -0
  11. data/app/graphql/types/targeting_enum.rb +7 -0
  12. data/app/lib/actions/remote_execution/run_host_job.rb +6 -1
  13. data/app/lib/actions/remote_execution/run_hosts_job.rb +57 -3
  14. data/app/mailers/rex_job_mailer.rb +15 -0
  15. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  16. data/app/models/job_invocation.rb +4 -0
  17. data/app/models/job_invocation_composer.rb +21 -13
  18. data/app/models/job_template.rb +1 -1
  19. data/app/models/remote_execution_provider.rb +17 -2
  20. data/app/models/rex_mail_notification.rb +13 -0
  21. data/app/models/targeting.rb +2 -2
  22. data/app/services/ui_notifications/remote_execution_jobs/base_job_finish.rb +2 -1
  23. data/app/views/dashboard/_latest-jobs.html.erb +21 -0
  24. data/app/views/job_invocations/refresh.js.erb +1 -0
  25. data/app/views/rex_job_mailer/job_finished.html.erb +24 -0
  26. data/app/views/rex_job_mailer/job_finished.text.erb +9 -0
  27. data/app/views/template_invocations/show.html.erb +2 -1
  28. data/config/routes.rb +1 -0
  29. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  30. data/db/seeds.d/50-notification_blueprints.rb +14 -0
  31. data/db/seeds.d/95-mail_notifications.rb +24 -0
  32. data/foreman_remote_execution.gemspec +2 -4
  33. data/lib/foreman_remote_execution/engine.rb +114 -6
  34. data/lib/foreman_remote_execution/version.rb +1 -1
  35. data/package.json +6 -6
  36. data/test/functional/api/v2/job_invocations_controller_test.rb +20 -0
  37. data/test/functional/cockpit_controller_test.rb +0 -1
  38. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  39. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  40. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  41. data/test/helpers/remote_execution_helper_test.rb +0 -1
  42. data/test/unit/actions/run_host_job_test.rb +21 -0
  43. data/test/unit/actions/run_hosts_job_test.rb +99 -4
  44. data/test/unit/concerns/host_extensions_test.rb +40 -7
  45. data/test/unit/input_template_renderer_test.rb +1 -89
  46. data/test/unit/job_invocation_composer_test.rb +4 -17
  47. data/test/unit/job_invocation_report_template_test.rb +16 -13
  48. data/test/unit/job_template_effective_user_test.rb +0 -4
  49. data/test/unit/remote_execution_provider_test.rb +34 -4
  50. data/test/unit/targeting_test.rb +68 -1
  51. data/webpack/JobWizard/JobWizard.js +106 -15
  52. data/webpack/JobWizard/JobWizard.scss +73 -39
  53. data/webpack/JobWizard/JobWizardConstants.js +36 -0
  54. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  55. data/webpack/JobWizard/__tests__/fixtures.js +81 -6
  56. data/webpack/JobWizard/__tests__/integration.test.js +26 -15
  57. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  58. data/webpack/JobWizard/autofill.js +38 -0
  59. data/webpack/JobWizard/index.js +7 -0
  60. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
  61. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  62. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
  63. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  64. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
  65. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  66. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  67. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  68. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  69. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  70. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
  71. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  72. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
  73. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  74. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  75. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  76. data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
  77. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  78. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  79. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  80. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  81. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  82. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  83. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  84. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  85. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  86. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +24 -21
  87. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +78 -23
  88. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +402 -0
  89. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +20 -10
  90. data/webpack/JobWizard/steps/Schedule/index.js +153 -19
  91. data/webpack/JobWizard/steps/form/DateTimePicker.js +126 -0
  92. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  93. data/webpack/JobWizard/steps/form/Formatter.js +39 -8
  94. data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
  95. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  96. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  97. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  98. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  99. data/webpack/JobWizard/submit.js +120 -0
  100. data/webpack/JobWizard/validation.js +53 -0
  101. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  102. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  103. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  104. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  105. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  106. data/webpack/helpers.js +1 -0
  107. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  108. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +73 -66
  109. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  110. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  111. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  112. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  113. metadata +56 -23
  114. data/app/models/setting/remote_execution.rb +0 -88
  115. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  116. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
  117. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
  118. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -0,0 +1,70 @@
1
+ import React, { useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, Checkbox } from '@patternfly/react-core';
4
+ import { translate as __, documentLocale } from 'foremanReact/common/I18n';
5
+ import { RepeatDaily } from './RepeatDaily';
6
+ import { noop } from '../../../helpers';
7
+
8
+ const getWeekDays = () => {
9
+ const locale = documentLocale().replace(/-/g, '_');
10
+ const baseDate = new Date(Date.UTC(2017, 0, 2)); // just a Monday
11
+ const weekDays = [];
12
+ for (let i = 0; i < 7; i++) {
13
+ try {
14
+ weekDays.push(baseDate.toLocaleDateString(locale, { weekday: 'short' }));
15
+ } catch {
16
+ weekDays.push(baseDate.toLocaleDateString('en', { weekday: 'short' }));
17
+ }
18
+ baseDate.setDate(baseDate.getDate() + 1);
19
+ }
20
+ return weekDays;
21
+ };
22
+
23
+ export const RepeatWeek = ({ repeatData, setRepeatData, setValid }) => {
24
+ const { daysOfWeek, at } = repeatData;
25
+ useEffect(() => {
26
+ if (daysOfWeek && Object.values(daysOfWeek).includes(true) && at) {
27
+ setValid(true);
28
+ } else {
29
+ setValid(false);
30
+ }
31
+ return () => setValid(true);
32
+ }, [setValid, daysOfWeek, at]);
33
+ const days = getWeekDays();
34
+ const handleChangeDays = (checked, { target: { name } }) => {
35
+ setRepeatData({
36
+ ...repeatData,
37
+ daysOfWeek: { ...repeatData.daysOfWeek, [name]: checked },
38
+ });
39
+ };
40
+ return (
41
+ <>
42
+ <FormGroup label={__('Days of week')} isRequired>
43
+ <div id="repeat-on-weekly">
44
+ {days.map((day, index) => (
45
+ <Checkbox
46
+ aria-label={`${day} checkbox`}
47
+ key={index}
48
+ isChecked={daysOfWeek?.[index]}
49
+ name={index}
50
+ id={`repeat-on-day-${index}`}
51
+ onChange={handleChangeDays}
52
+ label={day}
53
+ />
54
+ ))}
55
+ </div>
56
+ </FormGroup>
57
+
58
+ <RepeatDaily
59
+ repeatData={repeatData}
60
+ setRepeatData={setRepeatData}
61
+ setValid={noop}
62
+ />
63
+ </>
64
+ );
65
+ };
66
+ RepeatWeek.propTypes = {
67
+ repeatData: PropTypes.object.isRequired,
68
+ setRepeatData: PropTypes.func.isRequired,
69
+ setValid: PropTypes.func.isRequired,
70
+ };
@@ -1,25 +1,28 @@
1
- import React, { useState } from 'react';
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
2
3
  import { FormGroup, Radio } from '@patternfly/react-core';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
5
 
5
- export const ScheduleType = () => {
6
- const [isFuture, setIsFuture] = useState(false);
7
- return (
8
- <FormGroup label={__('Schedule type')} fieldId="schedule-type">
9
- <Radio
10
- isChecked={!isFuture}
11
- name="schedule-type"
12
- onChange={() => setIsFuture(false)}
13
- id="schedule-type-now"
14
- label={__('Execute now')}
15
- />
16
- <Radio
17
- isChecked={isFuture}
18
- name="schedule-type"
19
- onChange={() => setIsFuture(true)}
20
- id="schedule-type-future"
21
- label={__('Schedule for future execution')}
22
- />
23
- </FormGroup>
24
- );
6
+ export const ScheduleType = ({ isFuture, setIsFuture }) => (
7
+ <FormGroup label={__('Schedule type')} fieldId="schedule-type">
8
+ <Radio
9
+ isChecked={!isFuture}
10
+ name="schedule-type"
11
+ onChange={() => setIsFuture(false)}
12
+ id="schedule-type-now"
13
+ label={__('Execute now')}
14
+ />
15
+ <Radio
16
+ isChecked={isFuture}
17
+ name="schedule-type"
18
+ onChange={() => setIsFuture(true)}
19
+ id="schedule-type-future"
20
+ label={__('Schedule for future execution')}
21
+ />
22
+ </FormGroup>
23
+ );
24
+
25
+ ScheduleType.propTypes = {
26
+ isFuture: PropTypes.bool.isRequired,
27
+ setIsFuture: PropTypes.func.isRequired,
25
28
  };
@@ -1,35 +1,81 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { FormGroup, TextInput, Checkbox } from '@patternfly/react-core';
3
+ import { FormGroup, Checkbox } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { DateTimePicker } from '../form/DateTimePicker';
6
+ import { helpLabel } from '../form/FormHelpers';
5
7
 
6
- // TODO: change to datepicker
7
- export const StartEndDates = ({ starts, setStarts, ends, setEnds }) => {
8
- const [isNeverEnds, setIsNeverEnds] = useState(false);
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
+ }) => {
9
23
  const toggleIsNeverEnds = (checked, event) => {
10
24
  const value = event?.target?.checked;
11
25
  setIsNeverEnds(value);
12
- setEnds('');
13
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]);
14
39
  return (
15
40
  <>
16
- <FormGroup label={__('Starts')} fieldId="start-date">
17
- <TextInput
18
- id="start-date"
19
- value={starts}
20
- type="text"
21
- onChange={newValue => setStarts(newValue)}
22
- placeholder="mm/dd/yy, hh:mm UTC"
41
+ <FormGroup label={__('Starts at')} fieldId="start-at-date">
42
+ <DateTimePicker
43
+ allowEmpty={!isFuture}
44
+ ariaLabel="starts at"
45
+ dateTime={startsAt}
46
+ setDateTime={setStartsAt}
23
47
  />
24
48
  </FormGroup>
25
- <FormGroup label={__('Ends')} fieldId="end-date">
26
- <TextInput
27
- isDisabled={isNeverEnds}
28
- id="end-date"
29
- value={ends}
30
- type="text"
31
- onChange={newValue => setEnds(newValue)}
32
- placeholder="mm/dd/yy, hh:mm UTC"
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}
33
79
  />
34
80
  <Checkbox
35
81
  label={__('Never ends')}
@@ -44,8 +90,17 @@ export const StartEndDates = ({ starts, setStarts, ends, setEnds }) => {
44
90
  };
45
91
 
46
92
  StartEndDates.propTypes = {
47
- starts: PropTypes.string.isRequired,
48
- setStarts: PropTypes.func.isRequired,
93
+ startsAt: PropTypes.string.isRequired,
94
+ setStartsAt: PropTypes.func.isRequired,
95
+ startsBefore: PropTypes.string.isRequired,
96
+ setStartsBefore: PropTypes.func.isRequired,
49
97
  ends: PropTypes.string.isRequired,
50
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,
51
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
+ });
@@ -1,22 +1,32 @@
1
1
  import React from 'react';
2
- import { render, fireEvent, screen } from '@testing-library/react';
2
+ import { render, fireEvent, screen, act } from '@testing-library/react';
3
3
  import { StartEndDates } from '../StartEndDates';
4
4
 
5
5
  const setEnds = jest.fn();
6
+ const setIsNeverEnds = jest.fn();
7
+ const setValid = jest.fn();
6
8
  const props = {
7
- starts: '',
8
- setStarts: jest.fn(),
9
+ startsAt: '',
10
+ startsBefore: '',
11
+ setStartsAt: jest.fn(),
12
+ setStartsBefore: jest.fn(),
9
13
  ends: 'some-end-date',
10
14
  setEnds,
15
+ setIsNeverEnds,
16
+ isNeverEnds: false,
17
+ validEnd: true,
18
+ setValidEnd: setValid,
19
+ isFuture: false,
20
+ isStartBeforeDisabled: false,
21
+ isEndDisabled: false,
11
22
  };
12
23
 
13
24
  describe('StartEndDates', () => {
14
- it('never ends', () => {
15
- render(<StartEndDates {...props} />);
16
- const neverEnds = screen.getByLabelText('Never ends', {
17
- selector: 'input',
18
- });
19
- fireEvent.click(neverEnds);
20
- expect(setEnds).toBeCalledWith('');
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);
21
31
  });
22
32
  });