foreman-tasks 13.0.0 → 13.1.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
  3. data/lib/foreman_tasks/version.rb +1 -1
  4. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
  5. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
  6. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
  7. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
  8. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +236 -131
  9. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
  10. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
  13. data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
  14. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +48 -51
  15. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +0 -39
  16. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +0 -5
  17. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
  18. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
  19. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
  20. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
  21. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
  22. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +10 -33
  23. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
  24. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
  25. data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
  26. data/webpack/Routes/routes.js +1 -1
  27. data/webpack/Routes/routes.test.js +1 -1
  28. data/webpack/index.js +0 -5
  29. metadata +8 -2
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
3
4
  import '@testing-library/jest-dom';
4
5
  import { TaskButtons } from '../TaskButtons';
5
6
 
@@ -16,29 +17,42 @@ const defaultProps = {
16
17
  setForceUnlockModalOpen,
17
18
  };
18
19
 
20
+ const openTaskActionsMenu = async () => {
21
+ await act(async () => {
22
+ fireEvent.click(screen.getByRole('button', { name: /^task actions$/i }));
23
+ await new Promise(resolve => setTimeout(resolve, 0));
24
+ });
25
+
26
+ await waitFor(() => {
27
+ expect(screen.getByRole('menu')).toBeInTheDocument();
28
+ });
29
+ };
30
+
19
31
  describe('TaskButtons', () => {
20
32
  beforeEach(() => {
21
33
  jest.clearAllMocks();
22
34
  });
23
35
 
24
36
  describe('rendering', () => {
25
- it('renders reload button with correct text when taskReload is false', () => {
37
+ it('shows start auto-reloading in overflow when taskReload is false', async () => {
26
38
  render(<TaskButtons {...defaultProps} taskReload={false} />);
39
+ await openTaskActionsMenu();
27
40
  expect(
28
- screen.getByRole('button', { name: /start auto-reloading/i })
41
+ screen.getByRole('menuitem', { name: /start auto-reloading/i })
29
42
  ).toBeInTheDocument();
30
43
  expect(
31
- screen.queryByRole('button', { name: /stop auto-reloading/i })
44
+ screen.queryByRole('menuitem', { name: /stop auto-reloading/i })
32
45
  ).not.toBeInTheDocument();
33
46
  });
34
47
 
35
- it('renders reload button with correct text when taskReload is true', () => {
48
+ it('shows stop auto-reloading in overflow when taskReload is true', async () => {
36
49
  render(<TaskButtons {...defaultProps} taskReload />);
50
+ await openTaskActionsMenu();
37
51
  expect(
38
- screen.getByRole('button', { name: /stop auto-reloading/i })
52
+ screen.getByRole('menuitem', { name: /stop auto-reloading/i })
39
53
  ).toBeInTheDocument();
40
54
  expect(
41
- screen.queryByRole('button', { name: /start auto-reloading/i })
55
+ screen.queryByRole('menuitem', { name: /start auto-reloading/i })
42
56
  ).not.toBeInTheDocument();
43
57
  });
44
58
 
@@ -54,15 +68,21 @@ describe('TaskButtons', () => {
54
68
  expect(dynflowLink).toHaveAttribute('target', '_blank');
55
69
  });
56
70
 
57
- it('disables dynflow console link when dynflowEnableConsole is false', () => {
58
- render(<TaskButtons {...defaultProps} dynflowEnableConsole={false} />);
71
+ it('marks dynflow console link disabled when dynflowEnableConsole is false', () => {
72
+ render(
73
+ <TaskButtons
74
+ {...defaultProps}
75
+ dynflowEnableConsole={false}
76
+ externalId="ext-id"
77
+ />
78
+ );
59
79
  const dynflowLink = screen.getByRole('link', {
60
80
  name: /dynflow console/i,
61
81
  });
62
- expect(dynflowLink).not.toBeDisabled();
82
+ expect(dynflowLink).toHaveAttribute('aria-disabled', 'true');
63
83
  });
64
84
 
65
- it('enables dynflow console link when dynflowEnableConsole is true', () => {
85
+ it('does not disable dynflow console link when dynflowEnableConsole is true', () => {
66
86
  render(
67
87
  <TaskButtons
68
88
  {...defaultProps}
@@ -73,18 +93,20 @@ describe('TaskButtons', () => {
73
93
  const dynflowLink = screen.getByRole('link', {
74
94
  name: /dynflow console/i,
75
95
  });
76
- expect(dynflowLink).not.toBeDisabled();
96
+ expect(dynflowLink).not.toHaveAttribute('aria-disabled', 'true');
77
97
  });
78
98
 
79
- it('disables resume and cancel buttons when canEdit is false', () => {
99
+ it('disables resume overflow item and cancel button when canEdit is false', async () => {
80
100
  render(<TaskButtons {...defaultProps} canEdit={false} />);
81
- expect(screen.getByRole('button', { name: /resume/i })).toBeDisabled();
82
101
  expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled();
102
+ await openTaskActionsMenu();
103
+ expect(screen.getByRole('menuitem', { name: /^resume$/i })).toBeDisabled();
83
104
  });
84
105
 
85
- it('disables resume button when resumable is false', () => {
106
+ it('disables resume overflow item when resumable is false', async () => {
86
107
  render(<TaskButtons {...defaultProps} canEdit resumable={false} />);
87
- expect(screen.getByRole('button', { name: /resume/i })).toBeDisabled();
108
+ await openTaskActionsMenu();
109
+ expect(screen.getByRole('menuitem', { name: /^resume$/i })).toBeDisabled();
88
110
  });
89
111
 
90
112
  it('disables cancel button when cancellable is false', () => {
@@ -92,66 +114,63 @@ describe('TaskButtons', () => {
92
114
  expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled();
93
115
  });
94
116
 
95
- it('disables unlock button when state is not paused', () => {
117
+ it('disables unlock overflow item when state is not paused', async () => {
96
118
  render(<TaskButtons {...defaultProps} canEdit state="running" />);
97
- expect(screen.getByRole('button', { name: /^unlock$/i })).toBeDisabled();
119
+ await openTaskActionsMenu();
120
+ expect(screen.getByRole('menuitem', { name: /^unlock$/i })).toBeDisabled();
98
121
  });
99
122
 
100
- it('enables unlock button when state is paused and canEdit is true', () => {
123
+ it('enables unlock overflow item when state is paused and canEdit is true', async () => {
101
124
  render(<TaskButtons {...defaultProps} canEdit state="paused" />);
102
- expect(
103
- screen.getByRole('button', { name: /^unlock$/i })
104
- ).not.toBeDisabled();
125
+ await openTaskActionsMenu();
126
+ expect(screen.getByRole('menuitem', { name: /^unlock$/i })).not.toBeDisabled();
105
127
  });
106
128
 
107
- it('disables force unlock button when state is stopped', () => {
129
+ it('disables force unlock overflow item when state is stopped', async () => {
108
130
  render(<TaskButtons {...defaultProps} canEdit state="stopped" />);
109
- expect(
110
- screen.getByRole('button', { name: /force unlock/i })
111
- ).toBeDisabled();
131
+ await openTaskActionsMenu();
132
+ expect(screen.getByRole('menuitem', { name: /force unlock/i })).toBeDisabled();
112
133
  });
113
134
 
114
- it('enables force unlock button when state is not stopped and canEdit is true', () => {
135
+ it('enables force unlock overflow item when state is not stopped and canEdit is true', async () => {
115
136
  render(<TaskButtons {...defaultProps} canEdit state="running" />);
116
- expect(
117
- screen.getByRole('button', { name: /force unlock/i })
118
- ).not.toBeDisabled();
137
+ await openTaskActionsMenu();
138
+ expect(screen.getByRole('menuitem', { name: /force unlock/i })).not.toBeDisabled();
119
139
  });
120
140
 
121
- it('renders parent task button when parentTask is provided', () => {
141
+ it('includes parent task overflow link when parentTask is provided', async () => {
122
142
  render(<TaskButtons {...defaultProps} parentTask="parent-123" />);
123
- const parentButton = screen.getByRole('link', { name: /parent task/i });
124
- expect(parentButton).toBeInTheDocument();
125
- expect(parentButton).toHaveAttribute(
126
- 'href',
127
- '/foreman_tasks/tasks/parent-123'
128
- );
143
+ await openTaskActionsMenu();
144
+ expect(
145
+ screen.getByRole('menuitem', { name: /parent task/i })
146
+ ).toHaveAttribute('href', '/foreman_tasks/tasks/parent-123');
129
147
  });
130
148
 
131
- it('does not render parent task button when parentTask is not provided', () => {
149
+ it('does not include parent overflow item when parentTask is not provided', async () => {
132
150
  render(<TaskButtons {...defaultProps} />);
151
+ await openTaskActionsMenu();
133
152
  expect(
134
- screen.queryByRole('link', { name: /parent task/i })
153
+ screen.queryByRole('menuitem', { name: /parent task/i })
135
154
  ).not.toBeInTheDocument();
136
155
  });
137
156
 
138
- it('renders sub tasks button when hasSubTasks is true', () => {
157
+ it('includes sub tasks overflow link when hasSubTasks is true', async () => {
139
158
  render(<TaskButtons {...defaultProps} hasSubTasks id="task-123" />);
140
- const subTasksButton = screen.getByRole('link', { name: /sub tasks/i });
141
- expect(subTasksButton).toBeInTheDocument();
142
- expect(subTasksButton).toHaveAttribute(
143
- 'href',
144
- '/foreman_tasks/tasks/task-123/sub_tasks'
145
- );
159
+ await openTaskActionsMenu();
160
+ expect(
161
+ screen.getByRole('menuitem', { name: /sub tasks/i })
162
+ ).toHaveAttribute('href', '/foreman_tasks/tasks/task-123/sub_tasks');
146
163
  });
147
164
 
148
- it('does not render sub tasks button when hasSubTasks is false', () => {
165
+ it('does not include sub tasks overflow item when hasSubTasks is false', async () => {
149
166
  render(<TaskButtons {...defaultProps} hasSubTasks={false} />);
167
+ await openTaskActionsMenu();
150
168
  expect(
151
- screen.queryByRole('link', { name: /sub tasks/i })
169
+ screen.queryByRole('menuitem', { name: /sub tasks/i })
152
170
  ).not.toBeInTheDocument();
153
171
  });
154
172
  });
173
+
155
174
  describe('user interactions', () => {
156
175
  const cancelTaskRequest = jest.fn();
157
176
  const resumeTaskRequest = jest.fn();
@@ -174,44 +193,41 @@ describe('TaskButtons', () => {
174
193
  state: 'paused',
175
194
  };
176
195
 
177
- it('calls taskProgressToggle when reload button is clicked', () => {
196
+ it('calls taskProgressToggle when overflow reload item is clicked', async () => {
178
197
  render(<TaskButtons {...props} />);
179
- const reloadButton = screen.getByRole('button', {
180
- name: /start auto-reloading/i,
181
- });
182
- fireEvent.click(reloadButton);
198
+ await openTaskActionsMenu();
199
+ fireEvent.click(
200
+ screen.getByRole('menuitem', { name: /start auto-reloading/i })
201
+ );
183
202
  expect(taskProgressToggle).toHaveBeenCalled();
184
203
  });
185
204
 
186
- it('calls taskReloadStart and resumeTaskRequest when resume button is clicked', () => {
205
+ it('calls taskReloadStart and resumeTaskRequest when resume menu item is clicked', async () => {
187
206
  render(<TaskButtons {...props} />);
188
- const resumeButton = screen.getByRole('button', { name: /resume/i });
189
- fireEvent.click(resumeButton);
207
+ await openTaskActionsMenu();
208
+ fireEvent.click(screen.getByRole('menuitem', { name: /^resume$/i }));
190
209
  expect(taskReloadStart).toHaveBeenCalledWith(id);
191
210
  expect(resumeTaskRequest).toHaveBeenCalledWith(id, action);
192
211
  });
193
212
 
194
- it('calls taskReloadStart and cancelTaskRequest when cancel button is clicked', () => {
213
+ it('calls taskReloadStart and cancelTaskRequest when cancel button is clicked', async () => {
195
214
  render(<TaskButtons {...props} />);
196
- const cancelButton = screen.getByRole('button', { name: /cancel/i });
197
- fireEvent.click(cancelButton);
215
+ await userEvent.click(screen.getByRole('button', { name: /cancel/i }));
198
216
  expect(taskReloadStart).toHaveBeenCalledWith(id);
199
217
  expect(cancelTaskRequest).toHaveBeenCalledWith(id, action);
200
218
  });
201
219
 
202
- it('calls setUnlockModalOpen when unlock button is clicked', () => {
220
+ it('calls setUnlockModalOpen when unlock menu item is clicked', async () => {
203
221
  render(<TaskButtons {...props} />);
204
- const unlockButton = screen.getByRole('button', { name: /^unlock$/i });
205
- fireEvent.click(unlockButton);
222
+ await openTaskActionsMenu();
223
+ fireEvent.click(screen.getByRole('menuitem', { name: /^unlock$/i }));
206
224
  expect(setUnlockModalOpen).toHaveBeenCalledWith(true);
207
225
  });
208
226
 
209
- it('calls setForceUnlockModalOpen when force unlock button is clicked', () => {
227
+ it('calls setForceUnlockModalOpen when force unlock menu item is clicked', async () => {
210
228
  render(<TaskButtons {...props} />);
211
- const forceUnlockButton = screen.getByRole('button', {
212
- name: /force unlock/i,
213
- });
214
- fireEvent.click(forceUnlockButton);
229
+ await openTaskActionsMenu();
230
+ fireEvent.click(screen.getByRole('menuitem', { name: /force unlock/i }));
215
231
  expect(setForceUnlockModalOpen).toHaveBeenCalledWith(true);
216
232
  });
217
233
  });
@@ -1,4 +1,9 @@
1
- import { durationInWords } from '../TaskHelper';
1
+ import {
2
+ durationInWords,
3
+ formatTaskDuration,
4
+ isDelayed,
5
+ parseUsernameLinkHref,
6
+ } from '../TaskHelper';
2
7
 
3
8
  describe('durationInWords', () => {
4
9
  it('should work for seconds', () => {
@@ -20,3 +25,115 @@ describe('durationInWords', () => {
20
25
  });
21
26
  });
22
27
  });
28
+
29
+ describe('isDelayed', () => {
30
+ it('returns false when startAt is missing', () => {
31
+ expect(isDelayed({ startAt: null, startedAt: '2020-01-02' })).toBe(false);
32
+ expect(isDelayed({ startAt: '', startedAt: '2020-01-02' })).toBe(false);
33
+ expect(isDelayed({ startedAt: '2020-01-02' })).toBe(false);
34
+ });
35
+
36
+ it('returns false when startedAt is missing', () => {
37
+ expect(isDelayed({ startAt: '2020-01-01', startedAt: null })).toBe(false);
38
+ expect(isDelayed({ startAt: '2020-01-01', startedAt: '' })).toBe(false);
39
+ expect(isDelayed({ startAt: '2020-01-01' })).toBe(false);
40
+ });
41
+
42
+ it('returns false when either date is invalid', () => {
43
+ expect(
44
+ isDelayed({ startAt: 'not-a-date', startedAt: '2020-01-02' })
45
+ ).toBe(false);
46
+ expect(
47
+ isDelayed({ startAt: '2020-01-01', startedAt: 'not-a-date' })
48
+ ).toBe(false);
49
+ });
50
+
51
+ it('returns false when startAt and startedAt are the same instant', () => {
52
+ expect(
53
+ isDelayed({ startAt: '2020-01-01T10:00:00', startedAt: '2020-01-01T10:00:00' })
54
+ ).toBe(false);
55
+ });
56
+
57
+ it('ignores millisecond differences when comparing startAt and startedAt', () => {
58
+ expect(
59
+ isDelayed({
60
+ startAt: '2020-01-01T10:00:00.123',
61
+ startedAt: '2020-01-01T10:00:00.456',
62
+ })
63
+ ).toBe(false);
64
+ });
65
+
66
+ it('returns true when startAt and startedAt differ', () => {
67
+ expect(
68
+ isDelayed({ startAt: '2020-01-01', startedAt: '2020-01-02' })
69
+ ).toBe(true);
70
+ });
71
+ });
72
+
73
+ describe('parseUsernameLinkHref', () => {
74
+ it('returns null for empty or non-string input', () => {
75
+ expect(parseUsernameLinkHref(null)).toBeNull();
76
+ expect(parseUsernameLinkHref(undefined)).toBeNull();
77
+ expect(parseUsernameLinkHref('')).toBeNull();
78
+ expect(parseUsernameLinkHref(123)).toBeNull();
79
+ });
80
+
81
+ it('extracts href from double-quoted attribute values', () => {
82
+ expect(
83
+ parseUsernameLinkHref('<a href="/users/bob">Bob</a>')
84
+ ).toBe('/users/bob');
85
+ });
86
+
87
+ it('extracts href from single-quoted attribute values', () => {
88
+ expect(
89
+ parseUsernameLinkHref("<a href='/users/bob'>Bob</a>")
90
+ ).toBe('/users/bob');
91
+ });
92
+
93
+ it('returns null when no href attribute is present', () => {
94
+ expect(parseUsernameLinkHref('<a>Bob</a>')).toBeNull();
95
+ });
96
+ });
97
+
98
+ describe('formatTaskDuration', () => {
99
+ it('returns N/A when the task is not stopped', () => {
100
+ expect(
101
+ formatTaskDuration('2020-01-01T10:00:00', '2020-01-01T10:00:01', 'running')
102
+ ).toBe('N/A');
103
+ });
104
+
105
+ it('returns N/A when start or end time is missing', () => {
106
+ expect(formatTaskDuration('', '2020-01-01T10:00:01', 'stopped')).toBe(
107
+ 'N/A'
108
+ );
109
+ expect(formatTaskDuration('2020-01-01T10:00:00', '', 'stopped')).toBe(
110
+ 'N/A'
111
+ );
112
+ });
113
+
114
+ it('returns N/A when dates are invalid or end precedes start', () => {
115
+ expect(
116
+ formatTaskDuration('not-a-date', '2020-01-01T10:00:01', 'stopped')
117
+ ).toBe('N/A');
118
+ expect(
119
+ formatTaskDuration('2020-01-01T10:00:00', 'not-a-date', 'stopped')
120
+ ).toBe('N/A');
121
+ expect(
122
+ formatTaskDuration(
123
+ '2020-01-01T10:00:01',
124
+ '2020-01-01T10:00:00',
125
+ 'stopped'
126
+ )
127
+ ).toBe('N/A');
128
+ });
129
+
130
+ it('returns a humanized duration for stopped tasks with valid timestamps', () => {
131
+ expect(
132
+ formatTaskDuration(
133
+ '1/1/1 10:00:00',
134
+ '1/1/1 10:00:01',
135
+ 'stopped'
136
+ )
137
+ ).toBe('1 second');
138
+ });
139
+ });
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
3
4
  import '@testing-library/jest-dom';
4
5
 
5
6
  jest.mock('foremanReact/components/common/dates/RelativeDateTime', () => {
@@ -10,41 +11,210 @@ jest.mock('foremanReact/components/common/dates/RelativeDateTime', () => {
10
11
  });
11
12
 
12
13
  import TaskInfo from '../TaskInfo';
14
+ import { durationInWords } from '../TaskHelper';
13
15
 
14
16
  describe('TaskInfo', () => {
15
- it('renders default metadata labels when given minimal props', () => {
16
- render(<TaskInfo id="test" />);
17
- expect(screen.getByText(/name:/i)).toBeInTheDocument();
18
- expect(screen.getByText(/start at:/i)).toBeInTheDocument();
19
- expect(screen.getByText(/result:/i)).toBeInTheDocument();
17
+ afterEach(() => {
18
+ jest.clearAllMocks();
20
19
  });
21
20
 
22
- it('renders task fields and troubleshooting when full props are provided', () => {
21
+ it('renders overview metadata headings for minimal props', () => {
22
+ render(<TaskInfo id="tid" />);
23
+
24
+ expect(screen.getByText('Result')).toBeInTheDocument();
25
+ expect(screen.getByText('State')).toBeInTheDocument();
26
+ expect(screen.getByText('Started at')).toBeInTheDocument();
27
+ expect(screen.getByText('Duration')).toBeInTheDocument();
28
+ expect(screen.getByText('Triggered by')).toBeInTheDocument();
29
+ expect(screen.getByText('Id')).toBeInTheDocument();
30
+
31
+ expect(
32
+ screen.getByRole('button', { name: /show more details/i })
33
+ ).toBeInTheDocument();
34
+ expect(screen.getByText('tid')).toBeInTheDocument();
35
+ expect(screen.getByText(/^Error$/)).toBeInTheDocument();
36
+ });
37
+
38
+ it('shows progress only while task state is not stopped', () => {
39
+ const props = {
40
+ id: 'r1',
41
+ state: 'running',
42
+ result: 'pending',
43
+ progress: 42,
44
+ startedAt: '',
45
+ usernamePath: '',
46
+ };
47
+
48
+ const { rerender } = render(<TaskInfo {...props} />);
49
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
50
+ expect(screen.getByRole('progressbar')).toHaveAttribute(
51
+ 'aria-valuenow',
52
+ '42'
53
+ );
54
+
55
+ rerender(<TaskInfo {...props} state="stopped" />);
56
+ expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
57
+ });
58
+
59
+ it('expands to show label, execution type, start at, ended at, and external id', async () => {
23
60
  render(
24
61
  <TaskInfo
25
- id="test"
26
- startAt="2019-06-17 16:04:09 +0300"
62
+ id="tid"
27
63
  label="Actions::Katello::EventQueue::Monitor"
28
64
  action="Monitor Event Queue"
29
- username="admin"
65
+ externalId="ext-42"
66
+ startAt="2019-06-17 16:04:09 +0300"
30
67
  startedAt="2019-06-17 16:04:09 +0300"
31
- endedAt={null}
68
+ endedAt=""
32
69
  state="paused"
33
70
  result="error"
34
- progress={0.5}
35
- help={
36
- "A paused task represents a process that has not finished properly. Any task in paused state can lead to potential inconsistency and needs to be resolved.\nThe recommended approach is to investigate the error messages below and in 'errors' tab, address the primary cause of the issue and resume the task."
37
- }
38
- output={{}}
71
+ progress={1}
72
+ username="admin"
39
73
  usernamePath=""
40
74
  />
41
75
  );
42
- expect(screen.getByText('Monitor Event Queue')).toBeInTheDocument();
43
- expect(screen.getByText(/troubleshooting/i)).toBeInTheDocument();
76
+
77
+ await userEvent.click(
78
+ screen.getByRole('button', { name: /show more details/i })
79
+ );
80
+
81
+ expect(screen.getByText('Execution type')).toBeInTheDocument();
82
+ expect(screen.getByText('Label')).toBeInTheDocument();
83
+ expect(screen.getByText('Start at')).toBeInTheDocument();
84
+ expect(screen.getByText('Ended at')).toBeInTheDocument();
85
+ expect(screen.getByText('External Id')).toBeInTheDocument();
86
+ expect(
87
+ screen.getByText('Actions::Katello::EventQueue::Monitor')
88
+ ).toBeInTheDocument();
89
+ expect(screen.getByText('ext-42')).toBeInTheDocument();
90
+ expect(screen.getByText(/^Error$/)).toBeInTheDocument();
91
+ expect(screen.getAllByText(/^Paused$/).length).toBeGreaterThanOrEqual(1);
92
+ expect(screen.getByText('1% Complete')).toBeInTheDocument();
93
+ });
94
+
95
+ it('reports delayed execution type when startAt and startedAt differ', async () => {
96
+ render(
97
+ <TaskInfo
98
+ id=""
99
+ state="stopped"
100
+ startAt="2020-01-01"
101
+ startedAt="2020-01-02"
102
+ />
103
+ );
104
+
105
+ await userEvent.click(
106
+ screen.getByRole('button', { name: /show more details/i })
107
+ );
108
+
109
+ expect(screen.getByText('Delayed')).toBeInTheDocument();
110
+ });
111
+
112
+ it('reports immediate execution type when startAt is missing', async () => {
113
+ render(<TaskInfo id="" state="stopped" startedAt="2020-01-02" />);
114
+
115
+ await userEvent.click(
116
+ screen.getByRole('button', { name: /show more details/i })
117
+ );
118
+
119
+ expect(screen.getByText('Immediate')).toBeInTheDocument();
120
+ });
121
+
122
+ it('renders triggered-by link when usernamePath embeds an href', () => {
123
+ render(
124
+ <TaskInfo
125
+ id=""
126
+ username="bob"
127
+ usernamePath='<a href="/users/bob">Bob</a>'
128
+ state=""
129
+ />
130
+ );
131
+
132
+ const link = screen.getByRole('link', { name: 'bob' });
133
+ expect(link).toHaveAttribute('href', '/users/bob');
134
+ });
135
+
136
+ it('renders triggered-by link when usernamePath uses single-quoted href', () => {
137
+ render(
138
+ <TaskInfo
139
+ id=""
140
+ username="bob"
141
+ usernamePath="<a href='/users/bob'>Bob</a>"
142
+ state=""
143
+ />
144
+ );
145
+
146
+ const link = screen.getByRole('link', { name: 'bob' });
147
+ expect(link).toHaveAttribute('href', '/users/bob');
148
+ });
149
+
150
+ it('formats stop duration when started and ended are valid', () => {
151
+ const startedAt = '2020-01-01T10:00:00.000Z';
152
+ const endedAt = '2020-01-01T10:00:45.000Z';
153
+
154
+ render(<TaskInfo id="" state="stopped" startedAt={startedAt} endedAt={endedAt} />);
155
+
44
156
  expect(
45
- screen.getByText(/a paused task represents a process/i)
157
+ screen.getByText(durationInWords(startedAt, endedAt).text)
46
158
  ).toBeInTheDocument();
47
- expect(screen.getByText(/^paused$/)).toBeInTheDocument();
48
- expect(screen.getByText(/^error$/)).toBeInTheDocument();
159
+ });
160
+
161
+ it('shows start before deadline when startBefore is set', async () => {
162
+ render(
163
+ <TaskInfo
164
+ id="tid"
165
+ state="stopped"
166
+ startAt="2020-01-01"
167
+ startBefore="2020-01-05"
168
+ />
169
+ );
170
+
171
+ await userEvent.click(
172
+ screen.getByRole('button', { name: /show more details/i })
173
+ );
174
+
175
+ expect(screen.getByText('2020-01-01')).toBeInTheDocument();
176
+ expect(screen.getByText(/before/i)).toBeInTheDocument();
177
+ expect(screen.getByText('2020-01-05')).toBeInTheDocument();
178
+ });
179
+
180
+ it('does not show start before text when startBefore is missing', async () => {
181
+ render(<TaskInfo id="tid" state="stopped" startAt="2020-01-01" />);
182
+
183
+ await userEvent.click(
184
+ screen.getByRole('button', { name: /show more details/i })
185
+ );
186
+
187
+ expect(screen.getByText('2020-01-01')).toBeInTheDocument();
188
+ expect(screen.queryByText(/before/i)).not.toBeInTheDocument();
189
+ });
190
+
191
+ it('shows troubleshooting help when expanded and help is provided', async () => {
192
+ render(
193
+ <TaskInfo
194
+ id="tid"
195
+ state="paused"
196
+ help="Investigate the error and <a href='/docs'>troubleshooting documentation</a>."
197
+ />
198
+ );
199
+
200
+ await userEvent.click(
201
+ screen.getByRole('button', { name: /show more details/i })
202
+ );
203
+
204
+ expect(screen.getByText('Troubleshooting')).toBeInTheDocument();
205
+ expect(screen.getByText(/Investigate the error/i)).toBeInTheDocument();
206
+ expect(
207
+ screen.getByRole('link', { name: 'troubleshooting documentation' })
208
+ ).toHaveAttribute('href', '/docs');
209
+ });
210
+
211
+ it('does not show troubleshooting help when help is empty', async () => {
212
+ render(<TaskInfo id="tid" state="paused" help="" />);
213
+
214
+ await userEvent.click(
215
+ screen.getByRole('button', { name: /show more details/i })
216
+ );
217
+
218
+ expect(screen.queryByText('Troubleshooting')).not.toBeInTheDocument();
49
219
  });
50
220
  });