foreman_remote_execution 4.8.0 → 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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +9 -0
  3. data/app/controllers/ui_job_wizard_controller.rb +16 -4
  4. data/app/graphql/mutations/job_invocations/create.rb +43 -0
  5. data/app/graphql/types/job_invocation_input.rb +13 -0
  6. data/app/graphql/types/recurrence_input.rb +8 -0
  7. data/app/graphql/types/scheduling_input.rb +6 -0
  8. data/app/graphql/types/targeting_enum.rb +7 -0
  9. data/app/lib/actions/remote_execution/run_host_job.rb +4 -0
  10. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +8 -0
  11. data/app/models/job_invocation_composer.rb +1 -1
  12. data/app/models/targeting.rb +2 -2
  13. data/app/views/job_invocations/refresh.js.erb +1 -0
  14. data/config/routes.rb +1 -0
  15. data/db/migrate/20210816100932_rex_setting_category_to_dsl.rb +5 -0
  16. data/lib/foreman_remote_execution/engine.rb +110 -6
  17. data/lib/foreman_remote_execution/version.rb +1 -1
  18. data/package.json +6 -6
  19. data/test/functional/api/v2/job_invocations_controller_test.rb +10 -0
  20. data/test/functional/cockpit_controller_test.rb +0 -1
  21. data/test/graphql/mutations/job_invocations/create.rb +58 -0
  22. data/test/helpers/remote_execution_helper_test.rb +0 -1
  23. data/test/unit/actions/run_host_job_test.rb +21 -0
  24. data/test/unit/concerns/host_extensions_test.rb +36 -3
  25. data/test/unit/job_invocation_composer_test.rb +3 -5
  26. data/test/unit/job_invocation_report_template_test.rb +1 -1
  27. data/test/unit/job_template_effective_user_test.rb +0 -4
  28. data/test/unit/remote_execution_provider_test.rb +0 -4
  29. data/test/unit/targeting_test.rb +68 -1
  30. data/webpack/JobWizard/JobWizard.js +94 -13
  31. data/webpack/JobWizard/JobWizard.scss +59 -35
  32. data/webpack/JobWizard/JobWizardConstants.js +28 -1
  33. data/webpack/JobWizard/JobWizardSelectors.js +32 -0
  34. data/webpack/JobWizard/__tests__/fixtures.js +81 -6
  35. data/webpack/JobWizard/__tests__/integration.test.js +26 -15
  36. data/webpack/JobWizard/__tests__/validation.test.js +141 -0
  37. data/webpack/JobWizard/autofill.js +38 -0
  38. data/webpack/JobWizard/index.js +7 -0
  39. data/webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js +7 -4
  40. data/webpack/JobWizard/steps/AdvancedFields/DescriptionField.js +32 -9
  41. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +216 -12
  42. data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +82 -0
  43. data/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +1 -0
  44. data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +3 -2
  45. data/webpack/JobWizard/steps/HostsAndInputs/HostPreviewModal.js +62 -0
  46. data/webpack/JobWizard/steps/HostsAndInputs/HostSearch.js +54 -0
  47. data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +33 -0
  48. data/webpack/JobWizard/steps/HostsAndInputs/SelectGQL.js +52 -0
  49. data/webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js +82 -7
  50. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/HostsAndInputs.test.js +151 -0
  51. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js +7 -4
  52. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +18 -0
  53. data/webpack/JobWizard/steps/HostsAndInputs/hostgroups.gql +8 -0
  54. data/webpack/JobWizard/steps/HostsAndInputs/hosts.gql +8 -0
  55. data/webpack/JobWizard/steps/HostsAndInputs/index.js +182 -34
  56. data/webpack/JobWizard/steps/ReviewDetails/index.js +193 -0
  57. data/webpack/JobWizard/steps/Schedule/PurposeField.js +31 -0
  58. data/webpack/JobWizard/steps/Schedule/QueryType.js +46 -43
  59. data/webpack/JobWizard/steps/Schedule/RepeatCron.js +53 -0
  60. data/webpack/JobWizard/steps/Schedule/RepeatDaily.js +37 -0
  61. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +54 -0
  62. data/webpack/JobWizard/steps/Schedule/RepeatMonth.js +46 -0
  63. data/webpack/JobWizard/steps/Schedule/RepeatOn.js +95 -31
  64. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +70 -0
  65. data/webpack/JobWizard/steps/Schedule/StartEndDates.js +59 -19
  66. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +258 -11
  67. data/webpack/JobWizard/steps/Schedule/__tests__/StartEndDates.test.js +11 -2
  68. data/webpack/JobWizard/steps/Schedule/index.js +97 -21
  69. data/webpack/JobWizard/steps/form/DateTimePicker.js +41 -8
  70. data/webpack/JobWizard/steps/form/FormHelpers.js +4 -0
  71. data/webpack/JobWizard/steps/form/Formatter.js +39 -8
  72. data/webpack/JobWizard/steps/form/NumberInput.js +3 -2
  73. data/webpack/JobWizard/steps/form/ResourceSelect.js +29 -0
  74. data/webpack/JobWizard/steps/form/SearchSelect.js +121 -0
  75. data/webpack/JobWizard/steps/form/SelectField.js +14 -3
  76. data/webpack/JobWizard/steps/form/__tests__/SelectSearch.test.js +33 -0
  77. data/webpack/JobWizard/submit.js +120 -0
  78. data/webpack/JobWizard/validation.js +53 -0
  79. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +2 -0
  80. data/webpack/__mocks__/foremanReact/common/I18n.js +2 -0
  81. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js +1 -0
  82. data/webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js +1 -0
  83. data/webpack/__mocks__/foremanReact/routes/RouterSelector.js +1 -0
  84. data/webpack/helpers.js +1 -0
  85. data/webpack/react_app/components/RecentJobsCard/RecentJobsCard.js +1 -1
  86. metadata +38 -6
  87. data/app/models/setting/remote_execution.rb +0 -94
  88. data/webpack/JobWizard/steps/AdvancedFields/__tests__/DescriptionField.test.js +0 -23
  89. data/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js +0 -37
  90. data/webpack/JobWizard/steps/form/__tests__/Formatter.test.js.example +0 -76
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { Provider } from 'react-redux';
3
3
  import { mount } from '@theforeman/test';
4
4
  import { render, fireEvent, screen, act } from '@testing-library/react';
5
+ import { MockedProvider } from '@apollo/client/testing';
5
6
  import * as api from 'foremanReact/redux/API';
6
7
  import { JobWizard } from '../JobWizard';
7
8
  import * as selectors from '../JobWizardSelectors';
@@ -11,23 +12,31 @@ import {
11
12
  mockApi,
12
13
  jobCategories,
13
14
  jobTemplateResponse as jobTemplate,
15
+ gqlMock,
14
16
  } from './fixtures';
15
17
 
16
18
  const store = testSetup(selectors, api);
17
19
 
18
- selectors.selectJobTemplate.mockImplementation(() => {});
19
-
20
- api.get.mockImplementation(({ handleSuccess, ...action }) => {
21
- if (action.key === 'JOB_CATEGORIES') {
22
- handleSuccess && handleSuccess({ data: { job_categories: jobCategories } });
23
- }
24
- return { type: 'get', ...action };
25
- });
26
20
  describe('Job wizard fill', () => {
27
21
  it('should select template', async () => {
22
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
23
+ if (action.key === 'JOB_CATEGORIES') {
24
+ handleSuccess &&
25
+ handleSuccess({ data: { job_categories: jobCategories } });
26
+ } else if (action.key === 'JOB_TEMPLATE') {
27
+ handleSuccess &&
28
+ handleSuccess({
29
+ data: jobTemplate,
30
+ });
31
+ }
32
+ return { type: 'get', ...action };
33
+ });
34
+ selectors.selectJobTemplate.mockRestore();
35
+ jest.spyOn(selectors, 'selectJobTemplate');
36
+ selectors.selectJobTemplate.mockImplementation(() => ({}));
28
37
  const wrapper = mount(
29
38
  <Provider store={store}>
30
- <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
39
+ <JobWizard />
31
40
  </Provider>
32
41
  );
33
42
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
@@ -35,7 +44,8 @@ describe('Job wizard fill', () => {
35
44
  );
36
45
  selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
37
46
  expect(store.getActions()).toMatchSnapshot('initial');
38
-
47
+ selectors.selectJobTemplate.mockRestore();
48
+ jest.spyOn(selectors, 'selectJobTemplate');
39
49
  selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
40
50
  wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
41
51
  await act(async () => {
@@ -43,9 +53,9 @@ describe('Job wizard fill', () => {
43
53
  .find('.pf-c-select__menu-item')
44
54
  .first()
45
55
  .simulate('click');
46
- await wrapper.update();
47
56
  });
48
57
  expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
58
+ wrapper.update();
49
59
  expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-disabled')).toHaveLength(
50
60
  0
51
61
  );
@@ -53,15 +63,16 @@ describe('Job wizard fill', () => {
53
63
 
54
64
  it('have all steps', async () => {
55
65
  selectors.selectJobCategoriesStatus.mockImplementation(() => null);
56
- selectors.selectJobTemplate.mockRestore();
57
66
  selectors.selectJobTemplates.mockRestore();
58
67
  selectors.selectJobCategories.mockRestore();
59
68
  mockApi(api);
60
69
 
61
70
  render(
62
- <Provider store={store}>
63
- <JobWizard />
64
- </Provider>
71
+ <MockedProvider mocks={gqlMock} addTypename={false}>
72
+ <Provider store={store}>
73
+ <JobWizard />
74
+ </Provider>
75
+ </MockedProvider>
65
76
  );
66
77
  const titles = Object.values(WIZARD_TITLES);
67
78
  const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
@@ -0,0 +1,141 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { render, fireEvent, screen, act } from '@testing-library/react';
4
+ import { MockedProvider } from '@apollo/client/testing';
5
+ import '@testing-library/jest-dom';
6
+ import * as api from 'foremanReact/redux/API';
7
+ import { JobWizard } from '../JobWizard';
8
+ import * as selectors from '../JobWizardSelectors';
9
+ import { testSetup, mockApi, jobTemplateResponse, gqlMock } from './fixtures';
10
+ import { WIZARD_TITLES } from '../JobWizardConstants';
11
+
12
+ const store = testSetup(selectors, api);
13
+
14
+ mockApi(api);
15
+ const templateInputs = [...jobTemplateResponse.template_inputs];
16
+ const advancedTemplateInputs = [
17
+ ...jobTemplateResponse.advanced_template_inputs,
18
+ ];
19
+ templateInputs[0].default = null;
20
+ advancedTemplateInputs[0].default = null;
21
+ selectors.selectTemplateInputs.mockImplementation(() => templateInputs);
22
+ selectors.selectAdvancedTemplateInputs.mockImplementation(
23
+ () => advancedTemplateInputs
24
+ );
25
+
26
+ describe('Job wizard validation', () => {
27
+ afterAll(() => {
28
+ selectors.selectTemplateInputs.mockRestore();
29
+ selectors.selectAdvancedTemplateInputs.mockRestore();
30
+ });
31
+ it('requeried', async () => {
32
+ render(
33
+ <MockedProvider mocks={gqlMock} addTypename={false}>
34
+ <Provider store={store}>
35
+ <JobWizard />
36
+ </Provider>
37
+ </MockedProvider>
38
+ );
39
+ expect(screen.getByText(WIZARD_TITLES.advanced)).toBeDisabled();
40
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
41
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
42
+ await act(async () => {
43
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
44
+ });
45
+ const textField = screen.getByLabelText('plain hidden', {
46
+ selector: 'textarea',
47
+ });
48
+ await act(async () => {
49
+ await fireEvent.change(textField, {
50
+ target: { value: 'text' },
51
+ });
52
+ });
53
+ expect(screen.getByText(WIZARD_TITLES.advanced)).toBeEnabled();
54
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
55
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
56
+
57
+ await act(async () => {
58
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
59
+ });
60
+ const advTextField = screen.getByLabelText('adv plain hidden', {
61
+ selector: 'textarea',
62
+ });
63
+ await act(async () => {
64
+ await fireEvent.change(advTextField, {
65
+ target: { value: 'text' },
66
+ });
67
+ });
68
+
69
+ expect(
70
+ screen.getByText(WIZARD_TITLES.advanced, { selector: 'button' })
71
+ ).toBeEnabled();
72
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
73
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
74
+ });
75
+
76
+ it('advanced number', async () => {
77
+ render(
78
+ <MockedProvider mocks={gqlMock} addTypename={false}>
79
+ <Provider store={store}>
80
+ <JobWizard />
81
+ </Provider>
82
+ </MockedProvider>
83
+ );
84
+
85
+ // setup
86
+ await act(async () => {
87
+ fireEvent.click(screen.getByText(WIZARD_TITLES.hostsAndInputs));
88
+ });
89
+ await act(async () => {
90
+ await fireEvent.change(
91
+ screen.getByLabelText('plain hidden', {
92
+ selector: 'textarea',
93
+ }),
94
+ {
95
+ target: { value: 'text' },
96
+ }
97
+ );
98
+ });
99
+
100
+ await act(async () => {
101
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
102
+ });
103
+ await act(async () => {
104
+ await fireEvent.change(
105
+ screen.getByLabelText('adv plain hidden', {
106
+ selector: 'textarea',
107
+ }),
108
+ {
109
+ target: { value: 'text' },
110
+ }
111
+ );
112
+ });
113
+ expect(
114
+ screen.getByText(WIZARD_TITLES.advanced, { selector: 'button' })
115
+ ).toBeEnabled();
116
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
117
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
118
+
119
+ // test
120
+ const timeoutField = screen.getByLabelText('timeout to kill', {
121
+ selector: 'input',
122
+ });
123
+ await act(async () => {
124
+ await fireEvent.change(timeoutField, {
125
+ target: { value: 'text' },
126
+ });
127
+ });
128
+
129
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeDisabled();
130
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeDisabled();
131
+
132
+ await act(async () => {
133
+ await fireEvent.change(timeoutField, {
134
+ target: { value: 123 },
135
+ });
136
+ });
137
+
138
+ expect(screen.getByText(WIZARD_TITLES.schedule)).toBeEnabled();
139
+ expect(screen.getByText(WIZARD_TITLES.review)).toBeEnabled();
140
+ });
141
+ });
@@ -0,0 +1,38 @@
1
+ import { useEffect } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import { get } from 'foremanReact/redux/API';
4
+ import { HOST_IDS } from './JobWizardConstants';
5
+ import { selectRouterSearch } from './JobWizardSelectors';
6
+ import './JobWizard.scss';
7
+
8
+ export const useAutoFill = ({ setSelectedTargets, setHostsSearchQuery }) => {
9
+ const fills = useSelector(selectRouterSearch);
10
+ const dispatch = useDispatch();
11
+
12
+ useEffect(() => {
13
+ if (Object.keys(fills).length) {
14
+ if (fills['host_ids[]']) {
15
+ dispatch(
16
+ get({
17
+ key: HOST_IDS,
18
+ url: '/api/hosts',
19
+ params: { search: `id = ${fills['host_ids[]'].join(' or id = ')}` },
20
+ handleSuccess: ({ data }) => {
21
+ setSelectedTargets(currentTargets => ({
22
+ ...currentTargets,
23
+ hosts: (data.results || []).map(({ name }) => ({
24
+ id: name,
25
+ name,
26
+ })),
27
+ }));
28
+ },
29
+ })
30
+ );
31
+ }
32
+ if (fills.search) {
33
+ setHostsSearchQuery(fills.search);
34
+ }
35
+ }
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, []);
38
+ };
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import PropTypes from 'prop-types';
2
3
  import { Title, Divider } from '@patternfly/react-core';
3
4
  import { translate as __ } from 'foremanReact/common/I18n';
4
5
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
@@ -29,4 +30,10 @@ const JobWizardPage = () => {
29
30
  );
30
31
  };
31
32
 
33
+ JobWizardPage.propTypes = {
34
+ location: PropTypes.shape({
35
+ search: PropTypes.string,
36
+ }).isRequired,
37
+ };
38
+
32
39
  export default JobWizardPage;
@@ -5,7 +5,6 @@ import { Form } from '@patternfly/react-core';
5
5
  import {
6
6
  selectEffectiveUser,
7
7
  selectAdvancedTemplateInputs,
8
- selectTemplateInputs,
9
8
  } from '../../JobWizardSelectors';
10
9
  import {
11
10
  EffectiveUserField,
@@ -22,10 +21,13 @@ import { DescriptionField } from './DescriptionField';
22
21
  import { WIZARD_TITLES } from '../../JobWizardConstants';
23
22
  import { WizardTitle } from '../form/WizardTitle';
24
23
 
25
- export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
24
+ export const AdvancedFields = ({
25
+ templateValues,
26
+ advancedValues,
27
+ setAdvancedValues,
28
+ }) => {
26
29
  const effectiveUser = useSelector(selectEffectiveUser);
27
30
  const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
28
- const templateInputs = useSelector(selectTemplateInputs);
29
31
  return (
30
32
  <>
31
33
  <WizardTitle
@@ -49,7 +51,7 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
49
51
  />
50
52
  )}
51
53
  <DescriptionField
52
- inputs={templateInputs}
54
+ inputValues={{ ...templateValues, ...advancedValues.templateValues }}
53
55
  value={advancedValues.description}
54
56
  setValue={newValue => setAdvancedValues({ description: newValue })}
55
57
  />
@@ -117,5 +119,6 @@ export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
117
119
  AdvancedFields.propTypes = {
118
120
  advancedValues: PropTypes.object.isRequired,
119
121
  setAdvancedValues: PropTypes.func.isRequired,
122
+ templateValues: PropTypes.object.isRequired,
120
123
  };
121
124
  export default AdvancedFields;
@@ -1,10 +1,19 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useSelector } from 'react-redux';
2
3
  import PropTypes from 'prop-types';
3
- import { FormGroup, TextInput, Button } from '@patternfly/react-core';
4
+ import { FormGroup, TextInput, Tooltip, Button } from '@patternfly/react-core';
4
5
  import { translate as __ } from 'foremanReact/common/I18n';
6
+ import {
7
+ selectTemplateInputs,
8
+ selectAdvancedTemplateInputs,
9
+ } from '../../JobWizardSelectors';
5
10
 
6
- export const DescriptionField = ({ inputs, value, setValue }) => {
7
- const generateDesc = () => {
11
+ export const DescriptionField = ({ inputValues, value, setValue }) => {
12
+ const inputs = [
13
+ ...useSelector(selectTemplateInputs),
14
+ ...useSelector(selectAdvancedTemplateInputs),
15
+ ].map(input => input.name);
16
+ const generateDesc = useCallback(() => {
8
17
  let newDesc = value;
9
18
  if (value) {
10
19
  const re = new RegExp('%\\{([^\\}]+)\\}', 'gm');
@@ -15,16 +24,19 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
15
24
  results.forEach(result => {
16
25
  newDesc = newDesc.replace(
17
26
  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
27
+ inputValues[result.name] ||
28
+ (inputs.includes(result.name) ? '' : result.text)
20
29
  );
21
30
  });
22
31
  }
23
32
  return newDesc;
24
- };
33
+ }, [inputs, value, inputValues]);
25
34
  const [generatedDesc, setGeneratedDesc] = useState(generateDesc());
26
35
  const [isPreview, setIsPreview] = useState(true);
27
36
 
37
+ useEffect(() => {
38
+ setGeneratedDesc(generateDesc());
39
+ }, [generateDesc]);
28
40
  const togglePreview = () => {
29
41
  setGeneratedDesc(generateDesc());
30
42
  setIsPreview(v => !v);
@@ -43,9 +55,20 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
43
55
  }
44
56
  >
45
57
  {isPreview ? (
46
- <TextInput id="description-preview" value={generatedDesc} isDisabled />
58
+ <Tooltip content={generatedDesc}>
59
+ <div>
60
+ {/* div wrapper so the tooltip will be shown in chrome */}
61
+ <TextInput
62
+ aria-label="description preview"
63
+ id="description-preview"
64
+ value={generatedDesc}
65
+ isDisabled
66
+ />
67
+ </div>
68
+ </Tooltip>
47
69
  ) : (
48
70
  <TextInput
71
+ aria-label="description edit"
49
72
  type="text"
50
73
  autoComplete="description"
51
74
  id="description"
@@ -58,7 +81,7 @@ export const DescriptionField = ({ inputs, value, setValue }) => {
58
81
  };
59
82
 
60
83
  DescriptionField.propTypes = {
61
- inputs: PropTypes.array.isRequired,
84
+ inputValues: PropTypes.object.isRequired,
62
85
  value: PropTypes.string,
63
86
  setValue: PropTypes.func.isRequired,
64
87
  };
@@ -1,14 +1,19 @@
1
+ /* eslint-disable max-lines */
1
2
  import React from 'react';
2
3
  import { Provider } from 'react-redux';
3
4
  import { mount } from '@theforeman/test';
4
5
  import { fireEvent, screen, render, act } from '@testing-library/react';
6
+ import { MockedProvider } from '@apollo/client/testing';
5
7
  import * as api from 'foremanReact/redux/API';
6
8
  import { JobWizard } from '../../../JobWizard';
7
9
  import * as selectors from '../../../JobWizardSelectors';
8
10
  import {
9
- jobTemplateResponse as jobTemplate,
11
+ jobTemplateResponse,
12
+ jobTemplate,
10
13
  testSetup,
11
14
  mockApi,
15
+ jobCategories,
16
+ gqlMock,
12
17
  } from '../../../__tests__/fixtures';
13
18
  import { WIZARD_TITLES } from '../../../JobWizardConstants';
14
19
 
@@ -18,14 +23,16 @@ mockApi(api);
18
23
  jest.spyOn(selectors, 'selectEffectiveUser');
19
24
 
20
25
  selectors.selectEffectiveUser.mockImplementation(
21
- () => jobTemplate.effective_user
26
+ () => jobTemplateResponse.effective_user
22
27
  );
23
28
  describe('AdvancedFields', () => {
24
29
  it('should save data between steps for advanced fields', async () => {
25
30
  const wrapper = mount(
26
- <Provider store={store}>
27
- <JobWizard advancedValues={{}} setAdvancedValues={jest.fn()} />
28
- </Provider>
31
+ <MockedProvider mocks={gqlMock} addTypename={false}>
32
+ <Provider store={store}>
33
+ <JobWizard />
34
+ </Provider>
35
+ </MockedProvider>
29
36
  );
30
37
  // setup
31
38
  wrapper.find('.pf-c-button.pf-c-select__toggle-button').simulate('click');
@@ -78,9 +85,11 @@ describe('AdvancedFields', () => {
78
85
  });
79
86
  it('fill template fields', async () => {
80
87
  render(
81
- <Provider store={store}>
82
- <JobWizard />
83
- </Provider>
88
+ <MockedProvider mocks={gqlMock} addTypename={false}>
89
+ <Provider store={store}>
90
+ <JobWizard />
91
+ </Provider>
92
+ </MockedProvider>
84
93
  );
85
94
  await act(async () => {
86
95
  fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
@@ -91,7 +100,10 @@ describe('AdvancedFields', () => {
91
100
  const textField = screen.getByLabelText('adv plain hidden', {
92
101
  selector: 'textarea',
93
102
  });
94
- const selectField = screen.getByText('option 1');
103
+ const selectField = screen.getByLabelText('adv plain select toggle');
104
+ const resourceSelectField = screen.getByLabelText(
105
+ 'adv resource select toggle'
106
+ );
95
107
  const searchField = screen.getByPlaceholderText('Filter...');
96
108
  const dateField = screen.getByLabelText('adv date', {
97
109
  selector: 'input',
@@ -101,6 +113,10 @@ describe('AdvancedFields', () => {
101
113
  await act(async () => {
102
114
  await fireEvent.click(screen.getByText('option 2'));
103
115
  fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
116
+
117
+ fireEvent.click(resourceSelectField);
118
+ await fireEvent.click(screen.getByText('resource2'));
119
+
104
120
  await fireEvent.change(textField, {
105
121
  target: { value: textValue },
106
122
  });
@@ -134,12 +150,16 @@ describe('AdvancedFields', () => {
134
150
  expect(dateField.value).toBe(dateValue);
135
151
  expect(screen.queryAllByText('option 1')).toHaveLength(0);
136
152
  expect(screen.queryAllByText('option 2')).toHaveLength(1);
153
+ expect(screen.queryAllByDisplayValue('resource1')).toHaveLength(0);
154
+ expect(screen.queryAllByDisplayValue('resource2')).toHaveLength(1);
137
155
  });
138
156
  it('fill defaults into fields', async () => {
139
157
  render(
140
- <Provider store={store}>
141
- <JobWizard />
142
- </Provider>
158
+ <MockedProvider mocks={gqlMock} addTypename={false}>
159
+ <Provider store={store}>
160
+ <JobWizard />
161
+ </Provider>
162
+ </MockedProvider>
143
163
  );
144
164
  await act(async () => {
145
165
  fireEvent.click(screen.getByText('Advanced Fields'));
@@ -155,5 +175,189 @@ describe('AdvancedFields', () => {
155
175
  selector: 'input',
156
176
  }).value
157
177
  ).toBe('2');
178
+
179
+ expect(
180
+ screen.getByLabelText('description preview', {
181
+ selector: 'input',
182
+ }).value
183
+ ).toBe(
184
+ 'template1 with inputs adv plain hidden="Default val" adv plain select="" adv resource select="" adv search="" adv date="" plain hidden="Default val"'
185
+ );
186
+ });
187
+ it('DescriptionField', async () => {
188
+ render(
189
+ <Provider store={store}>
190
+ <JobWizard />
191
+ </Provider>
192
+ );
193
+ await act(async () => {
194
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
195
+ });
196
+
197
+ const textField = screen.getByLabelText('adv plain hidden', {
198
+ selector: 'textarea',
199
+ });
200
+ await act(async () => {
201
+ await fireEvent.change(textField, {
202
+ target: { value: 'test command' },
203
+ });
204
+ });
205
+ const descriptionValue = 'Run %{adv plain hidden} %{wrong command name}';
206
+
207
+ await act(async () => {
208
+ fireEvent.click(screen.getByText('Edit job description template'));
209
+ });
210
+
211
+ const editText = screen.getByLabelText('description edit', {
212
+ selector: 'input',
213
+ });
214
+ await fireEvent.change(editText, {
215
+ target: { value: descriptionValue },
216
+ });
217
+ await act(async () => {
218
+ fireEvent.click(screen.getByText('Preview job description'));
219
+ });
220
+ expect(
221
+ screen.getByLabelText('description preview', {
222
+ selector: 'input',
223
+ }).value
224
+ ).toBe('Run test command %{wrong command name}');
225
+ });
226
+
227
+ it('DescriptionField with no inputs', async () => {
228
+ jest.spyOn(api, 'get');
229
+
230
+ jest.spyOn(selectors, 'selectTemplateInputs');
231
+ jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
232
+ selectors.selectTemplateInputs.mockImplementation(() => []);
233
+ selectors.selectAdvancedTemplateInputs.mockImplementation(() => []);
234
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
235
+ if (action.key === 'JOB_CATEGORIES') {
236
+ handleSuccess &&
237
+ handleSuccess({ data: { job_categories: jobCategories } });
238
+ } else if (action.key === 'JOB_TEMPLATE') {
239
+ handleSuccess &&
240
+ handleSuccess({
241
+ data: {
242
+ ...jobTemplateResponse,
243
+ advanced_template_inputs: [],
244
+ template_inputs: [],
245
+ },
246
+ });
247
+ } else if (action.key === 'JOB_TEMPLATES') {
248
+ handleSuccess &&
249
+ handleSuccess({
250
+ data: { results: [jobTemplate] },
251
+ });
252
+ }
253
+ return { type: 'get', ...action };
254
+ });
255
+ render(
256
+ <Provider store={store}>
257
+ <JobWizard />
258
+ </Provider>
259
+ );
260
+ await act(async () => {
261
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
262
+ });
263
+ expect(
264
+ screen.getByLabelText('description preview', {
265
+ selector: 'input',
266
+ }).value
267
+ ).toBe('template1');
268
+ });
269
+
270
+ it('DescriptionField with description_format', async () => {
271
+ jest.spyOn(api, 'get');
272
+ jest.spyOn(selectors, 'selectTemplateInputs');
273
+ selectors.selectTemplateInputs.mockImplementation(() => [
274
+ {
275
+ name: 'command',
276
+ required: true,
277
+ input_type: 'user',
278
+ description: 'some Description',
279
+ advanced: true,
280
+ value_type: 'plain',
281
+ resource_type: 'ansible_roles',
282
+ default: 'Default val',
283
+ hidden_value: true,
284
+ },
285
+ ]);
286
+ api.get.mockImplementation(({ handleSuccess, ...action }) => {
287
+ if (action.key === 'JOB_CATEGORIES') {
288
+ handleSuccess &&
289
+ handleSuccess({ data: { job_categories: jobCategories } });
290
+ } else if (action.key === 'JOB_TEMPLATE') {
291
+ handleSuccess &&
292
+ handleSuccess({
293
+ data: {
294
+ ...jobTemplateResponse,
295
+ job_template: {
296
+ ...jobTemplateResponse.jobTemplate,
297
+ description_format: 'Run %{command}',
298
+ },
299
+
300
+ template_inputs: [
301
+ {
302
+ name: 'command',
303
+ required: true,
304
+ input_type: 'user',
305
+ description: 'some Description',
306
+ advanced: true,
307
+ value_type: 'plain',
308
+ resource_type: 'ansible_roles',
309
+ default: 'Default val',
310
+ hidden_value: true,
311
+ },
312
+ ],
313
+ },
314
+ });
315
+ } else if (action.key === 'JOB_TEMPLATES') {
316
+ handleSuccess &&
317
+ handleSuccess({
318
+ data: { results: [jobTemplate] },
319
+ });
320
+ }
321
+ return { type: 'get', ...action };
322
+ });
323
+ render(
324
+ <Provider store={store}>
325
+ <JobWizard />
326
+ </Provider>
327
+ );
328
+ await act(async () => {
329
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
330
+ });
331
+ expect(
332
+ screen.getByLabelText('description preview', {
333
+ selector: 'input',
334
+ }).value
335
+ ).toBe('Run Default val');
336
+ });
337
+
338
+ it('search resources action', async () => {
339
+ jest.useFakeTimers();
340
+ mockApi(api);
341
+ const newStore = testSetup(selectors, api);
342
+ render(
343
+ <Provider store={newStore}>
344
+ <JobWizard />
345
+ </Provider>
346
+ );
347
+ await act(async () => {
348
+ fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
349
+ });
350
+ const resourceSelectField = screen.getByLabelText(
351
+ 'adv resource select typeahead input'
352
+ );
353
+
354
+ await act(async () => {
355
+ await fireEvent.change(resourceSelectField, {
356
+ target: { value: 'some search' },
357
+ });
358
+
359
+ await jest.runAllTimers();
360
+ });
361
+ expect(newStore.getActions()).toMatchSnapshot('resource search');
158
362
  });
159
363
  });