foreman_remote_execution 16.3.1 → 16.3.2

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: 1f3605e5f510dbb583d0af3208b3f80f7fdea955420f5d65425d896a37997b3c
4
- data.tar.gz: '092f3cddbb03c09274aaedb3517b4d3cb9aafe2e08399e8361ac1772591d8d5d'
3
+ metadata.gz: a7254f1da767b1e48ba82c93e57cd093611b42cb302e9eca41f0bb844a0d1883
4
+ data.tar.gz: c19c6c596d7e531794003be27795b665ad4e2ef9055e94a51c4d6c50c361a75f
5
5
  SHA512:
6
- metadata.gz: f7295aee226fb5438f923dc74a629e427d01a967cc309404297417dd1b850bec2051350f0a465e26d3345ff707907abf963b4e547be7707095ce0e8d5e33f090
7
- data.tar.gz: 7a3a7d1d9730a007208053c240fba27a1cef39d8121af9c0242c293641b93d77c99c1b1c573674e0a5d9bebcb161263cf7f262260830ff29ec8ffcc8ccd0f406
6
+ metadata.gz: 792c2e9e61c5e42e471832f47503189eab7edb69a2f4c857a1ab9b659eef114da16fc89b08d78f891b600997f92221cbe0651249fbe953f8ad0f9bd5d9de4c37
7
+ data.tar.gz: 0e3797e25d92fb9e39606913937c0cb9a266f48c1780141969927362e095bd253a75426cf7e6309c14f4d981d29b77ff5aea018ad3a8f0b14fe49818d2c46050
@@ -116,13 +116,11 @@ module Api
116
116
  def hosts
117
117
  set_hosts_and_template_invocations
118
118
  set_statuses_and_smart_proxies
119
- @total = @job_invocation.targeting.hosts.size
119
+ @total = @hosts.size
120
120
  @hosts = @hosts.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
121
+ @subtotal = @hosts.total_entries
121
122
  if params[:awaiting]
122
123
  @hosts = @hosts.select { |host| @host_statuses[host.id] == 'N/A' }
123
- @subtotal = @hosts.size
124
- else
125
- @subtotal = @hosts.respond_to?(:total_entries) ? @hosts.total_entries : @hosts.sizes
126
124
  end
127
125
  render :hosts, :layout => 'api/v2/layouts/index_layout'
128
126
  end
@@ -303,7 +301,7 @@ module Api
303
301
  @pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values)
304
302
  @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
305
303
 
306
- unless params[:search].nil?
304
+ if params[:search].present?
307
305
  @hosts = @hosts.joins(:template_invocations)
308
306
  .where(:template_invocations => { :job_invocation_id => @job_invocation.id})
309
307
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '16.3.1'.freeze
2
+ VERSION = '16.3.2'.freeze
3
3
  end
@@ -74,6 +74,10 @@ section.job-additional-info {
74
74
  margin-left: 10px;
75
75
  margin-right: 15px;
76
76
  }
77
+
78
+ .pf-v5-c-table__tbody > tr.row-hidden {
79
+ display: none;
80
+ }
77
81
  }
78
82
 
79
83
  .template-invocation {
@@ -61,9 +61,47 @@ const JobInvocationHostTable = ({
61
61
  const [allHostsIds, setAllHostsIds] = useState([]);
62
62
 
63
63
  // Expansive items
64
- const [expandedHost, setExpandedHost] = useState([]);
64
+ const [expandedHost, setExpandedHost] = useState(new Set());
65
65
  const prevStatusLabel = useRef(statusLabel);
66
66
 
67
+ const [hostInvocationStates, setHostInvocationStates] = useState({});
68
+
69
+ const getInvocationState = hostId =>
70
+ hostInvocationStates[hostId] || {
71
+ showOutputType: { stderr: true, stdout: true, debug: true },
72
+ showTemplatePreview: false,
73
+ showCommand: false,
74
+ };
75
+
76
+ const updateInvocationState = (hostId, stateKey, value) => {
77
+ setHostInvocationStates(prevStates => {
78
+ const currentHostState = getInvocationState(hostId);
79
+
80
+ const newValue =
81
+ typeof value === 'function' ? value(currentHostState[stateKey]) : value;
82
+
83
+ return {
84
+ ...prevStates,
85
+ [hostId]: {
86
+ ...currentHostState,
87
+ [stateKey]: newValue,
88
+ },
89
+ };
90
+ });
91
+ };
92
+
93
+ const isHostExpanded = hostId => expandedHost.has(hostId);
94
+ const setHostExpanded = (hostId, isExpanding = true) =>
95
+ setExpandedHost(prevExpandedSet => {
96
+ const newSet = new Set(prevExpandedSet);
97
+ if (isExpanding) {
98
+ newSet.add(hostId);
99
+ } else {
100
+ newSet.delete(hostId);
101
+ }
102
+ return newSet;
103
+ });
104
+
67
105
  // Page table params
68
106
  // Parse URL
69
107
  const {
@@ -307,29 +345,18 @@ const JobInvocationHostTable = ({
307
345
  </Tr>
308
346
  );
309
347
 
310
- const isHostExpanded = host => expandedHost.includes(host.id);
311
-
312
- const setHostExpanded = (host, isExpanding = true) =>
313
- setExpandedHost(prevExpanded => {
314
- const otherExpandedHosts = prevExpanded.filter(h => h !== host.id);
315
- return isExpanding
316
- ? [...otherExpandedHosts, host.id]
317
- : otherExpandedHosts;
318
- });
319
-
320
348
  const pageHostIds = results.map(h => h.id);
321
349
 
322
350
  const areAllPageRowsExpanded =
323
351
  pageHostIds.length > 0 &&
324
- pageHostIds.every(hostId => expandedHost.includes(hostId));
352
+ pageHostIds.every(hostId => expandedHost.has(hostId));
325
353
 
326
354
  const onExpandAll = () => {
327
355
  setExpandedHost(() => {
328
356
  if (areAllPageRowsExpanded) {
329
- return [];
357
+ return new Set();
330
358
  }
331
-
332
- return pageHostIds;
359
+ return new Set(pageHostIds);
333
360
  });
334
361
  };
335
362
 
@@ -393,54 +420,88 @@ const JobInvocationHostTable = ({
393
420
  isDeleteable={false}
394
421
  childrenOutsideTbody
395
422
  >
396
- {results.map((result, rowIndex) => (
397
- <Tbody key={result.id} isExpanded={isHostExpanded(result)}>
398
- <Tr ouiaId={`table-row-${result.id}`}>
399
- <Td
400
- expand={{
401
- rowIndex,
402
- isExpanded: isHostExpanded(result),
403
- onToggle: () =>
404
- setHostExpanded(result, !isHostExpanded(result)),
405
- expandId: 'host-expandable',
406
- }}
407
- />
408
- <RowSelectTd rowData={result} {...{ selectOne, isSelected }} />
409
- {columnNamesKeys.map(k => (
410
- <Td key={k}>{columns[k].wrapper(result)}</Td>
411
- ))}
412
- <Td isActionCell>
413
- <RowActions hostID={result.id} jobID={id} />
414
- </Td>
415
- </Tr>
416
- <Tr
417
- isExpanded={isHostExpanded(result)}
418
- ouiaId="table-row-expanded-sections"
419
- >
420
- <Td
421
- dataLabel={`${result.id}-expandable-content`}
422
- colSpan={columnNamesKeys.length + 3}
423
+ {results.map((result, rowIndex) => {
424
+ const currentInvocationState = getInvocationState(result.id);
425
+ return (
426
+ <Tbody key={result.id}>
427
+ <Tr ouiaId={`table-row-${result.id}`}>
428
+ <Td
429
+ expand={{
430
+ rowIndex,
431
+ isExpanded: isHostExpanded(result.id),
432
+ onToggle: () =>
433
+ setHostExpanded(result.id, !isHostExpanded(result.id)),
434
+ expandId: 'host-expandable',
435
+ }}
436
+ />
437
+ <RowSelectTd
438
+ rowData={result}
439
+ selectOne={selectOne}
440
+ isSelected={isSelected}
441
+ />
442
+ {columnNamesKeys.map(k => (
443
+ <Td key={k}>{columns[k].wrapper(result)}</Td>
444
+ ))}
445
+ <Td isActionCell>
446
+ <RowActions hostID={result.id} jobID={id} />
447
+ </Td>
448
+ </Tr>
449
+ <Tr
450
+ isExpanded={isHostExpanded(result.id)}
451
+ ouiaId="table-row-expanded-sections"
452
+ className={!isHostExpanded(result.id) ? 'row-hidden' : ''}
423
453
  >
424
- <ExpandableRowContent>
425
- {result.job_status === 'cancelled' ||
426
- result.job_status === 'N/A' ? (
427
- <div>
428
- {__('A task for this host has not been started')}
429
- </div>
430
- ) : (
431
- <TemplateInvocation
432
- key={`${result.id}-${result.job_status}`}
433
- hostID={result.id}
434
- jobID={id}
435
- isInTableView
436
- isExpanded={isHostExpanded(result)}
437
- />
438
- )}
439
- </ExpandableRowContent>
440
- </Td>
441
- </Tr>
442
- </Tbody>
443
- ))}
454
+ <Td
455
+ dataLabel={`${result.id}-expandable-content`}
456
+ colSpan={columnNamesKeys.length + 3}
457
+ >
458
+ <ExpandableRowContent>
459
+ {result.job_status === 'cancelled' ||
460
+ result.job_status === 'N/A' ? (
461
+ <div>
462
+ {__('A task for this host has not been started')}
463
+ </div>
464
+ ) : (
465
+ <TemplateInvocation
466
+ key={result.id}
467
+ hostID={result.id}
468
+ jobID={id}
469
+ isInTableView
470
+ isExpanded={isHostExpanded(result.id)}
471
+ showOutputType={currentInvocationState.showOutputType}
472
+ showTemplatePreview={
473
+ currentInvocationState.showTemplatePreview
474
+ }
475
+ showCommand={currentInvocationState.showCommand}
476
+ setShowOutputType={value =>
477
+ updateInvocationState(
478
+ result.id,
479
+ 'showOutputType',
480
+ value
481
+ )
482
+ }
483
+ setShowTemplatePreview={value =>
484
+ updateInvocationState(
485
+ result.id,
486
+ 'showTemplatePreview',
487
+ value
488
+ )
489
+ }
490
+ setShowCommand={value =>
491
+ updateInvocationState(
492
+ result.id,
493
+ 'showCommand',
494
+ value
495
+ )
496
+ }
497
+ />
498
+ )}
499
+ </ExpandableRowContent>
500
+ </Td>
501
+ </Tr>
502
+ </Tbody>
503
+ );
504
+ })}
444
505
  </Table>
445
506
  </TableIndexPage>
446
507
  </>
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef } from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import { isEmpty } from 'lodash';
3
3
  import PropTypes from 'prop-types';
4
4
  import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
@@ -60,6 +60,12 @@ export const TemplateInvocation = ({
60
60
  isExpanded,
61
61
  hostName,
62
62
  hostProxy,
63
+ showOutputType,
64
+ setShowOutputType,
65
+ showTemplatePreview,
66
+ setShowTemplatePreview,
67
+ showCommand,
68
+ setShowCommand,
63
69
  }) => {
64
70
  const intervalRef = useRef(null);
65
71
  const templateURL = showTemplateInvocationUrl(hostID, jobID);
@@ -74,14 +80,6 @@ export const TemplateInvocation = ({
74
80
  responseRef.current = response;
75
81
  }, [response]);
76
82
 
77
- const [showOutputType, setShowOutputType] = useState({
78
- stderr: true,
79
- stdout: true,
80
- debug: true,
81
- });
82
- const [showTemplatePreview, setShowTemplatePreview] = useState(false);
83
- const [showCommand, setShowCommand] = useState(false);
84
-
85
83
  useEffect(() => {
86
84
  const dispatchFetch = () => {
87
85
  dispatch(
@@ -123,12 +121,8 @@ export const TemplateInvocation = ({
123
121
  };
124
122
  }, [isExpanded, dispatch, templateURL, hostID]);
125
123
 
126
- if (!isExpanded) {
127
- return null;
128
- }
129
-
130
- if ((status === STATUS.PENDING && isEmpty(response)) || !response) {
131
- return <Skeleton />;
124
+ if (!response || (status === STATUS.PENDING && isEmpty(response))) {
125
+ return <Skeleton data-testid="template-invocation-skeleton" />;
132
126
  }
133
127
 
134
128
  const errorMessage =
@@ -239,6 +233,16 @@ TemplateInvocation.propTypes = {
239
233
  jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
240
234
  isInTableView: PropTypes.bool,
241
235
  isExpanded: PropTypes.bool,
236
+ showOutputType: PropTypes.shape({
237
+ stderr: PropTypes.bool,
238
+ stdout: PropTypes.bool,
239
+ debug: PropTypes.bool,
240
+ }).isRequired,
241
+ setShowOutputType: PropTypes.func.isRequired,
242
+ showTemplatePreview: PropTypes.bool.isRequired,
243
+ setShowTemplatePreview: PropTypes.func.isRequired,
244
+ showCommand: PropTypes.bool.isRequired,
245
+ setShowCommand: PropTypes.func.isRequired,
242
246
  };
243
247
 
244
248
  TemplateInvocation.defaultProps = {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useCallback } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import {
4
4
  ToggleGroup,
@@ -27,31 +27,49 @@ export const OutputToggleGroup = ({
27
27
  taskCancellable,
28
28
  permissions,
29
29
  }) => {
30
- const handleSTDERRClick = _isSelected => {
31
- setShowOutputType(prevShowOutputType => ({
32
- ...prevShowOutputType,
33
- stderr: _isSelected,
34
- }));
35
- };
30
+ const handleSTDERRClick = useCallback(
31
+ _isSelected => {
32
+ setShowOutputType(prevShowOutputType => ({
33
+ ...prevShowOutputType,
34
+ stderr: _isSelected,
35
+ }));
36
+ },
37
+ [setShowOutputType]
38
+ );
36
39
 
37
- const handleSTDOUTClick = _isSelected => {
38
- setShowOutputType(prevShowOutputType => ({
39
- ...prevShowOutputType,
40
- stdout: _isSelected,
41
- }));
42
- };
43
- const handleDEBUGClick = _isSelected => {
44
- setShowOutputType(prevShowOutputType => ({
45
- ...prevShowOutputType,
46
- debug: _isSelected,
47
- }));
48
- };
49
- const handlePreviewTemplateClick = _isSelected => {
50
- setShowTemplatePreview(_isSelected);
51
- };
52
- const handleCommandClick = _isSelected => {
53
- setShowCommand(_isSelected);
54
- };
40
+ const handleSTDOUTClick = useCallback(
41
+ _isSelected => {
42
+ setShowOutputType(prevShowOutputType => ({
43
+ ...prevShowOutputType,
44
+ stdout: _isSelected,
45
+ }));
46
+ },
47
+ [setShowOutputType]
48
+ );
49
+
50
+ const handleDEBUGClick = useCallback(
51
+ _isSelected => {
52
+ setShowOutputType(prevShowOutputType => ({
53
+ ...prevShowOutputType,
54
+ debug: _isSelected,
55
+ }));
56
+ },
57
+ [setShowOutputType]
58
+ );
59
+
60
+ const handlePreviewTemplateClick = useCallback(
61
+ _isSelected => {
62
+ setShowTemplatePreview(_isSelected);
63
+ },
64
+ [setShowTemplatePreview]
65
+ );
66
+
67
+ const handleCommandClick = useCallback(
68
+ _isSelected => {
69
+ setShowCommand(_isSelected);
70
+ },
71
+ [setShowCommand]
72
+ );
55
73
 
56
74
  const toggleGroupItems = {
57
75
  stderr: {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { useSelector } from 'react-redux';
4
4
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
@@ -26,6 +26,15 @@ const TemplateInvocationPage = ({
26
26
  ],
27
27
  isPf4: true,
28
28
  };
29
+
30
+ const [showOutputType, setShowOutputType] = useState({
31
+ stderr: true,
32
+ stdout: true,
33
+ debug: true,
34
+ });
35
+ const [showTemplatePreview, setShowTemplatePreview] = useState(false);
36
+ const [showCommand, setShowCommand] = useState(false);
37
+
29
38
  return (
30
39
  <PageLayout
31
40
  header={description}
@@ -39,6 +48,12 @@ const TemplateInvocationPage = ({
39
48
  isExpanded
40
49
  hostName={hostName}
41
50
  hostProxy={hostProxy}
51
+ showOutputType={showOutputType}
52
+ setShowOutputType={setShowOutputType}
53
+ showTemplatePreview={showTemplatePreview}
54
+ setShowTemplatePreview={setShowTemplatePreview}
55
+ showCommand={showCommand}
56
+ setShowCommand={setShowCommand}
42
57
  />
43
58
  </PageLayout>
44
59
  );
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import configureMockStore from 'redux-mock-store';
3
3
  import { Provider } from 'react-redux';
4
- import { render, screen, act, fireEvent } from '@testing-library/react';
4
+ import { render, screen, fireEvent } from '@testing-library/react';
5
5
  import '@testing-library/jest-dom/extend-expect';
6
6
  import * as api from 'foremanReact/redux/API';
7
7
  import * as selectors from '../JobInvocationSelectors';
@@ -10,12 +10,7 @@ import { mockTemplateInvocationResponse } from './fixtures';
10
10
 
11
11
  jest.spyOn(api, 'get');
12
12
  jest.mock('../JobInvocationSelectors');
13
- selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
14
- 'RESOLVED'
15
- );
16
- selectors.selectTemplateInvocation.mockImplementation(() => () =>
17
- mockTemplateInvocationResponse
18
- );
13
+
19
14
  const mockStore = configureMockStore([]);
20
15
  const store = mockStore({
21
16
  HOSTS_API: {
@@ -24,79 +19,122 @@ const store = mockStore({
24
19
  },
25
20
  },
26
21
  });
22
+
23
+ Object.assign(navigator, {
24
+ clipboard: {
25
+ writeText: jest.fn().mockResolvedValue(undefined),
26
+ },
27
+ });
28
+
29
+ const mockProps = {
30
+ hostID: '1',
31
+ jobID: '1',
32
+ isInTableView: false,
33
+ isExpanded: true,
34
+ hostName: 'example-host',
35
+ hostProxy: { name: 'example-proxy', href: '#' },
36
+ showOutputType: { stderr: true, stdout: true, debug: true },
37
+ setShowOutputType: jest.fn(),
38
+ showTemplatePreview: false,
39
+ setShowTemplatePreview: jest.fn(),
40
+ showCommand: false,
41
+ setShowCommand: jest.fn(),
42
+ };
43
+
27
44
  describe('TemplateInvocation', () => {
28
- test('render', async () => {
45
+ beforeEach(() => {
46
+ selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
47
+ 'RESOLVED'
48
+ );
49
+ selectors.selectTemplateInvocation.mockImplementation(() => () =>
50
+ mockTemplateInvocationResponse
51
+ );
52
+ });
53
+
54
+ test('render', () => {
29
55
  render(
30
56
  <Provider store={store}>
31
- <TemplateInvocation
32
- hostID="1"
33
- jobID="1"
34
- isInTableView={false}
35
- isExpanded
36
- hostName="example-host"
37
- hostProxy={{ name: 'example-proxy', href: '#' }}
38
- />
57
+ <TemplateInvocation {...mockProps} />
39
58
  </Provider>
40
59
  );
41
60
 
42
61
  expect(screen.getByText('example-host')).toBeInTheDocument();
43
62
  expect(screen.getByText('example-proxy')).toBeInTheDocument();
44
-
45
63
  expect(screen.getByText(/using Smart Proxy/)).toBeInTheDocument();
46
64
  expect(screen.getByText(/Target:/)).toBeInTheDocument();
47
-
48
65
  expect(screen.getByText('This is red text')).toBeInTheDocument();
49
66
  expect(screen.getByText('This is default text')).toBeInTheDocument();
67
+ expect(screen.getByLabelText('Copy to clipboard')).toBeInTheDocument();
50
68
  });
51
- test('filtering toggles', () => {
52
- render(
69
+
70
+ test('shows "No output" message when all toggles are off', () => {
71
+ const { rerender } = render(
53
72
  <Provider store={store}>
54
- <TemplateInvocation
55
- hostID="1"
56
- jobID="1"
57
- isInTableView={false}
58
- isExpanded
59
- hostName="example-host"
60
- hostProxy={{ name: 'example-proxy', href: '#' }}
61
- />
73
+ <TemplateInvocation {...mockProps} />
62
74
  </Provider>
63
75
  );
64
76
 
65
- act(() => {
66
- fireEvent.click(screen.getByText('STDOUT'));
67
- fireEvent.click(screen.getByText('DEBUG'));
68
- fireEvent.click(screen.getByText('STDERR'));
69
- });
70
77
  expect(
71
- screen.queryAllByText('No output for the selected filters')
72
- ).toHaveLength(1);
73
- expect(screen.queryAllByText('Exit status: 1')).toHaveLength(0);
74
- expect(
75
- screen.queryAllByText('StandardError: Job execution failed')
76
- ).toHaveLength(0);
78
+ screen.queryByText('No output for the selected filters')
79
+ ).not.toBeInTheDocument();
80
+
81
+ const newProps = {
82
+ ...mockProps,
83
+ showOutputType: { stderr: false, stdout: false, debug: false },
84
+ };
85
+
86
+ rerender(
87
+ <Provider store={store}>
88
+ <TemplateInvocation {...newProps} />
89
+ </Provider>
90
+ );
77
91
 
78
- act(() => {
79
- fireEvent.click(screen.getByText('STDOUT'));
80
- });
81
92
  expect(
82
- screen.queryAllByText('No output for the selected filters')
83
- ).toHaveLength(0);
84
- expect(screen.queryAllByText('Exit status: 1')).toHaveLength(1);
93
+ screen.getByText('No output for the selected filters')
94
+ ).toBeInTheDocument();
95
+ });
96
+
97
+ test('correctly filters specific output types', () => {
98
+ const { rerender } = render(
99
+ <Provider store={store}>
100
+ <TemplateInvocation {...mockProps} />
101
+ </Provider>
102
+ );
103
+
104
+ expect(screen.getByText('Exit status: 1')).toBeInTheDocument(); // stdout
85
105
  expect(
86
- screen.queryAllByText('StandardError: Job execution failed')
87
- ).toHaveLength(0);
106
+ screen.getByText('StandardError: Job execution failed')
107
+ ).toBeInTheDocument(); // debug
88
108
 
89
- act(() => {
90
- fireEvent.click(screen.getByText('DEBUG'));
91
- });
109
+ // Turn off stdout
110
+ rerender(
111
+ <Provider store={store}>
112
+ <TemplateInvocation
113
+ {...mockProps}
114
+ showOutputType={{ stderr: true, stdout: false, debug: true }}
115
+ />
116
+ </Provider>
117
+ );
118
+ expect(screen.queryByText('Exit status: 1')).not.toBeInTheDocument();
92
119
  expect(
93
- screen.queryAllByText('No output for the selected filters')
94
- ).toHaveLength(0);
95
- expect(screen.queryAllByText('Exit status: 1')).toHaveLength(1);
120
+ screen.getByText('StandardError: Job execution failed')
121
+ ).toBeInTheDocument();
122
+
123
+ // Turn off debug
124
+ rerender(
125
+ <Provider store={store}>
126
+ <TemplateInvocation
127
+ {...mockProps}
128
+ showOutputType={{ stderr: true, stdout: false, debug: false }}
129
+ />
130
+ </Provider>
131
+ );
132
+ expect(screen.queryByText('Exit status: 1')).not.toBeInTheDocument();
96
133
  expect(
97
- screen.queryAllByText('StandardError: Job execution failed')
98
- ).toHaveLength(1);
134
+ screen.queryByText('StandardError: Job execution failed')
135
+ ).not.toBeInTheDocument();
99
136
  });
137
+
100
138
  test('displays an error alert when there is an error', async () => {
101
139
  selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
102
140
  'ERROR'
@@ -106,14 +144,7 @@ describe('TemplateInvocation', () => {
106
144
  }));
107
145
  render(
108
146
  <Provider store={store}>
109
- <TemplateInvocation
110
- hostID="1"
111
- jobID="1"
112
- isInTableView={false}
113
- isExpanded
114
- hostName="example-host"
115
- hostProxy={{ name: 'example-proxy', href: '#' }}
116
- />
147
+ <TemplateInvocation {...mockProps} />
117
148
  </Provider>
118
149
  );
119
150
 
@@ -129,19 +160,30 @@ describe('TemplateInvocation', () => {
129
160
  selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
130
161
  'PENDING'
131
162
  );
132
- selectors.selectTemplateInvocation.mockImplementation(() => () => ({}));
163
+ selectors.selectTemplateInvocation.mockImplementation(() => () => null);
133
164
  render(
134
165
  <Provider store={store}>
135
- <TemplateInvocation
136
- hostID="1"
137
- jobID="1"
138
- isInTableView={false}
139
- isExpanded
140
- hostName="example-host"
141
- />
166
+ <TemplateInvocation {...mockProps} />
142
167
  </Provider>
143
168
  );
144
169
 
145
- expect(document.querySelectorAll('.pf-v5-c-skeleton')).toHaveLength(1);
170
+ expect(
171
+ screen.getByTestId('template-invocation-skeleton')
172
+ ).toBeInTheDocument();
173
+ });
174
+
175
+ test('copies text to clipboard when clicked', async () => {
176
+ render(
177
+ <Provider store={store}>
178
+ <TemplateInvocation {...mockProps} />
179
+ </Provider>
180
+ );
181
+
182
+ const copyButton = screen.getByLabelText('Copy to clipboard');
183
+ fireEvent.click(copyButton);
184
+ expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
185
+ expect(
186
+ await screen.findByText('Successfully copied to clipboard!')
187
+ ).toBeInTheDocument();
146
188
  });
147
189
  });
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: 16.3.1
4
+ version: 16.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-11 00:00:00.000000000 Z
10
+ date: 2025-12-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: deface