foreman-tasks 12.2.4 → 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.
- checksums.yaml +4 -4
- data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
- data/config/routes.rb +3 -2
- data/lib/foreman_tasks/engine.rb +2 -2
- data/lib/foreman_tasks/version.rb +1 -1
- data/test/controllers/tasks_controller_test.rb +0 -9
- data/test/foreman_tasks_test_helper.rb +2 -2
- data/test/integration/tasks_test.rb +17 -0
- data/test/test_plugin_helper.rb +8 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +237 -177
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -35
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -69
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
- data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +76 -65
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -44
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -10
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +100 -1
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +226 -18
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
- data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +51 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +277 -0
- data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +13 -12
- data/webpack/Routes/routes.js +6 -0
- data/webpack/Routes/routes.test.js +16 -5
- data/webpack/index.js +0 -5
- metadata +16 -3
- data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen,
|
|
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
|
-
import { STATUS } from 'foremanReact/constants';
|
|
5
5
|
import { TaskButtons } from '../TaskButtons';
|
|
6
6
|
|
|
7
7
|
const setUnlockModalOpen = jest.fn();
|
|
@@ -17,29 +17,42 @@ const defaultProps = {
|
|
|
17
17
|
setForceUnlockModalOpen,
|
|
18
18
|
};
|
|
19
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
|
+
|
|
20
31
|
describe('TaskButtons', () => {
|
|
21
32
|
beforeEach(() => {
|
|
22
33
|
jest.clearAllMocks();
|
|
23
34
|
});
|
|
24
35
|
|
|
25
36
|
describe('rendering', () => {
|
|
26
|
-
it('
|
|
37
|
+
it('shows start auto-reloading in overflow when taskReload is false', async () => {
|
|
27
38
|
render(<TaskButtons {...defaultProps} taskReload={false} />);
|
|
39
|
+
await openTaskActionsMenu();
|
|
28
40
|
expect(
|
|
29
|
-
screen.getByRole('
|
|
41
|
+
screen.getByRole('menuitem', { name: /start auto-reloading/i })
|
|
30
42
|
).toBeInTheDocument();
|
|
31
43
|
expect(
|
|
32
|
-
screen.queryByRole('
|
|
44
|
+
screen.queryByRole('menuitem', { name: /stop auto-reloading/i })
|
|
33
45
|
).not.toBeInTheDocument();
|
|
34
46
|
});
|
|
35
47
|
|
|
36
|
-
it('
|
|
48
|
+
it('shows stop auto-reloading in overflow when taskReload is true', async () => {
|
|
37
49
|
render(<TaskButtons {...defaultProps} taskReload />);
|
|
50
|
+
await openTaskActionsMenu();
|
|
38
51
|
expect(
|
|
39
|
-
screen.getByRole('
|
|
52
|
+
screen.getByRole('menuitem', { name: /stop auto-reloading/i })
|
|
40
53
|
).toBeInTheDocument();
|
|
41
54
|
expect(
|
|
42
|
-
screen.queryByRole('
|
|
55
|
+
screen.queryByRole('menuitem', { name: /start auto-reloading/i })
|
|
43
56
|
).not.toBeInTheDocument();
|
|
44
57
|
});
|
|
45
58
|
|
|
@@ -55,15 +68,21 @@ describe('TaskButtons', () => {
|
|
|
55
68
|
expect(dynflowLink).toHaveAttribute('target', '_blank');
|
|
56
69
|
});
|
|
57
70
|
|
|
58
|
-
it('
|
|
59
|
-
render(
|
|
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
|
+
);
|
|
60
79
|
const dynflowLink = screen.getByRole('link', {
|
|
61
80
|
name: /dynflow console/i,
|
|
62
81
|
});
|
|
63
|
-
expect(dynflowLink).
|
|
82
|
+
expect(dynflowLink).toHaveAttribute('aria-disabled', 'true');
|
|
64
83
|
});
|
|
65
84
|
|
|
66
|
-
it('
|
|
85
|
+
it('does not disable dynflow console link when dynflowEnableConsole is true', () => {
|
|
67
86
|
render(
|
|
68
87
|
<TaskButtons
|
|
69
88
|
{...defaultProps}
|
|
@@ -74,18 +93,20 @@ describe('TaskButtons', () => {
|
|
|
74
93
|
const dynflowLink = screen.getByRole('link', {
|
|
75
94
|
name: /dynflow console/i,
|
|
76
95
|
});
|
|
77
|
-
expect(dynflowLink).not.
|
|
96
|
+
expect(dynflowLink).not.toHaveAttribute('aria-disabled', 'true');
|
|
78
97
|
});
|
|
79
98
|
|
|
80
|
-
it('disables resume and cancel
|
|
99
|
+
it('disables resume overflow item and cancel button when canEdit is false', async () => {
|
|
81
100
|
render(<TaskButtons {...defaultProps} canEdit={false} />);
|
|
82
|
-
expect(screen.getByRole('button', { name: /resume/i })).toBeDisabled();
|
|
83
101
|
expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled();
|
|
102
|
+
await openTaskActionsMenu();
|
|
103
|
+
expect(screen.getByRole('menuitem', { name: /^resume$/i })).toBeDisabled();
|
|
84
104
|
});
|
|
85
105
|
|
|
86
|
-
it('disables resume
|
|
106
|
+
it('disables resume overflow item when resumable is false', async () => {
|
|
87
107
|
render(<TaskButtons {...defaultProps} canEdit resumable={false} />);
|
|
88
|
-
|
|
108
|
+
await openTaskActionsMenu();
|
|
109
|
+
expect(screen.getByRole('menuitem', { name: /^resume$/i })).toBeDisabled();
|
|
89
110
|
});
|
|
90
111
|
|
|
91
112
|
it('disables cancel button when cancellable is false', () => {
|
|
@@ -93,66 +114,63 @@ describe('TaskButtons', () => {
|
|
|
93
114
|
expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled();
|
|
94
115
|
});
|
|
95
116
|
|
|
96
|
-
it('disables unlock
|
|
117
|
+
it('disables unlock overflow item when state is not paused', async () => {
|
|
97
118
|
render(<TaskButtons {...defaultProps} canEdit state="running" />);
|
|
98
|
-
|
|
119
|
+
await openTaskActionsMenu();
|
|
120
|
+
expect(screen.getByRole('menuitem', { name: /^unlock$/i })).toBeDisabled();
|
|
99
121
|
});
|
|
100
122
|
|
|
101
|
-
it('enables unlock
|
|
123
|
+
it('enables unlock overflow item when state is paused and canEdit is true', async () => {
|
|
102
124
|
render(<TaskButtons {...defaultProps} canEdit state="paused" />);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
).not.toBeDisabled();
|
|
125
|
+
await openTaskActionsMenu();
|
|
126
|
+
expect(screen.getByRole('menuitem', { name: /^unlock$/i })).not.toBeDisabled();
|
|
106
127
|
});
|
|
107
128
|
|
|
108
|
-
it('disables force unlock
|
|
129
|
+
it('disables force unlock overflow item when state is stopped', async () => {
|
|
109
130
|
render(<TaskButtons {...defaultProps} canEdit state="stopped" />);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
).toBeDisabled();
|
|
131
|
+
await openTaskActionsMenu();
|
|
132
|
+
expect(screen.getByRole('menuitem', { name: /force unlock/i })).toBeDisabled();
|
|
113
133
|
});
|
|
114
134
|
|
|
115
|
-
it('enables force unlock
|
|
135
|
+
it('enables force unlock overflow item when state is not stopped and canEdit is true', async () => {
|
|
116
136
|
render(<TaskButtons {...defaultProps} canEdit state="running" />);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
).not.toBeDisabled();
|
|
137
|
+
await openTaskActionsMenu();
|
|
138
|
+
expect(screen.getByRole('menuitem', { name: /force unlock/i })).not.toBeDisabled();
|
|
120
139
|
});
|
|
121
140
|
|
|
122
|
-
it('
|
|
141
|
+
it('includes parent task overflow link when parentTask is provided', async () => {
|
|
123
142
|
render(<TaskButtons {...defaultProps} parentTask="parent-123" />);
|
|
124
|
-
|
|
125
|
-
expect(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'/foreman_tasks/tasks/parent-123'
|
|
129
|
-
);
|
|
143
|
+
await openTaskActionsMenu();
|
|
144
|
+
expect(
|
|
145
|
+
screen.getByRole('menuitem', { name: /parent task/i })
|
|
146
|
+
).toHaveAttribute('href', '/foreman_tasks/tasks/parent-123');
|
|
130
147
|
});
|
|
131
148
|
|
|
132
|
-
it('does not
|
|
149
|
+
it('does not include parent overflow item when parentTask is not provided', async () => {
|
|
133
150
|
render(<TaskButtons {...defaultProps} />);
|
|
151
|
+
await openTaskActionsMenu();
|
|
134
152
|
expect(
|
|
135
|
-
screen.queryByRole('
|
|
153
|
+
screen.queryByRole('menuitem', { name: /parent task/i })
|
|
136
154
|
).not.toBeInTheDocument();
|
|
137
155
|
});
|
|
138
156
|
|
|
139
|
-
it('
|
|
157
|
+
it('includes sub tasks overflow link when hasSubTasks is true', async () => {
|
|
140
158
|
render(<TaskButtons {...defaultProps} hasSubTasks id="task-123" />);
|
|
141
|
-
|
|
142
|
-
expect(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
'/foreman_tasks/tasks/task-123/sub_tasks'
|
|
146
|
-
);
|
|
159
|
+
await openTaskActionsMenu();
|
|
160
|
+
expect(
|
|
161
|
+
screen.getByRole('menuitem', { name: /sub tasks/i })
|
|
162
|
+
).toHaveAttribute('href', '/foreman_tasks/tasks/task-123/sub_tasks');
|
|
147
163
|
});
|
|
148
164
|
|
|
149
|
-
it('does not
|
|
165
|
+
it('does not include sub tasks overflow item when hasSubTasks is false', async () => {
|
|
150
166
|
render(<TaskButtons {...defaultProps} hasSubTasks={false} />);
|
|
167
|
+
await openTaskActionsMenu();
|
|
151
168
|
expect(
|
|
152
|
-
screen.queryByRole('
|
|
169
|
+
screen.queryByRole('menuitem', { name: /sub tasks/i })
|
|
153
170
|
).not.toBeInTheDocument();
|
|
154
171
|
});
|
|
155
172
|
});
|
|
173
|
+
|
|
156
174
|
describe('user interactions', () => {
|
|
157
175
|
const cancelTaskRequest = jest.fn();
|
|
158
176
|
const resumeTaskRequest = jest.fn();
|
|
@@ -169,51 +187,47 @@ describe('TaskButtons', () => {
|
|
|
169
187
|
resumeTaskRequest,
|
|
170
188
|
taskProgressToggle,
|
|
171
189
|
taskReloadStart,
|
|
172
|
-
status: STATUS.RESOLVED,
|
|
173
190
|
canEdit: true,
|
|
174
191
|
resumable: true,
|
|
175
192
|
cancellable: true,
|
|
176
193
|
state: 'paused',
|
|
177
194
|
};
|
|
178
195
|
|
|
179
|
-
it('calls taskProgressToggle when reload
|
|
196
|
+
it('calls taskProgressToggle when overflow reload item is clicked', async () => {
|
|
180
197
|
render(<TaskButtons {...props} />);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
await openTaskActionsMenu();
|
|
199
|
+
fireEvent.click(
|
|
200
|
+
screen.getByRole('menuitem', { name: /start auto-reloading/i })
|
|
201
|
+
);
|
|
185
202
|
expect(taskProgressToggle).toHaveBeenCalled();
|
|
186
203
|
});
|
|
187
204
|
|
|
188
|
-
it('calls taskReloadStart and resumeTaskRequest when resume
|
|
205
|
+
it('calls taskReloadStart and resumeTaskRequest when resume menu item is clicked', async () => {
|
|
189
206
|
render(<TaskButtons {...props} />);
|
|
190
|
-
|
|
191
|
-
fireEvent.click(
|
|
207
|
+
await openTaskActionsMenu();
|
|
208
|
+
fireEvent.click(screen.getByRole('menuitem', { name: /^resume$/i }));
|
|
192
209
|
expect(taskReloadStart).toHaveBeenCalledWith(id);
|
|
193
210
|
expect(resumeTaskRequest).toHaveBeenCalledWith(id, action);
|
|
194
211
|
});
|
|
195
212
|
|
|
196
|
-
it('calls taskReloadStart and cancelTaskRequest when cancel button is clicked', () => {
|
|
213
|
+
it('calls taskReloadStart and cancelTaskRequest when cancel button is clicked', async () => {
|
|
197
214
|
render(<TaskButtons {...props} />);
|
|
198
|
-
|
|
199
|
-
fireEvent.click(cancelButton);
|
|
215
|
+
await userEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
|
200
216
|
expect(taskReloadStart).toHaveBeenCalledWith(id);
|
|
201
217
|
expect(cancelTaskRequest).toHaveBeenCalledWith(id, action);
|
|
202
218
|
});
|
|
203
219
|
|
|
204
|
-
it('calls setUnlockModalOpen when unlock
|
|
220
|
+
it('calls setUnlockModalOpen when unlock menu item is clicked', async () => {
|
|
205
221
|
render(<TaskButtons {...props} />);
|
|
206
|
-
|
|
207
|
-
fireEvent.click(
|
|
222
|
+
await openTaskActionsMenu();
|
|
223
|
+
fireEvent.click(screen.getByRole('menuitem', { name: /^unlock$/i }));
|
|
208
224
|
expect(setUnlockModalOpen).toHaveBeenCalledWith(true);
|
|
209
225
|
});
|
|
210
226
|
|
|
211
|
-
it('calls setForceUnlockModalOpen when force unlock
|
|
227
|
+
it('calls setForceUnlockModalOpen when force unlock menu item is clicked', async () => {
|
|
212
228
|
render(<TaskButtons {...props} />);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
});
|
|
216
|
-
fireEvent.click(forceUnlockButton);
|
|
229
|
+
await openTaskActionsMenu();
|
|
230
|
+
fireEvent.click(screen.getByRole('menuitem', { name: /force unlock/i }));
|
|
217
231
|
expect(setForceUnlockModalOpen).toHaveBeenCalledWith(true);
|
|
218
232
|
});
|
|
219
233
|
});
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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="
|
|
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
|
-
|
|
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=
|
|
68
|
+
endedAt=""
|
|
32
69
|
state="paused"
|
|
33
70
|
result="error"
|
|
34
|
-
progress={
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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(
|
|
157
|
+
screen.getByText(durationInWords(startedAt, endedAt).text)
|
|
46
158
|
).toBeInTheDocument();
|
|
47
|
-
|
|
48
|
-
|
|
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
|
});
|