foreman_remote_execution 4.8.0 → 5.0.0

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