foreman-tasks 11.0.7 → 12.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/views/foreman_tasks/api/tasks/dependency_summary.json.rabl +4 -0
  3. data/app/views/foreman_tasks/api/tasks/details.json.rabl +20 -0
  4. data/lib/foreman_tasks/engine.rb +6 -1
  5. data/lib/foreman_tasks/triggers.rb +4 -0
  6. data/lib/foreman_tasks/version.rb +1 -1
  7. data/lib/foreman_tasks.rb +24 -0
  8. data/test/unit/chaining_test.rb +62 -0
  9. data/webpack/ForemanTasks/Components/TaskDetails/Components/Dependencies.js +93 -0
  10. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +7 -12
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +40 -20
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Dependencies.test.js +92 -0
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +2 -3
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Task.test.js.snap +90 -112
  15. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +13 -1
  16. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +6 -0
  17. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetails.test.js.snap +19 -0
  18. data/webpack/ForemanTasks/Components/TaskDetails/index.js +4 -0
  19. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/OtherInfo.js +5 -3
  20. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/OtherInfo.test.js +58 -9
  21. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.scss +0 -30
  22. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.test.js +190 -9
  23. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCardTable.js +70 -35
  24. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.js +9 -14
  25. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.scss +3 -23
  26. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.test.js +33 -26
  27. data/webpack/ForemanTasks/Components/TasksTable/Components/ActionSelectButton.js +31 -30
  28. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/ActionSelectButton.test.js +78 -11
  29. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.js +40 -21
  30. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +64 -62
  31. metadata +6 -7
  32. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCardTable.test.js +0 -54
  33. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/OtherInfo.test.js.snap +0 -30
  34. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/StoppedTasksCard.test.js.snap +0 -107
  35. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/StoppedTasksCardTable.test.js.snap +0 -960
  36. data/webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/__snapshots__/TasksLabelsRow.test.js.snap +0 -47
  37. data/webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/ActionSelectButton.test.js.snap +0 -43
@@ -15,59 +15,48 @@ exports[`Task rendering render with minimal Props 1`] = `
15
15
  selectedRowsLen={1}
16
16
  setModalClosed={[Function]}
17
17
  />
18
- <Grid
19
- bsClass="container"
20
- componentClass="div"
21
- fluid={false}
22
- >
23
- <Row
24
- bsClass="row"
25
- componentClass="div"
26
- >
27
- <TaskButtons
28
- action=""
29
- canEdit={false}
30
- cancelTaskRequest={[Function]}
31
- cancellable={false}
32
- dynflowEnableConsole={false}
33
- externalId=""
34
- forceCancelTaskRequest={[Function]}
35
- hasSubTasks={false}
36
- id="test"
37
- parentTask=""
38
- resumable={false}
39
- resumeTaskRequest={[Function]}
40
- setForceUnlockModalOpen={[Function]}
41
- setUnlockModalOpen={[Function]}
42
- state=""
43
- taskProgressToggle={[MockFunction]}
44
- taskReload={false}
45
- taskReloadStart={[MockFunction]}
46
- unlockTaskRequest={[Function]}
47
- />
48
- </Row>
49
- <TaskInfo
50
- action=""
51
- endedAt=""
52
- errors={Array []}
53
- forceCancelTaskRequest={[Function]}
54
- help=""
55
- id="test"
56
- output=""
57
- progress={0}
58
- result="error"
59
- startAt=""
60
- startBefore=""
61
- startedAt=""
62
- state=""
63
- taskProgressToggle={[MockFunction]}
64
- taskReload={false}
65
- taskReloadStart={[MockFunction]}
66
- unlockTaskRequest={[Function]}
67
- username=""
68
- usernamePath=""
69
- />
70
- </Grid>
18
+ <TaskButtons
19
+ action=""
20
+ canEdit={false}
21
+ cancelTaskRequest={[Function]}
22
+ cancellable={false}
23
+ dynflowEnableConsole={false}
24
+ externalId=""
25
+ forceCancelTaskRequest={[Function]}
26
+ hasSubTasks={false}
27
+ id="test"
28
+ parentTask=""
29
+ resumable={false}
30
+ resumeTaskRequest={[Function]}
31
+ setForceUnlockModalOpen={[Function]}
32
+ setUnlockModalOpen={[Function]}
33
+ state=""
34
+ taskProgressToggle={[MockFunction]}
35
+ taskReload={false}
36
+ taskReloadStart={[MockFunction]}
37
+ unlockTaskRequest={[Function]}
38
+ />
39
+ <TaskInfo
40
+ action=""
41
+ endedAt=""
42
+ errors={Array []}
43
+ forceCancelTaskRequest={[Function]}
44
+ help=""
45
+ id="test"
46
+ output=""
47
+ progress={0}
48
+ result="error"
49
+ startAt=""
50
+ startBefore=""
51
+ startedAt=""
52
+ state=""
53
+ taskProgressToggle={[MockFunction]}
54
+ taskReload={false}
55
+ taskReloadStart={[MockFunction]}
56
+ unlockTaskRequest={[Function]}
57
+ username=""
58
+ usernamePath=""
59
+ />
71
60
  </Fragment>
72
61
  `;
73
62
 
@@ -86,64 +75,53 @@ exports[`Task rendering render with some Props 1`] = `
86
75
  selectedRowsLen={1}
87
76
  setModalClosed={[Function]}
88
77
  />
89
- <Grid
90
- bsClass="container"
91
- componentClass="div"
92
- fluid={false}
93
- >
94
- <Row
95
- bsClass="row"
96
- componentClass="div"
97
- >
98
- <TaskButtons
99
- action=""
100
- canEdit={true}
101
- cancelTaskRequest={[Function]}
102
- cancellable={false}
103
- dynflowEnableConsole={true}
104
- externalId=""
105
- forceCancelTaskRequest={[Function]}
106
- hasSubTasks={true}
107
- id="test"
108
- parentTask="parent-id"
109
- resumable={false}
110
- resumeTaskRequest={[Function]}
111
- setForceUnlockModalOpen={[Function]}
112
- setUnlockModalOpen={[Function]}
113
- state="paused"
114
- status="RESOLVED"
115
- taskProgressToggle={[MockFunction]}
116
- taskReload={true}
117
- taskReloadStart={[MockFunction]}
118
- unlockTaskRequest={[Function]}
119
- />
120
- </Row>
121
- <TaskInfo
122
- action=""
123
- canEdit={true}
124
- dynflowEnableConsole={true}
125
- endedAt=""
126
- errors={Array []}
127
- forceCancelTaskRequest={[Function]}
128
- hasSubTasks={true}
129
- help=""
130
- id="test"
131
- output=""
132
- parentTask="parent-id"
133
- progress={0}
134
- result="error"
135
- startAt=""
136
- startBefore=""
137
- startedAt=""
138
- state="paused"
139
- status="RESOLVED"
140
- taskProgressToggle={[MockFunction]}
141
- taskReload={true}
142
- taskReloadStart={[MockFunction]}
143
- unlockTaskRequest={[Function]}
144
- username=""
145
- usernamePath=""
146
- />
147
- </Grid>
78
+ <TaskButtons
79
+ action=""
80
+ canEdit={true}
81
+ cancelTaskRequest={[Function]}
82
+ cancellable={false}
83
+ dynflowEnableConsole={true}
84
+ externalId=""
85
+ forceCancelTaskRequest={[Function]}
86
+ hasSubTasks={true}
87
+ id="test"
88
+ parentTask="parent-id"
89
+ resumable={false}
90
+ resumeTaskRequest={[Function]}
91
+ setForceUnlockModalOpen={[Function]}
92
+ setUnlockModalOpen={[Function]}
93
+ state="paused"
94
+ status="RESOLVED"
95
+ taskProgressToggle={[MockFunction]}
96
+ taskReload={true}
97
+ taskReloadStart={[MockFunction]}
98
+ unlockTaskRequest={[Function]}
99
+ />
100
+ <TaskInfo
101
+ action=""
102
+ canEdit={true}
103
+ dynflowEnableConsole={true}
104
+ endedAt=""
105
+ errors={Array []}
106
+ forceCancelTaskRequest={[Function]}
107
+ hasSubTasks={true}
108
+ help=""
109
+ id="test"
110
+ output=""
111
+ parentTask="parent-id"
112
+ progress={0}
113
+ result="error"
114
+ startAt=""
115
+ startBefore=""
116
+ startedAt=""
117
+ state="paused"
118
+ status="RESOLVED"
119
+ taskProgressToggle={[MockFunction]}
120
+ taskReload={true}
121
+ taskReloadStart={[MockFunction]}
122
+ unlockTaskRequest={[Function]}
123
+ username=""
124
+ usernamePath=""
125
+ />
148
126
  </Fragment>
149
127
  `;
@@ -9,6 +9,7 @@ import RunningSteps from './Components/RunningSteps';
9
9
  import Errors from './Components/Errors';
10
10
  import Locks from './Components/Locks';
11
11
  import Raw from './Components/Raw';
12
+ import Dependencies from './Components/Dependencies';
12
13
  import { getTaskID } from './TasksDetailsHelper';
13
14
  import { TaskSkeleton } from './Components/TaskSkeleton';
14
15
 
@@ -20,6 +21,8 @@ const TaskDetails = ({
20
21
  runningSteps,
21
22
  locks,
22
23
  links,
24
+ dependsOn,
25
+ blocks,
23
26
  cancelStep,
24
27
  taskReloadStart,
25
28
  taskReloadStop,
@@ -90,7 +93,10 @@ const TaskDetails = ({
90
93
  <Tab eventKey={4} disabled={isLoading} title={__('Locks')}>
91
94
  <Locks locks={locks.concat(links)} />
92
95
  </Tab>
93
- <Tab eventKey={5} disabled={isLoading} title={__('Raw')}>
96
+ <Tab eventKey={5} disabled={isLoading} title={__('Dependencies')}>
97
+ <Dependencies dependsOn={dependsOn} blocks={blocks} />
98
+ </Tab>
99
+ <Tab eventKey={6} disabled={isLoading} title={__('Raw')}>
94
100
  <Raw
95
101
  id={id}
96
102
  label={props.label}
@@ -116,9 +122,12 @@ TaskDetails.propTypes = {
116
122
  taskReloadStop: PropTypes.func.isRequired,
117
123
  taskReloadStart: PropTypes.func.isRequired,
118
124
  links: PropTypes.array,
125
+ dependsOn: PropTypes.array,
126
+ blocks: PropTypes.array,
119
127
  ...Task.propTypes,
120
128
  ...Errors.propTypes,
121
129
  ...Locks.propTypes,
130
+ ...Dependencies.propTypes,
122
131
  ...Raw.propTypes,
123
132
  };
124
133
  TaskDetails.defaultProps = {
@@ -127,10 +136,13 @@ TaskDetails.defaultProps = {
127
136
  APIerror: null,
128
137
  status: STATUS.PENDING,
129
138
  links: [],
139
+ dependsOn: [],
140
+ blocks: [],
130
141
  ...Task.defaultProps,
131
142
  ...RunningSteps.defaultProps,
132
143
  ...Errors.defaultProps,
133
144
  ...Locks.defaultProps,
145
+ ...Dependencies.defaultProps,
134
146
  ...Raw.defaultProps,
135
147
  };
136
148
 
@@ -109,3 +109,9 @@ export const selectAPIError = state =>
109
109
  export const selectIsLoading = state =>
110
110
  !!selectAPIByKey(state, FOREMAN_TASK_DETAILS).response &&
111
111
  selectStatus(state) === STATUS.PENDING;
112
+
113
+ export const selectDependsOn = state =>
114
+ selectTaskDetailsResponse(state).depends_on || [];
115
+
116
+ export const selectBlocks = state =>
117
+ selectTaskDetailsResponse(state).blocks || [];
@@ -58,6 +58,16 @@ exports[`TaskDetails rendering render with loading Props 1`] = `
58
58
  <Tab
59
59
  disabled={true}
60
60
  eventKey={5}
61
+ title="Dependencies"
62
+ >
63
+ <Dependencies
64
+ blocks={Array []}
65
+ dependsOn={Array []}
66
+ />
67
+ </Tab>
68
+ <Tab
69
+ disabled={true}
70
+ eventKey={6}
61
71
  title="Raw"
62
72
  >
63
73
  <Raw
@@ -136,6 +146,15 @@ exports[`TaskDetails rendering render with min Props 1`] = `
136
146
  </Tab>
137
147
  <Tab
138
148
  eventKey={5}
149
+ title="Dependencies"
150
+ >
151
+ <Dependencies
152
+ blocks={Array []}
153
+ dependsOn={Array []}
154
+ />
155
+ </Tab>
156
+ <Tab
157
+ eventKey={6}
139
158
  title="Raw"
140
159
  >
141
160
  <Raw
@@ -36,6 +36,8 @@ import {
36
36
  selectStatus,
37
37
  selectAPIError,
38
38
  selectIsLoading,
39
+ selectDependsOn,
40
+ selectBlocks,
39
41
  } from './TaskDetailsSelectors';
40
42
 
41
43
  const mapStateToProps = state => ({
@@ -71,6 +73,8 @@ const mapStateToProps = state => ({
71
73
  status: selectStatus(state),
72
74
  APIerror: selectAPIError(state),
73
75
  isLoading: selectIsLoading(state),
76
+ dependsOn: selectDependsOn(state),
77
+ blocks: selectBlocks(state),
74
78
  });
75
79
 
76
80
  const mapDispatchToProps = dispatch =>
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import classNames from 'classnames';
4
4
 
5
- import { Button, Tooltip, TooltipPosition } from '@patternfly/react-core';
5
+ import { Button, Tooltip, TooltipPosition, Icon } from '@patternfly/react-core';
6
6
  import { InfoCircleIcon } from '@patternfly/react-icons';
7
7
  import { translate as __ } from 'foremanReact/common/I18n';
8
8
  import {
@@ -25,8 +25,10 @@ export const OtherInfo = ({ updateQuery, otherCount, query }) => {
25
25
  position={TooltipPosition.bottom}
26
26
  >
27
27
  <span>
28
- <InfoCircleIcon className="pficon" />
29
- <span>{__('Other:')} </span>
28
+ <Icon isInline>
29
+ <InfoCircleIcon />
30
+ </Icon>{' '}
31
+ {__('Other:')}
30
32
  </span>
31
33
  </Tooltip>
32
34
  <Button
@@ -1,14 +1,63 @@
1
- import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
2
4
 
3
5
  import { OtherInfo } from './OtherInfo';
6
+ import {
7
+ TASKS_DASHBOARD_AVAILABLE_QUERY_STATES,
8
+ TASKS_DASHBOARD_AVAILABLE_QUERY_RESULTS,
9
+ } from '../../../../TasksDashboardConstants';
4
10
 
5
- const fixtures = {
6
- render: {
7
- updateQuery: jest.fn,
8
- otherCount: 7,
9
- query: { state: 'STOPPED', result: 'OTHER' },
10
- },
11
+ jest.mock('foremanReact/common/I18n', () => ({
12
+ translate: str => str,
13
+ }));
14
+
15
+ const { STOPPED } = TASKS_DASHBOARD_AVAILABLE_QUERY_STATES;
16
+ const { OTHER } = TASKS_DASHBOARD_AVAILABLE_QUERY_RESULTS;
17
+
18
+ const defaultProps = {
19
+ updateQuery: jest.fn(),
20
+ otherCount: 7,
21
+ query: { state: STOPPED, result: OTHER },
11
22
  };
12
23
 
13
- describe('OtherInfo', () =>
14
- testComponentSnapshotsWithFixtures(OtherInfo, fixtures));
24
+ describe('OtherInfo', () => {
25
+ beforeEach(() => {
26
+ defaultProps.updateQuery.mockClear();
27
+ });
28
+
29
+ it('renders Other label, count', () => {
30
+ render(<OtherInfo {...defaultProps} />);
31
+ expect(screen.getByText('Other:')).toBeInTheDocument();
32
+ expect(screen.getByText('7')).toBeInTheDocument();
33
+ });
34
+
35
+ it('calls updateQuery with STOPPED and OTHER when the count button is clicked', () => {
36
+ render(<OtherInfo {...defaultProps} />);
37
+ const button = screen.getByRole('button', { name: '7' });
38
+ fireEvent.click(button);
39
+ expect(defaultProps.updateQuery).toHaveBeenCalledWith({
40
+ state: STOPPED,
41
+ result: OTHER,
42
+ });
43
+ });
44
+
45
+ it('applies other-active class when query state is STOPPED and result is OTHER', () => {
46
+ const { container } = render(
47
+ <OtherInfo {...defaultProps} query={{ state: STOPPED, result: OTHER }} />
48
+ );
49
+ const wrapper = container.querySelector('.other-active');
50
+ expect(wrapper).toBeInTheDocument();
51
+ });
52
+
53
+ it('does not apply other-active class when query result is not OTHER', () => {
54
+ const { container } = render(
55
+ <OtherInfo
56
+ {...defaultProps}
57
+ query={{ state: STOPPED, result: 'error' }}
58
+ />
59
+ );
60
+ const wrapper = container.querySelector('.other-active');
61
+ expect(wrapper).not.toBeInTheDocument();
62
+ });
63
+ });
@@ -1,19 +1,5 @@
1
1
  .stopped-tasks-card {
2
2
  .stopped-table {
3
- width: 100%;
4
- border: none;
5
- table-layout: fixed;
6
-
7
- thead {
8
- background-color: transparent;
9
- background-image: none;
10
- tr {
11
- th {
12
- font-weight: bold;
13
- border: none;
14
- }
15
- }
16
- }
17
3
 
18
4
  td,
19
5
  th {
@@ -29,12 +15,6 @@
29
15
  td {
30
16
  &.active {
31
17
  font-weight: bold;
32
- }
33
- }
34
- .data-col {
35
- &:hover {
36
- cursor: pointer;
37
- border: 1px solid #d1d1d1;
38
18
  background-color: #def3fe;
39
19
  }
40
20
  }
@@ -43,14 +23,4 @@
43
23
  .other-active {
44
24
  text-decoration: underline;
45
25
  }
46
-
47
- .pficon {
48
- margin-right: 5px;
49
- }
50
-
51
- .btn-link {
52
- font-size: 14px;
53
- padding-top: 0;
54
- padding-bottom: 0;
55
- }
56
26
  }
@@ -1,15 +1,196 @@
1
- import { testComponentSnapshotsWithFixtures } from '@theforeman/test';
2
- import { TASKS_DASHBOARD_AVAILABLE_QUERY_STATES } from '../../../../TasksDashboardConstants';
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+
5
+ import {
6
+ TASKS_DASHBOARD_AVAILABLE_QUERY_STATES,
7
+ TASKS_DASHBOARD_AVAILABLE_QUERY_MODES,
8
+ TASKS_DASHBOARD_AVAILABLE_TIMES,
9
+ } from '../../../../TasksDashboardConstants';
3
10
  import StoppedTasksCard from './StoppedTasksCard';
4
11
 
5
- const { STOPPED } = TASKS_DASHBOARD_AVAILABLE_QUERY_STATES;
12
+ const { STOPPED, RUNNING } = TASKS_DASHBOARD_AVAILABLE_QUERY_STATES;
13
+ const { LAST } = TASKS_DASHBOARD_AVAILABLE_QUERY_MODES;
14
+ const { WEEK } = TASKS_DASHBOARD_AVAILABLE_TIMES;
6
15
 
7
- const fixtures = {
8
- 'render with minimal props': {},
9
- 'render selected': {
10
- query: { state: STOPPED },
16
+ const defaultTableData = {
17
+ results: {
18
+ error: { total: 9, last: 1 },
19
+ warning: { total: 8, last: 2 },
20
+ success: { total: 7, last: 3 },
11
21
  },
22
+ other: 0,
12
23
  };
13
24
 
14
- describe('StoppedTasksCard', () =>
15
- testComponentSnapshotsWithFixtures(StoppedTasksCard, fixtures));
25
+ describe('StoppedTasksCard', () => {
26
+ describe('rendering', () => {
27
+ it('renders card with base classes and id', () => {
28
+ const { container } = render(<StoppedTasksCard />);
29
+ const card = container.querySelector('#stopped-tasks-card');
30
+ expect(screen.getByText('Stopped')).toBeInTheDocument();
31
+ expect(card).toBeInTheDocument();
32
+ expect(card).toHaveClass('tasks-donut-card');
33
+ expect(card).toHaveClass('stopped-tasks-card');
34
+ });
35
+
36
+ it('renders selected state when query.state is STOPPED', () => {
37
+ const { container } = render(
38
+ <StoppedTasksCard query={{ state: STOPPED }} />
39
+ );
40
+ expect(screen.getByText('Stopped')).toBeInTheDocument();
41
+ const card = container.querySelector('#stopped-tasks-card');
42
+ expect(card).toHaveClass('selected-tasks-card');
43
+ });
44
+
45
+ it('renders not-focused state when query.state is not STOPPED', () => {
46
+ const { container } = render(
47
+ <StoppedTasksCard query={{ state: RUNNING }} />
48
+ );
49
+ const card = container.querySelector('#stopped-tasks-card');
50
+ expect(card).toHaveClass('not-focused');
51
+ expect(card).not.toHaveClass('selected-tasks-card');
52
+ });
53
+
54
+ it('renders StoppedTable with Error, Warning, Success labels', () => {
55
+ render(<StoppedTasksCard />);
56
+ expect(screen.getByText('Error')).toBeInTheDocument();
57
+ expect(screen.getByText('Warning')).toBeInTheDocument();
58
+ expect(screen.getByText('Success')).toBeInTheDocument();
59
+ expect(screen.getByText('Other:')).toBeInTheDocument();
60
+ });
61
+
62
+ it('renders data counts from props', () => {
63
+ const data = {
64
+ results: {
65
+ error: { total: 5, last: 2 },
66
+ warning: { total: 3, last: 1 },
67
+ success: { total: 10, last: 4 },
68
+ },
69
+ other: 7,
70
+ };
71
+ render(<StoppedTasksCard data={data} />);
72
+ expect(screen.getByText('5')).toBeInTheDocument();
73
+ expect(screen.getByText('2')).toBeInTheDocument();
74
+ expect(screen.getByText('3')).toBeInTheDocument();
75
+ expect(screen.getByText('1')).toBeInTheDocument();
76
+ expect(screen.getByText('10')).toBeInTheDocument();
77
+ expect(screen.getByText('4')).toBeInTheDocument();
78
+ expect(screen.getByText('7')).toBeInTheDocument();
79
+ });
80
+ });
81
+
82
+ describe('user interactions', () => {
83
+ it('calls updateQuery with state STOPPED when title is clicked', () => {
84
+ const updateQuery = jest.fn();
85
+ render(<StoppedTasksCard updateQuery={updateQuery} />);
86
+ fireEvent.click(screen.getByText('Stopped'));
87
+ expect(updateQuery).toHaveBeenCalledWith({ state: STOPPED });
88
+ });
89
+ });
90
+
91
+ describe('StoppedTable (via StoppedTasksCard)', () => {
92
+ it('renders table with data and time', () => {
93
+ render(
94
+ <StoppedTasksCard
95
+ data={defaultTableData}
96
+ time={WEEK}
97
+ query={{}}
98
+ updateQuery={jest.fn()}
99
+ />
100
+ );
101
+ expect(screen.getByText('Total')).toBeInTheDocument();
102
+ expect(screen.getByText('week')).toBeInTheDocument();
103
+ });
104
+
105
+ it('applies active class to Total cell when query has state STOPPED and result (total selected)', () => {
106
+ const { container } = render(
107
+ <StoppedTasksCard
108
+ data={defaultTableData}
109
+ time={WEEK}
110
+ query={{ state: STOPPED, result: 'error' }}
111
+ updateQuery={jest.fn()}
112
+ />
113
+ );
114
+ const table = container.querySelector('.stopped-table');
115
+ expect(table).toBeInTheDocument();
116
+ const activeCells = table.querySelectorAll('.data-col.active');
117
+ expect(activeCells).toHaveLength(1);
118
+ expect(activeCells[0].textContent).toBe('9');
119
+ });
120
+
121
+ ['warning', 'success'].forEach(result => {
122
+ it(`applies active class to Total cell when result is ${result} (total selected)`, () => {
123
+ const { container } = render(
124
+ <StoppedTasksCard
125
+ data={defaultTableData}
126
+ time={WEEK}
127
+ query={{ state: STOPPED, result }}
128
+ updateQuery={jest.fn()}
129
+ />
130
+ );
131
+ const table = container.querySelector('.stopped-table');
132
+ const activeCells = table.querySelectorAll('.data-col.active');
133
+ expect(activeCells.length).toBe(1);
134
+ const expectedTotal = result === 'warning' ? '8' : '7';
135
+ expect(activeCells[0].textContent).toBe(expectedTotal);
136
+ });
137
+ });
138
+
139
+ it('applies active class to Last time cell when query has result, mode LAST and time (last selected)', () => {
140
+ const { container } = render(
141
+ <StoppedTasksCard
142
+ data={defaultTableData}
143
+ time={WEEK}
144
+ query={{
145
+ state: STOPPED,
146
+ result: 'error',
147
+ mode: LAST,
148
+ time: WEEK,
149
+ }}
150
+ updateQuery={jest.fn()}
151
+ />
152
+ );
153
+ const table = container.querySelector('.stopped-table');
154
+ const activeCells = table.querySelectorAll('.data-col.active');
155
+ expect(activeCells).toHaveLength(1);
156
+ expect(activeCells[0].textContent).toBe('1');
157
+ });
158
+
159
+ it('calls updateQuery with state and result when Total is clicked', () => {
160
+ const updateQuery = jest.fn();
161
+ render(
162
+ <StoppedTasksCard
163
+ data={defaultTableData}
164
+ time={WEEK}
165
+ query={{}}
166
+ updateQuery={updateQuery}
167
+ />
168
+ );
169
+ fireEvent.click(screen.getByRole('button', { name: '9' }));
170
+ expect(updateQuery).toHaveBeenCalledWith({
171
+ state: STOPPED,
172
+ result: 'error',
173
+ });
174
+ });
175
+
176
+ it('calls updateQuery with state, result, mode LAST and time when Last time is clicked', () => {
177
+ const updateQuery = jest.fn();
178
+ render(
179
+ <StoppedTasksCard
180
+ data={defaultTableData}
181
+ time={WEEK}
182
+ query={{}}
183
+ updateQuery={updateQuery}
184
+ />
185
+ );
186
+ const lastButtons = screen.getAllByRole('button', { name: '1' });
187
+ fireEvent.click(lastButtons[0]);
188
+ expect(updateQuery).toHaveBeenCalledWith({
189
+ state: STOPPED,
190
+ result: 'error',
191
+ mode: LAST,
192
+ time: WEEK,
193
+ });
194
+ });
195
+ });
196
+ });