foreman_remote_execution 17.2.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2273dcbf9a1bd18e838f9312d802e752f181723049c543895e2ba9bbe47309b
4
- data.tar.gz: 4be8496d42f9abcd54116d8e0dc09669d0e8c67674341e9672601450dc77a57a
3
+ metadata.gz: 4b301e962ec24666ada0b4a297277232ee94dc74a36b86875f4ce2230659c254
4
+ data.tar.gz: 923c5679450846add2cfba3a6741654e7bfd06f23e9e33b8ad5f19dfd2b6c217
5
5
  SHA512:
6
- metadata.gz: 0656e10eb461a75898d1359ab6c03f5e10a6bbac18e03bac8ca804afd06f1050dc6bb6fb8a05ca0ced9a725dc4cd6b6202e164fca9a0ecf74179708aaa05d955
7
- data.tar.gz: 76bfdccd452647bbdd56866eb6175c1efbb1efde242ef416a23902e429b1be6099e3a1f00ae9ddd1f2e956a21cb28baf099e31934ab501e112532934fdc7eadd
6
+ metadata.gz: 561489f3cee2bf28837084baa1fd3fb8b3a68b8acd632c0337f1b3204dc176acafefb85ecc15d058c464038ec38413a56899ce4ebd2d723596571622cb9c130f
7
+ data.tar.gz: '0938ab65b49a36d63fb709d905322559305ebd691f83bcf1aee8d81b9cb6cffd83db2fe50381295b7f589d37beddc457a7ee0aef3b8c02819f12008c9f461461'
@@ -76,6 +76,10 @@ module Api
76
76
  def resource_class
77
77
  ForeignInputSet
78
78
  end
79
+
80
+ def allowed_nested_id
81
+ %w(template_id)
82
+ end
79
83
  end
80
84
  end
81
85
  end
@@ -1,5 +1,5 @@
1
1
  f = Feature.where(:name => 'SSH').first_or_create
2
- raise "Unable to create proxy feature: #{format_errors f}" if f.nil? || f.errors.any?
2
+ raise "Unable to create proxy feature: #{SeedHelper.format_errors(f)}" if f.nil? || f.errors.any?
3
3
 
4
4
  f = Feature.where(:name => 'Script').first_or_create
5
- raise "Unable to create proxy feature: #{format_errors f}" if f.nil? || f.errors.any?
5
+ raise "Unable to create proxy feature: #{SeedHelper.format_errors(f)}" if f.nil? || f.errors.any?
@@ -15,6 +15,6 @@ Bookmark.without_auditing do
15
15
  b = Bookmark.where(:name => input[:name], :controller => input[:controller]).first || Bookmark.new
16
16
  b.attributes = attributes
17
17
  b.save
18
- raise "Unable to create bookmark: #{format_errors b}" if b.errors.any?
18
+ raise "Unable to create bookmark: #{SeedHelper.format_errors(b)}" if b.errors.any?
19
19
  end
20
20
  end
@@ -18,7 +18,7 @@ notifications.each do |notification|
18
18
  created_notification = RexMailNotification.create(notification)
19
19
  if created_notification.nil? || created_notification.errors.any?
20
20
  raise ::Foreman::Exception.new(N_("Unable to create mail notification: %s"),
21
- format_errors(created_notification))
21
+ SeedHelper.format_errors(created_notification))
22
22
  end
23
23
  end
24
24
  end
@@ -1,5 +1,5 @@
1
1
  Foreman::Plugin.register :foreman_remote_execution do
2
- requires_foreman '>= 5.0'
2
+ requires_foreman '>= 5.1'
3
3
  register_global_js_file 'global'
4
4
  register_gettext
5
5
 
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '17.2.1'.freeze
2
+ VERSION = '18.0.0'.freeze
3
3
  end
@@ -55,6 +55,16 @@ export const STATUS_TITLES = {
55
55
  NOT_STARTED: { id: 'N/A', title: __('Scheduled') },
56
56
  };
57
57
 
58
+ export const TERMINAL_HOST_STATUSES = new Set([
59
+ 'success',
60
+ 'error',
61
+ 'cancelled',
62
+ ]);
63
+
64
+ export const areAllHostsTerminal = results =>
65
+ results?.length > 0 &&
66
+ results.every(host => TERMINAL_HOST_STATUSES.has(host.job_status));
67
+
58
68
  export const DATE_OPTIONS = {
59
69
  day: 'numeric',
60
70
  month: 'short',
@@ -42,6 +42,7 @@ import Columns, {
42
42
  STATUS_UPPERCASE,
43
43
  AWAITING_STATUS_FILTER,
44
44
  AUTO_REFRESH_INTERVAL_MS,
45
+ areAllHostsTerminal,
45
46
  } from './JobInvocationConstants';
46
47
  import { TemplateInvocation } from './TemplateInvocation';
47
48
  import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
@@ -196,7 +197,13 @@ const JobInvocationHostTable = ({
196
197
  if (thisRequest !== requestIdRef.current) return;
197
198
  if (!mountedRef.current) return;
198
199
  updateHostsState(data);
199
- if (!jobFinishedRef.current) {
200
+ const hasActiveFilter = !!(
201
+ currentPollParams.current.search ||
202
+ currentPollParams.current.awaiting
203
+ );
204
+ const pageAllTerminal =
205
+ !hasActiveFilter && areAllHostsTerminal(data.data?.results);
206
+ if (!jobFinishedRef.current && !pageAllTerminal) {
200
207
  pollTimeoutId.current = setTimeout(
201
208
  () => makeApiCall(currentPollParams.current),
202
209
  AUTO_REFRESH_INTERVAL_MS
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import React from 'react';
2
3
  import { render, act } from '@testing-library/react';
3
4
  import '@testing-library/jest-dom';
@@ -44,13 +45,28 @@ const hostsResponse = {
44
45
  operatingsystem_name: 'RHEL 9',
45
46
  hostgroup_id: 1,
46
47
  hostgroup_name: 'default',
47
- job_status: 'success',
48
+ job_status: 'running',
48
49
  smart_proxy_id: 1,
49
50
  smart_proxy_name: 'proxy1',
50
51
  },
51
52
  ],
52
53
  };
53
54
 
55
+ const terminalHostsResponse = {
56
+ ...hostsResponse,
57
+ results: [{ ...hostsResponse.results[0], job_status: 'success' }],
58
+ };
59
+
60
+ const mixedHostsResponse = {
61
+ ...hostsResponse,
62
+ total: 2,
63
+ subtotal: 2,
64
+ results: [
65
+ { ...hostsResponse.results[0], id: 1, job_status: 'success' },
66
+ { ...hostsResponse.results[0], id: 2, job_status: 'running' },
67
+ ],
68
+ };
69
+
54
70
  let apiGetSpy;
55
71
  let hostsCalls;
56
72
  let pendingCallbacks;
@@ -66,9 +82,9 @@ const flushPendingCallbacks = () => {
66
82
  batch.forEach(cb => cb());
67
83
  };
68
84
 
69
- const renderTable = (props = {}) => {
85
+ const renderTable = (props = {}, historyEntries = ['/']) => {
70
86
  const store = createStore();
71
- const history = createMemoryHistory();
87
+ const history = createMemoryHistory({ initialEntries: historyEntries });
72
88
  const Wrapper = createForemanContextWrapper();
73
89
 
74
90
  const defaultProps = {
@@ -93,16 +109,14 @@ const renderTable = (props = {}) => {
93
109
  return { ...result, store, history };
94
110
  };
95
111
 
96
- const setupSuccessMock = () => {
112
+ const setupSuccessMock = (response = hostsResponse) => {
97
113
  apiGetSpy = jest
98
114
  .spyOn(APIActions, 'get')
99
115
  .mockImplementation(opts => dispatch => {
100
116
  if (opts.key === JOB_INVOCATION_HOSTS) {
101
117
  hostsCalls.push(opts);
102
118
  if (opts.handleSuccess) {
103
- pendingCallbacks.push(() =>
104
- opts.handleSuccess({ data: hostsResponse })
105
- );
119
+ pendingCallbacks.push(() => opts.handleSuccess({ data: response }));
106
120
  }
107
121
  }
108
122
  });
@@ -311,6 +325,98 @@ describe('JobInvocationHostTable polling', () => {
311
325
  expect(hostsCalls.length).toBeGreaterThan(callsAfterFilterChange);
312
326
  });
313
327
 
328
+ it('stops polling when all hosts on the page are terminal', () => {
329
+ apiGetSpy.mockRestore();
330
+ setupSuccessMock(terminalHostsResponse);
331
+
332
+ renderTable();
333
+
334
+ expect(hostsCalls).toHaveLength(1);
335
+
336
+ act(() => {
337
+ flushPendingCallbacks();
338
+ });
339
+
340
+ act(() => {
341
+ jest.advanceTimersByTime(10000);
342
+ });
343
+
344
+ act(() => {
345
+ flushPendingCallbacks();
346
+ });
347
+
348
+ expect(hostsCalls).toHaveLength(1);
349
+ });
350
+
351
+ it('continues polling when at least one host on the page is non-terminal', () => {
352
+ apiGetSpy.mockRestore();
353
+ setupSuccessMock(mixedHostsResponse);
354
+
355
+ renderTable();
356
+
357
+ expect(hostsCalls).toHaveLength(1);
358
+
359
+ act(() => {
360
+ flushPendingCallbacks();
361
+ });
362
+
363
+ act(() => {
364
+ jest.advanceTimersByTime(5000);
365
+ });
366
+
367
+ act(() => {
368
+ flushPendingCallbacks();
369
+ });
370
+
371
+ expect(hostsCalls).toHaveLength(2);
372
+ });
373
+
374
+ it('continues polling when all hosts on the page are terminal but a status filter is active', () => {
375
+ apiGetSpy.mockRestore();
376
+ setupSuccessMock(terminalHostsResponse);
377
+
378
+ renderTable({ initialFilter: 'success' });
379
+
380
+ expect(hostsCalls).toHaveLength(1);
381
+
382
+ act(() => {
383
+ flushPendingCallbacks();
384
+ });
385
+
386
+ act(() => {
387
+ jest.advanceTimersByTime(5000);
388
+ });
389
+
390
+ act(() => {
391
+ flushPendingCallbacks();
392
+ });
393
+
394
+ expect(hostsCalls).toHaveLength(2);
395
+ });
396
+
397
+ it('continues polling when all hosts on the page are terminal but a search query is active', () => {
398
+ apiGetSpy.mockRestore();
399
+ setupSuccessMock(terminalHostsResponse);
400
+
401
+ renderTable({}, ['/?search=host1']);
402
+
403
+ expect(hostsCalls).toHaveLength(1);
404
+
405
+ act(() => {
406
+ flushPendingCallbacks();
407
+ });
408
+
409
+ act(() => {
410
+ jest.advanceTimersByTime(5000);
411
+ });
412
+
413
+ act(() => {
414
+ flushPendingCallbacks();
415
+ });
416
+
417
+ expect(hostsCalls).toHaveLength(2);
418
+ });
419
+
314
420
  it('sends include_permissions only on the first request', () => {
315
421
  renderTable();
316
422
 
@@ -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
+ });
@@ -1,8 +1,9 @@
1
1
  /* eslint-disable max-lines */
2
2
  import React from 'react';
3
3
  import { Provider } from 'react-redux';
4
- import { mount } from '@theforeman/test';
5
- import { fireEvent, screen, render, act } from '@testing-library/react';
4
+ import { screen, render, act, fireEvent } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+ import '@testing-library/jest-dom';
6
7
  import { MockedProvider } from '@apollo/client/testing';
7
8
  import * as api from 'foremanReact/redux/API';
8
9
  import { JobWizard } from '../../../JobWizard';
@@ -20,11 +21,55 @@ import { WIZARD_TITLES } from '../../../JobWizardConstants';
20
21
  const lodash = require('lodash');
21
22
 
22
23
  lodash.debounce = fn => fn;
24
+
23
25
  const store = testSetup(selectors, api);
24
26
  mockApi(api);
25
27
 
26
28
  jest.useFakeTimers();
27
29
 
30
+ const clickOptions = { skipHover: true };
31
+
32
+ const wizardStepButton = title =>
33
+ screen.getByText(title, { selector: 'button' });
34
+
35
+ const clickWizardStep = async title => {
36
+ await act(async () => {
37
+ fireEvent.click(wizardStepButton(title));
38
+ jest.advanceTimersByTime(1000);
39
+ });
40
+ };
41
+
42
+ const typeIntoField = async (field, value) => {
43
+ await act(async () => {
44
+ await userEvent.click(field, {}, clickOptions);
45
+ await userEvent.clear(field);
46
+ userEvent.paste(field, value);
47
+ jest.advanceTimersByTime(1000);
48
+ });
49
+ };
50
+
51
+ const renderJobWizard = ({ customStore = store, withGql = true } = {}) => {
52
+ const wizard = (
53
+ <Provider store={customStore}>
54
+ <JobWizard />
55
+ </Provider>
56
+ );
57
+
58
+ if (withGql) {
59
+ return render(
60
+ <MockedProvider mocks={gqlMock} addTypename={false}>
61
+ {wizard}
62
+ </MockedProvider>
63
+ );
64
+ }
65
+
66
+ return render(wizard);
67
+ };
68
+
69
+ const goToAdvancedStep = async () => {
70
+ await clickWizardStep(WIZARD_TITLES.advanced);
71
+ };
72
+
28
73
  describe('AdvancedFields', () => {
29
74
  beforeEach(() => {
30
75
  jest.spyOn(selectors, 'selectRouterSearch');
@@ -32,87 +77,45 @@ describe('AdvancedFields', () => {
32
77
  'host_ids[]': ['105', '37'],
33
78
  }));
34
79
  });
80
+
35
81
  afterEach(() => {
36
82
  selectors.selectRouterSearch.mockRestore();
37
83
  });
38
- it('should save data between steps for advanced fields', async () => {
39
- const wrapper = mount(
40
- <MockedProvider mocks={gqlMock} addTypename={false}>
41
- <Provider store={store}>
42
- <JobWizard />
43
- </Provider>
44
- </MockedProvider>
45
- );
46
- // setup
47
- wrapper
48
- .find('.pf-v5-c-button.pf-v5-c-select__toggle-button')
49
- .simulate('click');
50
- wrapper
51
- .find('.pf-v5-c-select__menu-item')
52
- .first()
53
- .simulate('click');
54
-
55
- // test
56
- expect(
57
- wrapper.find('.pf-v5-c-wizard__nav-link.pf-m-disabled')
58
- ).toHaveLength(0);
59
- wrapper
60
- .find('.pf-v5-c-wizard__nav-link')
61
- .at(2)
62
- .simulate('click'); // Advanced step
63
84
 
64
- await act(async () => {
65
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
85
+ it('preserves advanced field values when navigating between wizard steps', async () => {
86
+ renderJobWizard();
87
+ await goToAdvancedStep();
88
+
89
+ const effectiveUserInput = screen.getByLabelText('effective user', {
90
+ selector: 'input',
91
+ });
92
+ const advancedTemplateInput = screen.getByLabelText('adv plain hidden', {
93
+ selector: 'textarea',
66
94
  });
67
- const effectiveUserInput = () => wrapper.find('input#effective-user');
68
- const advancedTemplateInput = () =>
69
- wrapper.find('.pf-v5-c-form__group-control textarea');
70
- const effectiveUesrValue = 'effective user new value';
95
+ const effectiveUserValue = 'effective user new value';
71
96
  const advancedTemplateInputValue = 'advanced input new value';
72
- effectiveUserInput().getDOMNode().value = effectiveUesrValue;
73
-
74
- effectiveUserInput().simulate('change');
75
- wrapper.update();
76
- advancedTemplateInput().getDOMNode().value = advancedTemplateInputValue;
77
- advancedTemplateInput().simulate('change');
78
- expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
79
- expect(advancedTemplateInput().prop('value')).toEqual(
80
- advancedTemplateInputValue
81
- );
82
97
 
83
- wrapper
84
- .find('.pf-v5-c-wizard__nav-link')
85
- .at(1)
86
- .simulate('click');
98
+ await typeIntoField(effectiveUserInput, effectiveUserValue);
99
+ await typeIntoField(advancedTemplateInput, advancedTemplateInputValue);
87
100
 
88
- expect(
89
- wrapper.find('.pf-v5-c-wizard__nav-link.pf-m-current').text()
90
- ).toEqual('Target hosts and inputs');
91
- wrapper
92
- .find('.pf-v5-c-wizard__nav-link')
93
- .at(2)
94
- .simulate('click'); // Advanced step
101
+ expect(effectiveUserInput).toHaveValue(effectiveUserValue);
102
+ expect(advancedTemplateInput).toHaveValue(advancedTemplateInputValue);
95
103
 
96
- await act(async () => {
97
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
98
- });
99
- expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
100
- expect(advancedTemplateInput().prop('value')).toEqual(
101
- advancedTemplateInputValue
104
+ await clickWizardStep(WIZARD_TITLES.hostsAndInputs);
105
+ expect(wizardStepButton(WIZARD_TITLES.hostsAndInputs)).toHaveAttribute(
106
+ 'aria-current',
107
+ 'step'
102
108
  );
109
+
110
+ await goToAdvancedStep();
111
+
112
+ expect(effectiveUserInput).toHaveValue(effectiveUserValue);
113
+ expect(advancedTemplateInput).toHaveValue(advancedTemplateInputValue);
103
114
  });
104
- it('fill template fields', async () => {
105
- render(
106
- <MockedProvider mocks={gqlMock} addTypename={false}>
107
- <Provider store={store}>
108
- <JobWizard />
109
- </Provider>
110
- </MockedProvider>
111
- );
112
- await act(async () => {
113
- fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
114
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
115
- });
115
+
116
+ it('preserves template field values when navigating between wizard steps', async () => {
117
+ renderJobWizard();
118
+ await goToAdvancedStep();
116
119
 
117
120
  const searchValue = 'search test';
118
121
  const textValue = 'I am a text';
@@ -131,134 +134,126 @@ describe('AdvancedFields', () => {
131
134
 
132
135
  fireEvent.click(selectField);
133
136
  await act(async () => {
134
- await fireEvent.click(screen.getByText('option 2'));
135
- fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
136
-
137
- fireEvent.click(resourceSelectField);
138
- fireEvent.click(screen.getByText('resource2'));
139
- fireEvent.change(textField, {
140
- target: { value: textValue },
141
- });
142
- fireEvent.change(searchField, {
143
- target: { value: searchValue },
144
- });
145
- await fireEvent.change(dateField, {
146
- target: { value: dateValue },
147
- });
148
- fireEvent.change(timeField, {
149
- target: { value: timeValue },
150
- });
151
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
152
- });
153
- expect(
154
- screen.getByLabelText('adv plain hidden', {
155
- selector: 'textarea',
156
- }).value
157
- ).toBe(textValue);
158
- expect(searchField.value).toBe(searchValue);
159
- expect(screen.getByLabelText('adv date datepicker').value).toBe(dateValue);
160
- expect(timeField.value).toBe(timeValue);
161
- await act(async () => {
162
- fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
137
+ await userEvent.click(screen.getByText('option 2'), {}, clickOptions);
138
+ await userEvent.click(
139
+ screen.getAllByText(WIZARD_TITLES.advanced)[0],
140
+ {},
141
+ clickOptions
142
+ );
143
+
144
+ await userEvent.click(resourceSelectField, {}, clickOptions);
145
+ await userEvent.click(screen.getByText('resource2'), {}, clickOptions);
146
+ await typeIntoField(textField, textValue);
147
+ await typeIntoField(searchField, searchValue);
148
+ fireEvent.change(dateField, { target: { value: dateValue } });
149
+ fireEvent.change(timeField, { target: { value: timeValue } });
150
+ jest.advanceTimersByTime(1000);
163
151
  });
152
+
153
+ expect(textField).toHaveValue(textValue);
154
+ expect(searchField).toHaveValue(searchValue);
155
+ expect(dateField).toHaveValue(dateValue);
156
+ expect(timeField).toHaveValue(timeValue);
157
+
158
+ await clickWizardStep(WIZARD_TITLES.categoryAndTemplate);
164
159
  expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
165
160
  3
166
161
  );
167
162
 
168
- await act(async () => {
169
- await fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
170
- jest.advanceTimersByTime(1000);
171
- });
172
- expect(textField.value).toBe(textValue);
173
- expect(searchField.value).toBe(searchValue);
174
- expect(dateField.value).toBe(dateValue);
175
- expect(timeField.value).toBe(timeValue);
176
- expect(screen.queryAllByText('option 1')).toHaveLength(0);
177
- expect(screen.queryAllByText('option 2')).toHaveLength(1);
178
- expect(screen.queryAllByDisplayValue('resource1')).toHaveLength(0);
179
- expect(screen.queryAllByDisplayValue('resource2')).toHaveLength(1);
163
+ await goToAdvancedStep();
164
+
165
+ expect(textField).toHaveValue(textValue);
166
+ expect(searchField).toHaveValue(searchValue);
167
+ expect(dateField).toHaveValue(dateValue);
168
+ expect(timeField).toHaveValue(timeValue);
169
+ expect(screen.queryByText('option 1')).not.toBeInTheDocument();
170
+ expect(screen.getByText('option 2')).toBeInTheDocument();
171
+ expect(screen.queryByDisplayValue('resource1')).not.toBeInTheDocument();
172
+ expect(screen.getByDisplayValue('resource2')).toBeInTheDocument();
180
173
  });
181
- it('fill defaults into fields', async () => {
182
- render(
183
- <MockedProvider mocks={gqlMock} addTypename={false}>
184
- <Provider store={store}>
185
- <JobWizard />
186
- </Provider>
187
- </MockedProvider>
188
- );
189
- await act(async () => {
190
- fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
191
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
192
- });
174
+
175
+ it('fills default values into advanced fields', async () => {
176
+ renderJobWizard();
177
+ await goToAdvancedStep();
193
178
 
194
179
  expect(
195
- screen.getByLabelText('ssh user', {
196
- selector: 'input',
197
- }).value
198
- ).toBe('');
180
+ screen.getByLabelText('ssh user', { selector: 'input' })
181
+ ).toHaveValue('');
199
182
  expect(
200
- screen.getByLabelText('effective user', {
201
- selector: 'input',
202
- }).value
203
- ).toBe('default effective user');
183
+ screen.getByLabelText('effective user', { selector: 'input' })
184
+ ).toHaveValue('default effective user');
204
185
  expect(
205
- screen.getByLabelText('timeout to kill', {
206
- selector: 'input',
207
- }).value
208
- ).toBe('2');
209
-
186
+ screen.getByLabelText('timeout to kill', { selector: 'input' })
187
+ ).toHaveValue('2');
210
188
  expect(
211
- screen.getByLabelText('description preview', {
212
- selector: 'input',
213
- }).value
214
- ).toBe(
189
+ screen.getByLabelText('description preview', { selector: 'input' })
190
+ ).toHaveValue(
215
191
  'template1 with inputs adv plain hidden="Default val" adv plain select="" adv resource select="" adv search="" adv date="" plain hidden="Default val"'
216
192
  );
217
193
  });
218
- it('DescriptionField', async () => {
219
- render(
220
- <Provider store={store}>
221
- <JobWizard />
222
- </Provider>
223
- );
224
- await act(async () => {
225
- fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
226
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
227
- });
194
+
195
+ it('renders advanced fields step with expected form fields', async () => {
196
+ renderJobWizard();
197
+ await goToAdvancedStep();
198
+
199
+ expect(
200
+ screen.getByLabelText('adv plain hidden', { selector: 'textarea' })
201
+ ).toBeInTheDocument();
202
+ expect(
203
+ screen.getByLabelText('adv plain select toggle')
204
+ ).toBeInTheDocument();
205
+ expect(
206
+ screen.getByLabelText('adv resource select toggle')
207
+ ).toBeInTheDocument();
208
+ expect(
209
+ screen.getByLabelText('ssh user', { selector: 'input' })
210
+ ).toBeInTheDocument();
211
+ expect(
212
+ screen.getByLabelText('effective user', { selector: 'input' })
213
+ ).toBeInTheDocument();
214
+ expect(
215
+ screen.getByLabelText('description preview', { selector: 'input' })
216
+ ).toBeInTheDocument();
217
+ });
218
+
219
+ it('updates description preview when editing the description template', async () => {
220
+ renderJobWizard({ withGql: false });
221
+ await goToAdvancedStep();
228
222
 
229
223
  const textField = screen.getByLabelText('adv plain hidden', {
230
224
  selector: 'textarea',
231
225
  });
232
- await act(async () => {
233
- await fireEvent.change(textField, {
234
- target: { value: 'test command' },
235
- });
236
- });
226
+ await typeIntoField(textField, 'test command');
227
+
237
228
  const descriptionValue = 'Run %{adv plain hidden} %{wrong command name}';
238
229
 
239
230
  await act(async () => {
240
- fireEvent.click(screen.getByText('Edit job description template'));
231
+ await userEvent.click(
232
+ screen.getByText('Edit job description template'),
233
+ {},
234
+ clickOptions
235
+ );
241
236
  });
242
237
 
243
238
  const editText = screen.getByLabelText('description edit', {
244
239
  selector: 'input',
245
240
  });
246
- await fireEvent.change(editText, {
247
- target: { value: descriptionValue },
248
- });
241
+ await typeIntoField(editText, descriptionValue);
249
242
  await act(async () => {
250
- fireEvent.click(screen.getByText('Preview job description'));
243
+ await userEvent.click(
244
+ screen.getByText('Preview job description'),
245
+ {},
246
+ clickOptions
247
+ );
251
248
  });
249
+
252
250
  expect(
253
- screen.getByLabelText('description preview', {
254
- selector: 'input',
255
- }).value
256
- ).toBe('Run test command %{wrong command name}');
251
+ screen.getByLabelText('description preview', { selector: 'input' })
252
+ ).toHaveValue('Run test command %{wrong command name}');
257
253
  });
258
254
 
259
- it('DescriptionField with no inputs', async () => {
255
+ it('shows template name in description preview when template has no inputs', async () => {
260
256
  jest.spyOn(api, 'get');
261
-
262
257
  jest.spyOn(selectors, 'selectTemplateInputs');
263
258
  jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
264
259
  selectors.selectTemplateInputs.mockImplementation(() => []);
@@ -289,23 +284,20 @@ describe('AdvancedFields', () => {
289
284
  }
290
285
  return { type: 'get', ...action };
291
286
  });
292
- render(
293
- <Provider store={store}>
294
- <JobWizard />
295
- </Provider>
296
- );
297
- await act(async () => {
298
- fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
299
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
300
- });
287
+
288
+ renderJobWizard({ withGql: false });
289
+ await goToAdvancedStep();
290
+
301
291
  expect(
302
- screen.getByLabelText('description preview', {
303
- selector: 'input',
304
- }).value
305
- ).toBe('template1');
292
+ screen.getByLabelText('description preview', { selector: 'input' })
293
+ ).toHaveValue('template1');
294
+
295
+ selectors.selectTemplateInputs.mockRestore();
296
+ selectors.selectAdvancedTemplateInputs.mockRestore();
297
+ api.get.mockRestore();
306
298
  });
307
299
 
308
- it('DescriptionField with description_format', async () => {
300
+ it('uses description_format for description preview', async () => {
309
301
  jest.spyOn(api, 'get');
310
302
  jest.spyOn(selectors, 'selectTemplateInputs');
311
303
  selectors.selectTemplateInputs.mockImplementation(() => [
@@ -334,7 +326,6 @@ describe('AdvancedFields', () => {
334
326
  ...jobTemplateResponse.job_template,
335
327
  description_format: 'Run %{command}',
336
328
  },
337
-
338
329
  template_inputs: [
339
330
  {
340
331
  name: 'command',
@@ -363,46 +354,34 @@ describe('AdvancedFields', () => {
363
354
  }
364
355
  return { type: 'get', ...action };
365
356
  });
366
- render(
367
- <Provider store={store}>
368
- <JobWizard />
369
- </Provider>
370
- );
371
- await act(async () => {
372
- fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
373
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
374
- });
357
+
358
+ renderJobWizard({ withGql: false });
359
+ await goToAdvancedStep();
360
+
375
361
  expect(
376
- screen.getByLabelText('description preview', {
377
- selector: 'input',
378
- }).value
379
- ).toBe('Run Default val');
362
+ screen.getByLabelText('description preview', { selector: 'input' })
363
+ ).toHaveValue('Run Default val');
364
+
365
+ selectors.selectTemplateInputs.mockRestore();
366
+ api.get.mockRestore();
380
367
  });
381
368
 
382
- it('search resources action', async () => {
383
- jest.useFakeTimers();
384
- mockApi(api);
369
+ it('dispatches resource search when typing in resource select', async () => {
385
370
  const newStore = testSetup(selectors, api);
386
- render(
387
- <Provider store={newStore}>
388
- <JobWizard />
389
- </Provider>
390
- );
391
- await act(async () => {
392
- fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
393
- jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
394
- });
371
+ mockApi(api);
372
+
373
+ renderJobWizard({ customStore: newStore, withGql: false });
374
+ await goToAdvancedStep();
375
+
395
376
  const resourceSelectField = screen.getByLabelText(
396
377
  'adv resource select typeahead input'
397
378
  );
398
379
 
380
+ await typeIntoField(resourceSelectField, 'some search');
399
381
  await act(async () => {
400
- await fireEvent.change(resourceSelectField, {
401
- target: { value: 'some search' },
402
- });
403
-
404
382
  jest.advanceTimersByTime(10000);
405
383
  });
384
+
406
385
  const actions = newStore.getActions();
407
386
  const resourceSearchAction = actions.filter(
408
387
  action => action.key === 'ForemanTasksTask'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 17.2.1
4
+ version: 18.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-09-03 00:00:00.000000000 Z
10
+ date: 2026-09-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: deface
@@ -405,6 +405,7 @@ files:
405
405
  - webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js
406
406
  - webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
407
407
  - webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js
408
+ - webpack/JobInvocationDetail/__tests__/areAllHostsTerminal.test.js
408
409
  - webpack/JobInvocationDetail/__tests__/fixtures.js
409
410
  - webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js
410
411
  - webpack/JobInvocationDetail/index.js
@@ -566,7 +567,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
566
567
  - !ruby/object:Gem::Version
567
568
  version: '0'
568
569
  requirements: []
569
- rubygems_version: 4.0.16
570
+ rubygems_version: 4.0.20
570
571
  specification_version: 4
571
572
  summary: A plugin bringing remote execution to the Foreman, completing the config
572
573
  management functionality with remote management functionality.