foreman_remote_execution 4.5.1 → 4.7.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_ci.yml +7 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +7 -0
  4. data/app/graphql/types/job_invocation.rb +16 -0
  5. data/app/helpers/concerns/foreman_remote_execution/hosts_helper_extensions.rb +5 -1
  6. data/app/helpers/remote_execution_helper.rb +9 -3
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  8. data/app/models/job_invocation_composer.rb +3 -3
  9. data/app/models/job_template.rb +1 -1
  10. data/app/models/remote_execution_feature.rb +5 -1
  11. data/app/models/remote_execution_provider.rb +1 -1
  12. data/app/views/templates/ssh/module_action.erb +1 -0
  13. data/app/views/templates/ssh/power_action.erb +2 -0
  14. data/app/views/templates/ssh/puppet_run_once.erb +1 -0
  15. data/foreman_remote_execution.gemspec +2 -4
  16. data/lib/foreman_remote_execution/engine.rb +3 -0
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/test/graphql/queries/job_invocation_query_test.rb +31 -0
  19. data/test/graphql/queries/job_invocations_query_test.rb +35 -0
  20. data/test/unit/concerns/host_extensions_test.rb +4 -4
  21. data/test/unit/input_template_renderer_test.rb +1 -89
  22. data/test/unit/job_invocation_composer_test.rb +1 -12
  23. data/webpack/JobWizard/JobWizard.js +28 -8
  24. data/webpack/JobWizard/JobWizard.scss +39 -0
  25. data/webpack/JobWizard/JobWizardConstants.js +10 -0
  26. data/webpack/JobWizard/JobWizardSelectors.js +9 -0
  27. data/webpack/JobWizard/__tests__/fixtures.js +104 -2
  28. data/webpack/JobWizard/__tests__/integration.test.js +13 -85
  29. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +21 -4
  30. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +67 -0
  31. data/webpack/JobWizard/steps/AdvancedFields/Fields.js +73 -59
  32. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +135 -16
  33. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +23 -0
  34. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.test.js +122 -51
  35. data/webpack/JobWizard/steps/Schedule/QueryType.js +48 -0
  36. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +61 -0
  37. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +25 -0
  38. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +51 -0
  39. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +22 -0
  40. data/webpack/JobWizard/steps/Schedule/index.js +41 -0
  41. data/webpack/JobWizard/steps/form/FormHelpers.js +1 -0
  42. data/webpack/JobWizard/steps/form/Formatter.js +149 -0
  43. data/webpack/JobWizard/steps/form/NumberInput.js +33 -0
  44. data/webpack/JobWizard/steps/form/SelectField.js +14 -2
  45. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +76 -0
  46. data/webpack/__mocks__/foremanReact/components/SearchBar.js +18 -1
  47. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +43 -0
  48. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +72 -66
  49. data/webpack/react_app/components/RecentJobsCard/RecentJobsTable.js +98 -0
  50. data/webpack/react_app/components/RecentJobsCard/constants.js +11 -0
  51. data/webpack/react_app/components/RecentJobsCard/styles.scss +11 -0
  52. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -0
  53. data/webpack/react_app/extend/fillRecentJobsCard.js +1 -1
  54. metadata +23 -27
  55. data/webpack/JobWizard/__tests__/JobWizard.test.js +0 -13
  56. data/webpack/JobWizard/__tests__/__snapshots__/JobWizard.test.js.snap +0 -32
  57. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +0 -249
  58. data/webpack/JobWizard/steps/CategoryAndTemplate/__snapshots__/CategoryAndTemplate.test.js.snap +0 -113
  59. data/webpack/JobWizard/steps/form/__tests__/GroupedSelectField.test.js +0 -38
  60. data/webpack/JobWizard/steps/form/__tests__/SelectField.test.js +0 -23
  61. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/GroupedSelectField.test.js.snap +0 -37
  62. data/webpack/JobWizard/steps/form/__tests__/__snapshots__/SelectField.test.js.snap +0 -23
  63. data/webpack/react_app/components/RecentJobsCard/styles.css +0 -15
@@ -0,0 +1,67 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormGroup, TextInput, Button } from '@patternfly/react-core';
4
+ import { translate as __ } from 'foremanReact/common/I18n';
5
+
6
+ export const DescriptionField = ({ inputs, value, setValue }) => {
7
+ const generateDesc = () => {
8
+ let newDesc = value;
9
+ if (value) {
10
+ const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
11
+ const results = [...newDesc.matchAll(re)].map(result => ({
12
+ name: result[1],
13
+ text: result[0],
14
+ }));
15
+ results.forEach(result => {
16
+ newDesc = newDesc.replace(
17
+ result.text,
18
+ // TODO: Replace with the value of the input from Target Hosts step
19
+ inputs.find(input => input.name === result.name)?.name || result.text
20
+ );
21
+ });
22
+ }
23
+ return newDesc;
24
+ };
25
+ const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
26
+ const [isPreview, setIsPreview] = useState(true);
27
+
28
+ const togglePreview = () => {
29
+ setGeneratedDesc(generateDesc());
30
+ setIsPreview(v => !v);
31
+ };
32
+
33
+ return (
34
+ <FormGroup
35
+ label={__('Description')}
36
+ fieldId="description"
37
+ helperText={
38
+ <Button variant="link" isInline onClick={togglePreview}>
39
+ {isPreview
40
+ ? __('Edit job description template')
41
+ : __('Preview job description')}
42
+ </Button>
43
+ }
44
+ >
45
+ {isPreview ? (
46
+ <TextInput id="description-preview" value={generatedDesc} isDisabled />
47
+ ) : (
48
+ <TextInput
49
+ type="text"
50
+ autoComplete="description"
51
+ id="description"
52
+ value={value}
53
+ onChange={newValue => setValue(newValue)}
54
+ />
55
+ )}
56
+ </FormGroup>
57
+ );
58
+ };
59
+
60
+ DescriptionField.propTypes = {
61
+ inputs: PropTypes.array.isRequired,
62
+ value: PropTypes.string,
63
+ setValue: PropTypes.func.isRequired,
64
+ };
65
+ DescriptionField.defaultProps = {
66
+ value: '',
67
+ };
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
3
3
  import { FormGroup, TextInput } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { helpLabel } from '../form/FormHelpers';
6
+ import { formatter } from '../form/Formatter';
7
+ import { NumberInput } from '../form/NumberInput';
6
8
 
7
9
  export const EffectiveUserField = ({ value, setValue }) => (
8
10
  <FormGroup
@@ -26,25 +28,25 @@ export const EffectiveUserField = ({ value, setValue }) => (
26
28
  );
27
29
 
28
30
  export const TimeoutToKillField = ({ value, setValue }) => (
29
- <FormGroup
30
- label={__('Timeout to kill')}
31
- labelIcon={helpLabel(
32
- __(
33
- 'Time in seconds from the start on the remote host after which the job should be killed.'
31
+ <NumberInput
32
+ formProps={{
33
+ label: __('Timeout to kill'),
34
+ labelIcon: helpLabel(
35
+ __(
36
+ 'Time in seconds from the start on the remote host after which the job should be killed.'
37
+ ),
38
+ 'timeout-to-kill'
34
39
  ),
35
- 'timeout-to-kill'
36
- )}
37
- fieldId="timeout-to-kill"
38
- >
39
- <TextInput
40
- type="number"
41
- value={value}
42
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
43
- autoComplete="timeout-to-kill"
44
- id="timeout-to-kill"
45
- onChange={newValue => setValue(newValue)}
46
- />
47
- </FormGroup>
40
+ fieldId: 'timeout-to-kill',
41
+ }}
42
+ inputProps={{
43
+ value,
44
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
45
+ autoComplete: 'timeout-to-kill',
46
+ id: 'timeout-to-kill',
47
+ onChange: newValue => setValue(newValue),
48
+ }}
49
+ />
48
50
  );
49
51
 
50
52
  export const PasswordField = ({ value, setValue }) => (
@@ -56,11 +58,11 @@ export const PasswordField = ({ value, setValue }) => (
56
58
  ),
57
59
  'password'
58
60
  )}
59
- fieldId="password"
61
+ fieldId="job-password"
60
62
  >
61
63
  <TextInput
62
- autoComplete="password"
63
- id="password"
64
+ autoComplete="new-password" // to prevent firefox from autofilling the user password
65
+ id="job-password"
64
66
  type="password"
65
67
  placeholder="*****"
66
68
  value={value}
@@ -114,51 +116,54 @@ export const EffectiveUserPasswordField = ({ value, setValue }) => (
114
116
  );
115
117
 
116
118
  export const ConcurrencyLevelField = ({ value, setValue }) => (
117
- <FormGroup
118
- label={__('Concurrency level')}
119
- labelIcon={helpLabel(
120
- __(
121
- 'Run at most N tasks at a time. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
119
+ <NumberInput
120
+ formProps={{
121
+ label: __('Concurrency level'),
122
+ labelIcon: helpLabel(
123
+ __(
124
+ 'Run at most N tasks at a time. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
125
+ ),
126
+ 'concurrency-level'
122
127
  ),
123
- 'concurrency-level'
124
- )}
125
- fieldId="concurrency-level"
126
- >
127
- <TextInput
128
- min={1}
129
- type="number"
130
- autoComplete="concurrency-level"
131
- id="concurrency-level"
132
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
133
- value={value}
134
- onChange={newValue => setValue(newValue)}
135
- />
136
- </FormGroup>
128
+ fieldId: 'concurrency-level',
129
+ }}
130
+ inputProps={{
131
+ min: 1,
132
+ autoComplete: 'concurrency-level',
133
+ id: 'concurrency-level',
134
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
135
+ value,
136
+ onChange: newValue => setValue(newValue),
137
+ }}
138
+ />
137
139
  );
138
140
 
139
141
  export const TimeSpanLevelField = ({ value, setValue }) => (
140
- <FormGroup
141
- label={__('Time span')}
142
- labelIcon={helpLabel(
143
- __(
144
- 'Distribute execution over N seconds. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
142
+ <NumberInput
143
+ formProps={{
144
+ label: __('Time span'),
145
+ labelIcon: helpLabel(
146
+ __(
147
+ 'Distribute execution over N seconds. If this is set and proxy batch triggering is enabled, then tasks are triggered on the smart proxy in batches of size 1.'
148
+ ),
149
+ 'time-span'
145
150
  ),
146
- 'time-span'
147
- )}
148
- fieldId="time-span"
149
- >
150
- <TextInput
151
- min={1}
152
- type="number"
153
- autoComplete="time-span"
154
- id="time-span"
155
- placeholder={__('For example: 1, 2, 3, 4, 5...')}
156
- value={value}
157
- onChange={newValue => setValue(newValue)}
158
- />
159
- </FormGroup>
151
+ fieldId: 'time-span',
152
+ }}
153
+ inputProps={{
154
+ min: 1,
155
+ autoComplete: 'time-span',
156
+ id: 'time-span',
157
+ placeholder: __('For example: 1, 2, 3, 4, 5...'),
158
+ value,
159
+ onChange: newValue => setValue(newValue),
160
+ }}
161
+ />
160
162
  );
161
163
 
164
+ export const TemplateInputsFields = ({ inputs, value, setValue }) => (
165
+ <>{inputs?.map(input => formatter(input, value, setValue))}</>
166
+ );
162
167
  EffectiveUserField.propTypes = {
163
168
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
164
169
  setValue: PropTypes.func.isRequired,
@@ -179,3 +184,12 @@ ConcurrencyLevelField.propTypes = EffectiveUserField.propTypes;
179
184
  ConcurrencyLevelField.defaultProps = EffectiveUserField.defaultProps;
180
185
  TimeSpanLevelField.propTypes = EffectiveUserField.propTypes;
181
186
  TimeSpanLevelField.defaultProps = EffectiveUserField.defaultProps;
187
+ TemplateInputsFields.propTypes = {
188
+ inputs: PropTypes.array.isRequired,
189
+ value: PropTypes.object,
190
+ setValue: PropTypes.func.isRequired,
191
+ };
192
+
193
+ TemplateInputsFields.defaultProps = {
194
+ value: {},
195
+ };
@@ -1,25 +1,144 @@
1
1
  import React from 'react';
2
2
  import { Provider } from 'react-redux';
3
- import configureMockStore from 'redux-mock-store';
4
- import * as patternfly from '@patternfly/react-core';
5
3
  import { mount } from '@theforeman/test';
6
- import { AdvancedFields } from '../AdvancedFields';
7
-
8
- jest.spyOn(patternfly, 'FormGroup');
9
- patternfly.FormGroup.mockImplementation(props => (
10
- <div>{props.navAriaLabel}</div>
11
- ));
12
- const mockStore = configureMockStore([]);
13
- const store = mockStore({
14
- JOB_TEMPLATE: { response: { effective_user: { overridable: true } } },
15
- });
4
+ import { fireEvent, screen, render, act } from '@testing-library/react';
5
+ import * as api from 'foremanReact/redux/API';
6
+ import { JobWizard } from '../../../JobWizard';
7
+ import * as selectors from '../../../JobWizardSelectors';
8
+ import {
9
+ jobTemplateResponse as jobTemplate,
10
+ testSetup,
11
+ mockApi,
12
+ } from '../../../__tests__/fixtures';
13
+
14
+ const store = testSetup(selectors, api);
15
+ mockApi(api);
16
+
17
+ jest.spyOn(selectors, 'selectEffectiveUser');
18
+ jest.spyOn(selectors, 'selectTemplateInputs');
19
+ jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
20
+
21
+ selectors.selectEffectiveUser.mockImplementation(
22
+ () => jobTemplate.effective_user
23
+ );
24
+ selectors.selectTemplateInputs.mockImplementation(
25
+ () => jobTemplate.template_inputs
26
+ );
27
+
28
+ selectors.selectAdvancedTemplateInputs.mockImplementation(
29
+ () => jobTemplate.advanced_template_inputs
30
+ );
16
31
  describe('AdvancedFields', () => {
17
- it('rendring', () => {
18
- const component = mount(
32
+ it('should save data between steps for advanced fields', async () => {
33
+ const wrapper = mount(
19
34
  <Provider store={store}>
20
- <AdvancedFields advancedValues={{}} setAdvancedValues={jest.fn()} />
35
+ <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
21
36
  </Provider>
22
37
  );
23
- expect(component).toMatchSnapshot();
38
+ // setup
39
+ wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
40
+ wrapper
41
+ .find('.pf-c-select__menu-item')
42
+ .first()
43
+ .simulate('click');
44
+
45
+ // test
46
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
47
+ 0
48
+ );
49
+ wrapper
50
+ .find('.pf-c-wizard__nav-link')
51
+ .at(2)
52
+ .simulate('click'); // Advanced step
53
+ const effectiveUserInput = () => wrapper.find('input#effective-user');
54
+ const advancedTemplateInput = () =>
55
+ wrapper.find('.pf-c-form__group-control textarea');
56
+ const effectiveUesrValue = 'effective user new value';
57
+ const advancedTemplateInputValue = 'advanced input new value';
58
+ effectiveUserInput().getDOMNode().value = effectiveUesrValue;
59
+
60
+ effectiveUserInput().simulate('change');
61
+ wrapper.update();
62
+ advancedTemplateInput().getDOMNode().value = advancedTemplateInputValue;
63
+ advancedTemplateInput().simulate('change');
64
+ expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
65
+ expect(advancedTemplateInput().prop('value')).toEqual(
66
+ advancedTemplateInputValue
67
+ );
68
+
69
+ wrapper
70
+ .find('.pf-c-wizard__nav-link')
71
+ .at(1)
72
+ .simulate('click');
73
+
74
+ expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
75
+ 'Target Hosts'
76
+ );
77
+ wrapper
78
+ .find('.pf-c-wizard__nav-link')
79
+ .at(2)
80
+ .simulate('click'); // Advanced step
81
+
82
+ expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
83
+ expect(advancedTemplateInput().prop('value')).toEqual(
84
+ advancedTemplateInputValue
85
+ );
86
+ });
87
+ it('fill template fields', async () => {
88
+ render(
89
+ <Provider store={store}>
90
+ <JobWizard />
91
+ </Provider>
92
+ );
93
+ await act(async () => {
94
+ fireEvent.click(screen.getByText('Advanced Fields'));
95
+ });
96
+ const searchValue = 'search test';
97
+ const textValue = 'I am a text';
98
+ const dateValue = '08/07/2021';
99
+ const textField = screen.getByLabelText('adv plain hidden', {
100
+ selector: 'textarea',
101
+ });
102
+ const selectField = screen.getByText('option 1');
103
+ const searchField = screen.getByPlaceholderText('Filter...');
104
+ const dateField = screen.getByLabelText('adv date', {
105
+ selector: 'input',
106
+ });
107
+
108
+ fireEvent.click(selectField);
109
+ await act(async () => {
110
+ await fireEvent.click(screen.getByText('option 2'));
111
+ fireEvent.click(screen.getAllByText('Advanced Fields')[0]); // to remove focus
112
+ await fireEvent.change(textField, {
113
+ target: { value: textValue },
114
+ });
115
+
116
+ await fireEvent.change(searchField, {
117
+ target: { value: searchValue },
118
+ });
119
+ await fireEvent.change(dateField, {
120
+ target: { value: dateValue },
121
+ });
122
+ });
123
+ expect(
124
+ screen.getByLabelText('adv plain hidden', {
125
+ selector: 'textarea',
126
+ }).value
127
+ ).toBe(textValue);
128
+ expect(searchField.value).toBe(searchValue);
129
+ expect(dateField.value).toBe(dateValue);
130
+ await act(async () => {
131
+ fireEvent.click(screen.getByText('Category and Template'));
132
+ });
133
+ expect(screen.getAllByText('Category and Template')).toHaveLength(3);
134
+
135
+ await act(async () => {
136
+ fireEvent.click(screen.getByText('Advanced Fields'));
137
+ });
138
+ expect(textField.value).toBe(textValue);
139
+ expect(searchField.value).toBe(searchValue);
140
+ expect(dateField.value).toBe(dateValue);
141
+ expect(screen.queryAllByText('option 1')).toHaveLength(0);
142
+ expect(screen.queryAllByText('option 2')).toHaveLength(1);
24
143
  });
25
144
  });
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { mount } from '@theforeman/test';
3
+ import { DescriptionField } from '../DescriptionField';
4
+
5
+ describe('DescriptionField', () => {
6
+ it('rendring', () => {
7
+ const component = mount(
8
+ <DescriptionField
9
+ inputs={[{ name: 'command' }]}
10
+ value="Run %{command}"
11
+ setValue={jest.fn()}
12
+ />
13
+ );
14
+ const preview = component.find('#description-preview').hostNodes();
15
+ const findLink = () => component.find('.pf-m-link.pf-m-inline');
16
+ expect(findLink().text()).toEqual('Edit job description template');
17
+ expect(preview.props().value).toEqual('Run command');
18
+ findLink().simulate('click');
19
+ const description = component.find('#description').hostNodes();
20
+ expect(description.props().value).toEqual('Run %{command}');
21
+ expect(findLink().text()).toEqual('Preview job description');
22
+ });
23
+ });
@@ -1,52 +1,123 @@
1
- import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
- import { CategoryAndTemplate } from './CategoryAndTemplate';
3
-
4
- const baseProps = {
5
- setJobTemplate: jest.fn(),
6
- selectedTemplateID: 190,
7
- setCategory: jest.fn(),
8
- };
9
- const fixtures = {
10
- 'renders with props': {
11
- ...baseProps,
12
- jobCategories: [
13
- 'Commands',
14
- 'Ansible Playbook',
15
- 'Ansible Galaxy',
16
- 'Ansible Roles Installation',
17
- ],
18
- jobTemplates: [
19
- {
20
- id: 190,
21
- name: 'ab Run Command - SSH Default clone',
22
- job_category: 'Commands',
23
- provider_type: 'SSH',
24
- snippet: false,
25
- },
26
- {
27
- id: 168,
28
- name: 'Ansible Roles - Ansible Default',
29
- job_category: 'Ansible Playbook',
30
- provider_type: 'Ansible',
31
- snippet: false,
32
- },
33
- {
34
- id: 170,
35
- name: 'Ansible Roles - Install from git',
36
- job_category: 'Ansible Roles Installation',
37
- provider_type: 'Ansible',
38
- snippet: false,
39
- },
40
- ],
41
- selectedCategory: 'I am a category',
42
- },
43
- 'render with error': {
44
- ...baseProps,
45
- errors: { allTemplatesError: 'I have an error' },
46
- },
47
- };
48
-
49
- describe('CategoryAndTemplate', () => {
50
- describe('rendering', () =>
51
- testComponentSnapshotsWithFixtures(CategoryAndTemplate, fixtures));
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { fireEvent, screen, render, act } from '@testing-library/react';
4
+ import * as api from 'foremanReact/redux/API';
5
+ import { JobWizard } from '../../JobWizard';
6
+ import * as selectors from '../../JobWizardSelectors';
7
+ import { testSetup, mockApi } from '../../__tests__/fixtures';
8
+
9
+ const store = testSetup(selectors, api);
10
+ mockApi(api);
11
+ jest.spyOn(selectors, 'selectCategoryError');
12
+ jest.spyOn(selectors, 'selectAllTemplatesError');
13
+ jest.spyOn(selectors, 'selectTemplateError');
14
+
15
+ describe('Category And Template', () => {
16
+ it('should select ', async () => {
17
+ selectors.selectCategoryError.mockImplementation(() => null);
18
+ selectors.selectAllTemplatesError.mockImplementation(() => null);
19
+ selectors.selectTemplateError.mockImplementation(() => null);
20
+ render(
21
+ <Provider store={store}>
22
+ <JobWizard />
23
+ </Provider>
24
+ );
25
+
26
+ expect(screen.queryAllByLabelText('Error')).toHaveLength(0);
27
+ expect(screen.queryAllByLabelText('failed')).toHaveLength(0);
28
+ // Category
29
+ fireEvent.click(
30
+ screen.getByLabelText('Ansible Commands', { selector: 'button' })
31
+ );
32
+ await act(async () => {
33
+ await fireEvent.click(screen.getByText('Puppet'));
34
+ });
35
+ fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
36
+ expect(
37
+ screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
38
+ ).toHaveLength(0);
39
+ expect(
40
+ screen.queryAllByLabelText('Puppet', { selector: 'button' })
41
+ ).toHaveLength(1);
42
+
43
+ // Template
44
+ fireEvent.click(
45
+ screen.getByDisplayValue('template1', { selector: 'button' })
46
+ );
47
+ await act(async () => {
48
+ await fireEvent.click(screen.getByText('template2'));
49
+ });
50
+ fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
51
+ expect(
52
+ screen.queryAllByDisplayValue('template1', { selector: 'button' })
53
+ ).toHaveLength(0);
54
+ expect(
55
+ screen.queryAllByDisplayValue('template2', { selector: 'button' })
56
+ ).toHaveLength(1);
57
+ });
58
+ it('category error ', async () => {
59
+ selectors.selectCategoryError.mockImplementation(() => 'category error');
60
+ selectors.selectAllTemplatesError.mockImplementation(() => null);
61
+ selectors.selectTemplateError.mockImplementation(() => null);
62
+ render(
63
+ <Provider store={store}>
64
+ <JobWizard />
65
+ </Provider>
66
+ );
67
+
68
+ expect(
69
+ screen.queryAllByText('Categories list failed with:', { exact: false })
70
+ ).toHaveLength(1);
71
+
72
+ expect(
73
+ screen.queryAllByText('Templates list failed with:', { exact: false })
74
+ ).toHaveLength(0);
75
+ expect(
76
+ screen.queryAllByText('Template failed with:', { exact: false })
77
+ ).toHaveLength(0);
78
+ });
79
+ it('templates error ', async () => {
80
+ selectors.selectCategoryError.mockImplementation(() => null);
81
+ selectors.selectAllTemplatesError.mockImplementation(
82
+ () => 'templates error'
83
+ );
84
+ selectors.selectTemplateError.mockImplementation(() => null);
85
+ render(
86
+ <Provider store={store}>
87
+ <JobWizard />
88
+ </Provider>
89
+ );
90
+
91
+ expect(
92
+ screen.queryAllByText('Categories list failed with:', { exact: false })
93
+ ).toHaveLength(0);
94
+
95
+ expect(
96
+ screen.queryAllByText('Templates list failed with:', { exact: false })
97
+ ).toHaveLength(1);
98
+ expect(
99
+ screen.queryAllByText('Template failed with:', { exact: false })
100
+ ).toHaveLength(0);
101
+ });
102
+ it('template error ', async () => {
103
+ selectors.selectCategoryError.mockImplementation(() => null);
104
+ selectors.selectAllTemplatesError.mockImplementation(() => null);
105
+ selectors.selectTemplateError.mockImplementation(() => 'template error');
106
+ render(
107
+ <Provider store={store}>
108
+ <JobWizard />
109
+ </Provider>
110
+ );
111
+
112
+ expect(
113
+ screen.queryAllByText('Categories list failed with:', { exact: false })
114
+ ).toHaveLength(0);
115
+
116
+ expect(
117
+ screen.queryAllByText('Templates list failed with:', { exact: false })
118
+ ).toHaveLength(0);
119
+ expect(
120
+ screen.queryAllByText('Template failed with:', { exact: false })
121
+ ).toHaveLength(1);
122
+ });
52
123
  });