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
@@ -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'
@@ -1,6 +1,76 @@
1
- import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import '@testing-library/jest-dom';
2
5
  import HostItem from '../components/HostItem';
3
6
  import { HostItemFixtures } from './fixtures';
4
7
 
5
- describe('HostItem', () =>
6
- testComponentSnapshotsWithFixtures(HostItem, HostItemFixtures));
8
+ jest.unmock('foremanReact/components/common/ActionButtons/ActionButtons');
9
+
10
+ const renderHostItem = props =>
11
+ render(
12
+ <table>
13
+ <tbody>
14
+ <HostItem {...props} />
15
+ </tbody>
16
+ </table>
17
+ );
18
+
19
+ describe('HostItem', () => {
20
+ it('renders the host name as a link with the provided URL', () => {
21
+ renderHostItem(HostItemFixtures.renders);
22
+
23
+ expect(screen.getByRole('link', { name: 'Host1' })).toHaveAttribute(
24
+ 'href',
25
+ '/host1'
26
+ );
27
+ });
28
+
29
+ it('renders the host status', () => {
30
+ renderHostItem(HostItemFixtures.renders);
31
+
32
+ expect(screen.getByText('Succeeded')).toBeInTheDocument();
33
+ });
34
+
35
+ it('renders no action buttons when actions are empty', () => {
36
+ renderHostItem(HostItemFixtures.renders);
37
+
38
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
39
+ });
40
+
41
+ it('renders a disabled link when link is not provided', () => {
42
+ renderHostItem({
43
+ ...HostItemFixtures.renders,
44
+ link: '',
45
+ });
46
+
47
+ expect(screen.getByRole('link', { name: 'Host1' })).toHaveAttribute(
48
+ 'href',
49
+ '#'
50
+ );
51
+ });
52
+
53
+ it('renders action buttons and calls action on click', async () => {
54
+ const onClick = jest.fn();
55
+
56
+ renderHostItem({
57
+ ...HostItemFixtures.renders,
58
+ actions: [{ title: 'View output', action: { onClick } }],
59
+ });
60
+
61
+ await userEvent.click(
62
+ screen.getByRole('button', { name: 'View output' })
63
+ );
64
+
65
+ expect(onClick).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it('renders failed status text for error status', () => {
69
+ renderHostItem({
70
+ ...HostItemFixtures.renders,
71
+ status: 'error',
72
+ });
73
+
74
+ expect(screen.getByText('Failed')).toBeInTheDocument();
75
+ });
76
+ });