foreman_remote_execution 4.5.1 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
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
  });