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.
- checksums.yaml +4 -4
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
- data/lib/foreman_tasks/version.rb +1 -1
- 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 +236 -131
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
- 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 +48 -51
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +0 -39
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +0 -5
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
- 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 +10 -33
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
- data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
- data/webpack/Routes/routes.js +1 -1
- data/webpack/Routes/routes.test.js +1 -1
- data/webpack/index.js +0 -5
- metadata +8 -2
|
@@ -1,5 +1,6 @@
|
|
|
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
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('
|
|
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('
|
|
41
|
+
screen.getByRole('menuitem', { name: /start auto-reloading/i })
|
|
29
42
|
).toBeInTheDocument();
|
|
30
43
|
expect(
|
|
31
|
-
screen.queryByRole('
|
|
44
|
+
screen.queryByRole('menuitem', { name: /stop auto-reloading/i })
|
|
32
45
|
).not.toBeInTheDocument();
|
|
33
46
|
});
|
|
34
47
|
|
|
35
|
-
it('
|
|
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('
|
|
52
|
+
screen.getByRole('menuitem', { name: /stop auto-reloading/i })
|
|
39
53
|
).toBeInTheDocument();
|
|
40
54
|
expect(
|
|
41
|
-
screen.queryByRole('
|
|
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('
|
|
58
|
-
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
|
+
);
|
|
59
79
|
const dynflowLink = screen.getByRole('link', {
|
|
60
80
|
name: /dynflow console/i,
|
|
61
81
|
});
|
|
62
|
-
expect(dynflowLink).
|
|
82
|
+
expect(dynflowLink).toHaveAttribute('aria-disabled', 'true');
|
|
63
83
|
});
|
|
64
84
|
|
|
65
|
-
it('
|
|
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.
|
|
96
|
+
expect(dynflowLink).not.toHaveAttribute('aria-disabled', 'true');
|
|
77
97
|
});
|
|
78
98
|
|
|
79
|
-
it('disables resume and cancel
|
|
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
|
|
106
|
+
it('disables resume overflow item when resumable is false', async () => {
|
|
86
107
|
render(<TaskButtons {...defaultProps} canEdit resumable={false} />);
|
|
87
|
-
|
|
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
|
|
117
|
+
it('disables unlock overflow item when state is not paused', async () => {
|
|
96
118
|
render(<TaskButtons {...defaultProps} canEdit state="running" />);
|
|
97
|
-
|
|
119
|
+
await openTaskActionsMenu();
|
|
120
|
+
expect(screen.getByRole('menuitem', { name: /^unlock$/i })).toBeDisabled();
|
|
98
121
|
});
|
|
99
122
|
|
|
100
|
-
it('enables unlock
|
|
123
|
+
it('enables unlock overflow item when state is paused and canEdit is true', async () => {
|
|
101
124
|
render(<TaskButtons {...defaultProps} canEdit state="paused" />);
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
129
|
+
it('disables force unlock overflow item when state is stopped', async () => {
|
|
108
130
|
render(<TaskButtons {...defaultProps} canEdit state="stopped" />);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
).toBeDisabled();
|
|
131
|
+
await openTaskActionsMenu();
|
|
132
|
+
expect(screen.getByRole('menuitem', { name: /force unlock/i })).toBeDisabled();
|
|
112
133
|
});
|
|
113
134
|
|
|
114
|
-
it('enables force unlock
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
).not.toBeDisabled();
|
|
137
|
+
await openTaskActionsMenu();
|
|
138
|
+
expect(screen.getByRole('menuitem', { name: /force unlock/i })).not.toBeDisabled();
|
|
119
139
|
});
|
|
120
140
|
|
|
121
|
-
it('
|
|
141
|
+
it('includes parent task overflow link when parentTask is provided', async () => {
|
|
122
142
|
render(<TaskButtons {...defaultProps} parentTask="parent-123" />);
|
|
123
|
-
|
|
124
|
-
expect(
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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('
|
|
153
|
+
screen.queryByRole('menuitem', { name: /parent task/i })
|
|
135
154
|
).not.toBeInTheDocument();
|
|
136
155
|
});
|
|
137
156
|
|
|
138
|
-
it('
|
|
157
|
+
it('includes sub tasks overflow link when hasSubTasks is true', async () => {
|
|
139
158
|
render(<TaskButtons {...defaultProps} hasSubTasks id="task-123" />);
|
|
140
|
-
|
|
141
|
-
expect(
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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('
|
|
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
|
|
196
|
+
it('calls taskProgressToggle when overflow reload item is clicked', async () => {
|
|
178
197
|
render(<TaskButtons {...props} />);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
205
|
+
it('calls taskReloadStart and resumeTaskRequest when resume menu item is clicked', async () => {
|
|
187
206
|
render(<TaskButtons {...props} />);
|
|
188
|
-
|
|
189
|
-
fireEvent.click(
|
|
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
|
-
|
|
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
|
|
220
|
+
it('calls setUnlockModalOpen when unlock menu item is clicked', async () => {
|
|
203
221
|
render(<TaskButtons {...props} />);
|
|
204
|
-
|
|
205
|
-
fireEvent.click(
|
|
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
|
|
227
|
+
it('calls setForceUnlockModalOpen when force unlock menu item is clicked', async () => {
|
|
210
228
|
render(<TaskButtons {...props} />);
|
|
211
|
-
|
|
212
|
-
|
|
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 {
|
|
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
|
});
|