foreman_remote_execution 17.2.0 → 18.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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/foreign_input_sets_controller.rb +4 -0
  3. data/app/controllers/job_invocations_controller.rb +16 -1
  4. data/config/routes.rb +2 -1
  5. data/db/seeds.d/60-ssh_proxy_feature.rb +2 -2
  6. data/db/seeds.d/90-bookmarks.rb +1 -1
  7. data/db/seeds.d/95-mail_notifications.rb +1 -1
  8. data/lib/foreman_remote_execution/plugin.rb +2 -2
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/job_invocations_controller_test.rb +84 -0
  11. data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -2
  12. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +8 -1
  13. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +6 -78
  14. data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +113 -7
  15. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +4 -39
  16. data/webpack/JobInvocationDetail/__tests__/areAllHostsTerminal.test.js +40 -0
  17. data/webpack/JobInvocationDetail/__tests__/fixtures.js +0 -8
  18. data/webpack/JobWizard/Footer.js +16 -13
  19. data/webpack/JobWizard/__tests__/Footer.test.js +64 -0
  20. data/webpack/JobWizard/__tests__/integration.test.js +98 -53
  21. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +191 -212
  22. data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +73 -3
  23. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +69 -36
  24. metadata +5 -5
  25. data/webpack/JobWizard/__tests__/__snapshots__/integration.test.js.snap +0 -51
  26. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +0 -31
@@ -0,0 +1,40 @@
1
+ import { areAllHostsTerminal } from '../JobInvocationConstants';
2
+
3
+ describe('areAllHostsTerminal', () => {
4
+ it('returns false for an empty results array', () => {
5
+ expect(areAllHostsTerminal([])).toBe(false);
6
+ });
7
+
8
+ it('returns false for undefined results', () => {
9
+ expect(areAllHostsTerminal(undefined)).toBe(false);
10
+ });
11
+
12
+ it('returns true when every host is in a terminal state', () => {
13
+ expect(
14
+ areAllHostsTerminal([
15
+ { id: 1, job_status: 'success' },
16
+ { id: 2, job_status: 'error' },
17
+ { id: 3, job_status: 'cancelled' },
18
+ ])
19
+ ).toBe(true);
20
+ });
21
+
22
+ it('returns false when at least one host is non-terminal', () => {
23
+ expect(
24
+ areAllHostsTerminal([
25
+ { id: 1, job_status: 'success' },
26
+ { id: 2, job_status: 'running' },
27
+ ])
28
+ ).toBe(false);
29
+ });
30
+
31
+ it('returns false when every host is non-terminal', () => {
32
+ expect(
33
+ areAllHostsTerminal([
34
+ { id: 1, job_status: 'running' },
35
+ { id: 2, job_status: 'planned' },
36
+ { id: 3, job_status: 'N/A' },
37
+ ])
38
+ ).toBe(false);
39
+ });
40
+ });
@@ -140,14 +140,6 @@ export const mockPermissionsData = {
140
140
  edit_recurring_logics: true,
141
141
  };
142
142
 
143
- export const mockReportTemplatesResponse = {
144
- results: [{ id: '12', name: 'Job - Invocation Report' }],
145
- };
146
-
147
- export const mockReportTemplateInputsResponse = {
148
- results: [{ id: '34', name: 'job_id' }],
149
- };
150
-
151
143
  const templateInvocationID = 157;
152
144
 
153
145
  export const jobInvocationOutput = [
@@ -22,6 +22,19 @@ export const Footer = ({ canSubmit, onSave }) => {
22
22
  activeStep && activeStep.enableNext !== undefined
23
23
  ? activeStep.enableNext
24
24
  : true;
25
+ const isReviewStep = activeStep.name === WIZARD_TITLES.review;
26
+ const canSkipToReview = canSubmit && !isReviewStep;
27
+ const skipToReviewTooltip = () => {
28
+ if (canSkipToReview) {
29
+ return __('Skip to review step');
30
+ }
31
+ if (isReviewStep) {
32
+ return __('Already on the review step');
33
+ }
34
+ return __(
35
+ 'Fill all required fields in all the steps to start the job'
36
+ );
37
+ };
25
38
 
26
39
  return (
27
40
  <>
@@ -32,9 +45,7 @@ export const Footer = ({ canSubmit, onSave }) => {
32
45
  onClick={onNext}
33
46
  isDisabled={!isValid || isSubmitting}
34
47
  >
35
- {activeStep.name === WIZARD_TITLES.review
36
- ? __('Submit')
37
- : __('Next')}
48
+ {isReviewStep ? __('Submit') : __('Next')}
38
49
  </Button>
39
50
  <Button
40
51
  ouiaId="back-footer"
@@ -68,22 +79,14 @@ export const Footer = ({ canSubmit, onSave }) => {
68
79
  </Button>
69
80
  </Tooltip>
70
81
  <Tooltip
71
- content={
72
- <div>
73
- {canSubmit
74
- ? __('Skip to review step')
75
- : __(
76
- 'Fill all required fields in all the steps to start the job'
77
- )}
78
- </div>
79
- }
82
+ content={<div>{skipToReviewTooltip()}</div>}
80
83
  triggerRef={tooltipSkipTo}
81
84
  >
82
85
  <Button
83
86
  ouiaId="skip-to-review-footer"
84
87
  variant="tertiary"
85
88
  onClick={() => goToStepByName(WIZARD_TITLES.review)}
86
- isAriaDisabled={!canSubmit}
89
+ isAriaDisabled={!canSkipToReview}
87
90
  isDisabled={isSubmitting}
88
91
  ref={tooltipSkipTo}
89
92
  >
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { render, fireEvent, screen, act } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import { Wizard } from '@patternfly/react-core/deprecated';
6
+ import configureMockStore from 'redux-mock-store';
7
+ import { Footer } from '../Footer';
8
+ import { WIZARD_TITLES } from '../JobWizardConstants';
9
+
10
+ const store = configureMockStore([])({ API: {} });
11
+
12
+ const renderFooterWizard = ({ canSubmit = true, startAtStep = 1 } = {}) =>
13
+ render(
14
+ <Provider store={store}>
15
+ <Wizard
16
+ steps={[
17
+ {
18
+ name: WIZARD_TITLES.categoryAndTemplate,
19
+ component: <div>category step</div>,
20
+ },
21
+ {
22
+ name: WIZARD_TITLES.review,
23
+ component: <div>review step</div>,
24
+ },
25
+ ]}
26
+ startAtStep={startAtStep}
27
+ footer={<Footer canSubmit={canSubmit} onSave={jest.fn()} />}
28
+ />
29
+ </Provider>
30
+ );
31
+
32
+ describe('Job wizard footer', () => {
33
+ it('enables Skip to review when required fields are filled', () => {
34
+ renderFooterWizard({ canSubmit: true });
35
+
36
+ expect(screen.getByText('Skip to review')).not.toHaveAttribute(
37
+ 'aria-disabled',
38
+ 'true'
39
+ );
40
+ });
41
+
42
+ it('disables Skip to review when required fields are missing', () => {
43
+ renderFooterWizard({ canSubmit: false });
44
+
45
+ expect(screen.getByText('Skip to review')).toHaveAttribute(
46
+ 'aria-disabled',
47
+ 'true'
48
+ );
49
+ });
50
+
51
+ it('disables Skip to review on the review step', async () => {
52
+ renderFooterWizard({ canSubmit: true });
53
+
54
+ await act(async () => {
55
+ fireEvent.click(screen.getByText('Skip to review'));
56
+ });
57
+
58
+ expect(screen.getByText('Submit')).toBeInTheDocument();
59
+ expect(screen.getByText('Skip to review')).toHaveAttribute(
60
+ 'aria-disabled',
61
+ 'true'
62
+ );
63
+ });
64
+ });
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import { Provider } from 'react-redux';
3
- import { mount } from '@theforeman/test';
4
- import { render, fireEvent, screen, act } from '@testing-library/react';
3
+ import { render, screen, within, waitFor } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import '@testing-library/jest-dom';
5
6
  import { MockedProvider } from '@apollo/client/testing';
6
7
  import * as api from 'foremanReact/redux/API';
7
8
  import { JobWizard } from '../JobWizard';
@@ -17,17 +18,55 @@ import {
17
18
 
18
19
  const store = testSetup(selectors, api);
19
20
 
21
+ const renderJobWizard = ({ withGql = false } = {}) => {
22
+ const wizard = (
23
+ <Provider store={store}>
24
+ <JobWizard />
25
+ </Provider>
26
+ );
27
+
28
+ if (withGql) {
29
+ return render(
30
+ <MockedProvider mocks={gqlMock} addTypename={false}>
31
+ {wizard}
32
+ </MockedProvider>
33
+ );
34
+ }
35
+
36
+ return render(wizard);
37
+ };
38
+
39
+ const getWizardNavigation = () =>
40
+ screen.getByRole('navigation', { name: 'Run Job steps' });
41
+
42
+ const getWizardStep = stepName =>
43
+ within(getWizardNavigation()).getByRole('button', { name: stepName });
44
+
45
+ const getDisabledWizardSteps = () =>
46
+ within(getWizardNavigation())
47
+ .getAllByRole('button')
48
+ .filter(button => button.getAttribute('aria-disabled') === 'true');
49
+
50
+ const expectDispatchedGet = expected => {
51
+ expect(store.getActions()).toEqual(
52
+ expect.arrayContaining([expect.objectContaining({ type: 'get', ...expected })])
53
+ );
54
+ };
55
+
20
56
  describe('Job wizard fill', () => {
21
57
  beforeEach(() => {
58
+ store.clearActions();
22
59
  jest.spyOn(selectors, 'selectRouterSearch');
23
60
  selectors.selectRouterSearch.mockImplementation(() => ({
24
61
  'host_ids[]': ['105', '37'],
25
62
  }));
26
63
  });
64
+
27
65
  afterEach(() => {
28
66
  selectors.selectRouterSearch.mockRestore();
29
67
  });
30
- it('should select template', async () => {
68
+
69
+ it('selects a template and enables wizard steps', async () => {
31
70
  api.get.mockImplementation(({ handleSuccess, ...action }) => {
32
71
  if (action.key === 'JOB_CATEGORIES') {
33
72
  handleSuccess &&
@@ -54,74 +93,80 @@ describe('Job wizard fill', () => {
54
93
  selectors.selectJobTemplate.mockRestore();
55
94
  jest.spyOn(selectors, 'selectJobTemplate');
56
95
  selectors.selectJobTemplate.mockImplementation(() => ({}));
57
- const wrapper = mount(
58
- <Provider store={store}>
59
- <JobWizard />
60
- </Provider>
61
- );
62
- expect(
63
- wrapper.find('.pf-v5-c-wizard__nav-link.pf-m-disabled')
64
- ).toHaveLength(5);
96
+
97
+ renderJobWizard();
98
+
99
+ expect(getDisabledWizardSteps()).toHaveLength(5);
100
+ expect(getWizardStep(WIZARD_TITLES.categoryAndTemplate)).toBeInTheDocument();
101
+
102
+ expectDispatchedGet({
103
+ key: 'JOB_CATEGORIES',
104
+ url: '/ui_job_wizard/categories',
105
+ });
106
+ expectDispatchedGet({
107
+ key: 'HOST_IDS',
108
+ params: { search: 'id = 105 or id = 37' },
109
+ url: '/api/hosts',
110
+ });
111
+ expectDispatchedGet({
112
+ key: 'JOB_TEMPLATES',
113
+ });
114
+
65
115
  selectors.selectJobCategoriesStatus.mockImplementation(() => 'RESOLVED');
66
- expect(store.getActions()).toMatchSnapshot('initial');
67
116
  selectors.selectJobTemplate.mockRestore();
68
117
  jest.spyOn(selectors, 'selectJobTemplate');
69
118
  selectors.selectJobTemplate.mockImplementation(() => jobTemplate);
70
- wrapper
71
- .find('.pf-v5-c-button.pf-v5-c-select__toggle-button')
72
- .simulate('click');
73
- await act(async () => {
74
- await wrapper
75
- .find('.pf-v5-c-select__menu-item')
76
- .first()
77
- .simulate('click');
119
+
120
+ await userEvent.click(
121
+ screen.getByRole('button', { name: 'Job template toggle' })
122
+ );
123
+ await userEvent.click(screen.getByText(jobTemplate.job_template.name));
124
+
125
+ expect(store.getActions().at(-1)).toEqual(
126
+ expect.objectContaining({
127
+ key: 'JOB_TEMPLATE',
128
+ type: 'get',
129
+ url: '/ui_job_wizard/template/178',
130
+ })
131
+ );
132
+
133
+ await waitFor(() => {
134
+ expect(getDisabledWizardSteps()).toHaveLength(0);
78
135
  });
79
- expect(store.getActions().slice(-1)).toMatchSnapshot('select template');
80
- wrapper.update();
81
- expect(
82
- wrapper.find('.pf-v5-c-wizard__nav-link.pf-m-disabled')
83
- ).toHaveLength(0);
84
136
  });
85
137
 
86
- it('have all steps', async () => {
138
+ it('renders all wizard steps and navigates between them', async () => {
87
139
  selectors.selectJobCategoriesStatus.mockImplementation(() => null);
88
140
  selectors.selectJobTemplates.mockRestore();
89
141
  selectors.selectJobCategories.mockRestore();
90
142
  mockApi(api);
91
143
 
92
- render(
93
- <MockedProvider mocks={gqlMock} addTypename={false}>
94
- <Provider store={store}>
95
- <JobWizard />
96
- </Provider>
97
- </MockedProvider>
98
- );
144
+ renderJobWizard({ withGql: true });
145
+
99
146
  const steps = [
100
147
  WIZARD_TITLES.hostsAndInputs,
101
148
  WIZARD_TITLES.categoryAndTemplate,
102
149
  WIZARD_TITLES.advanced,
103
150
  WIZARD_TITLES.review,
104
151
  ];
105
- // eslint-disable-next-line no-unused-vars
106
- for await (const step of steps) {
107
- const stepSelector = screen.getByText(step);
108
- const stepTitle = screen.getAllByText(step);
109
- expect(stepTitle).toHaveLength(1);
110
- await act(async () => {
111
- await fireEvent.click(stepSelector);
112
- });
113
- const stepTitles = screen.getAllByText(step);
114
- expect(stepTitles).toHaveLength(3);
152
+
153
+ for (const step of steps) {
154
+ expect(screen.getAllByText(step)).toHaveLength(1);
155
+
156
+ await userEvent.click(getWizardStep(step));
157
+
158
+ expect(screen.getAllByText(step)).toHaveLength(3);
115
159
  }
116
- const step = WIZARD_TITLES.typeOfExecution;
117
- const stepTitle = screen.getAllByText(step);
118
- expect(stepTitle).toHaveLength(1);
119
- expect(screen.queryAllByText('Select the type of execution')).toHaveLength(
120
- 0
121
- );
122
- await act(async () => {
123
- await fireEvent.click(stepTitle[0]);
124
- });
125
- expect(screen.getAllByText('Select the type of execution')).toHaveLength(1);
160
+
161
+ expect(screen.getAllByText(WIZARD_TITLES.typeOfExecution)).toHaveLength(1);
162
+ expect(
163
+ screen.queryByText('Select the type of execution')
164
+ ).not.toBeInTheDocument();
165
+
166
+ await userEvent.click(getWizardStep(WIZARD_TITLES.typeOfExecution));
167
+
168
+ expect(
169
+ screen.getByText('Select the type of execution')
170
+ ).toBeInTheDocument();
126
171
  });
127
172
  });