foreman_remote_execution 16.0.4 → 16.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +33 -6
  3. data/app/controllers/job_invocations_controller.rb +28 -1
  4. data/app/controllers/template_invocations_controller.rb +1 -1
  5. data/app/helpers/remote_execution_helper.rb +1 -1
  6. data/config/routes.rb +4 -2
  7. data/lib/foreman_remote_execution/engine.rb +9 -256
  8. data/lib/foreman_remote_execution/plugin.rb +240 -0
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  11. data/test/unit/job_invocation_report_template_test.rb +6 -6
  12. data/webpack/JobInvocationDetail/CheckboxesActions.js +327 -0
  13. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  14. data/webpack/JobInvocationDetail/JobInvocationConstants.js +8 -9
  15. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
  16. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
  17. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  18. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +23 -28
  19. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  20. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  21. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  22. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  23. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +2 -4
  24. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +204 -0
  25. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  26. data/webpack/JobInvocationDetail/index.js +71 -41
  27. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  28. data/webpack/Routes/routes.js +1 -1
  29. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  30. metadata +8 -6
  31. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  32. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +0 -110
@@ -3,31 +3,33 @@ import {
3
3
  Flex,
4
4
  PageSection,
5
5
  PageSectionVariants,
6
+ Skeleton,
6
7
  } from '@patternfly/react-core';
7
- import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
8
+ import React, { useEffect, useState } from 'react';
8
9
  import { translate as __, documentLocale } from 'foremanReact/common/I18n';
9
- import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
10
+ import { useDispatch, useSelector } from 'react-redux';
10
11
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
11
12
  import PropTypes from 'prop-types';
12
- import React, { useEffect, useState } from 'react';
13
- import { useDispatch, useSelector } from 'react-redux';
13
+ import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
14
+ import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
15
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
14
16
 
15
17
  import { JobAdditionInfo } from './JobAdditionInfo';
18
+ import JobInvocationHostTable from './JobInvocationHostTable';
19
+ import JobInvocationOverview from './JobInvocationOverview';
20
+ import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
21
+ import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
16
22
  import { getJobInvocation, getTask } from './JobInvocationActions';
23
+ import './JobInvocationDetail.scss';
17
24
  import {
18
25
  CURRENT_PERMISSIONS,
19
- currentPermissionsUrl,
20
26
  DATE_OPTIONS,
21
27
  JOB_INVOCATION_KEY,
22
28
  STATUS,
29
+ STATUS_UPPERCASE,
30
+ currentPermissionsUrl,
23
31
  } from './JobInvocationConstants';
24
- import JobInvocationHostTable from './JobInvocationHostTable';
25
- import JobInvocationOverview from './JobInvocationOverview';
26
32
  import { selectItems } from './JobInvocationSelectors';
27
- import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
28
- import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
29
-
30
- import './JobInvocationDetail.scss';
31
33
 
32
34
  const JobInvocationDetailPage = ({
33
35
  match: {
@@ -38,21 +40,20 @@ const JobInvocationDetailPage = ({
38
40
  const items = useSelector(selectItems);
39
41
  const {
40
42
  description,
43
+ failed = 0,
41
44
  status_label: statusLabel,
42
45
  task,
43
46
  start_at: startAt,
44
- targeting,
47
+ targeting = {},
45
48
  } = items;
46
49
  const finished =
47
50
  statusLabel === STATUS.FAILED ||
48
51
  statusLabel === STATUS.SUCCEEDED ||
49
52
  statusLabel === STATUS.CANCELLED;
50
53
  const autoRefresh = task?.state === STATUS.PENDING || false;
51
- const { response, status } = useAPI(
52
- 'get',
53
- currentPermissionsUrl,
54
- CURRENT_PERMISSIONS
55
- );
54
+ useAPI('get', currentPermissionsUrl, {
55
+ key: CURRENT_PERMISSIONS,
56
+ });
56
57
  const [selectedFilter, setSelectedFilter] = useState('');
57
58
 
58
59
  const handleFilterChange = newFilter => {
@@ -88,10 +89,22 @@ const JobInvocationDetailPage = ({
88
89
  // eslint-disable-next-line react-hooks/exhaustive-deps
89
90
  }, [dispatch, task?.id]);
90
91
 
92
+ const pageStatus =
93
+ items.id === undefined
94
+ ? STATUS_UPPERCASE.PENDING
95
+ : STATUS_UPPERCASE.RESOLVED;
96
+
91
97
  const breadcrumbOptions = {
92
98
  breadcrumbItems: [
93
99
  { caption: __('Jobs'), url: `/job_invocations` },
94
- { caption: description },
100
+ {
101
+ caption:
102
+ pageStatus === STATUS_UPPERCASE.PENDING ? (
103
+ <Skeleton width="350px" />
104
+ ) : (
105
+ description
106
+ ),
107
+ },
95
108
  ],
96
109
  isPf4: true,
97
110
  };
@@ -101,26 +114,27 @@ const JobInvocationDetailPage = ({
101
114
  <PageLayout
102
115
  header={description}
103
116
  breadcrumbOptions={breadcrumbOptions}
104
- toolbarButtons={
105
- <JobInvocationToolbarButtons
106
- jobId={id}
107
- data={items}
108
- currentPermissions={response.results}
109
- permissionsStatus={status}
110
- />
111
- }
117
+ toolbarButtons={<JobInvocationToolbarButtons jobId={id} data={items} />}
112
118
  searchable={false}
113
119
  >
114
120
  <Flex
115
121
  className="job-invocation-detail-flex"
116
122
  alignItems={{ default: 'alignItemsFlexStart' }}
117
123
  >
118
- <JobInvocationSystemStatusChart
119
- data={items}
120
- isAlreadyStarted={isAlreadyStarted}
121
- formattedStartDate={formattedStartDate}
122
- onFilterChange={handleFilterChange}
123
- />
124
+ <SkeletonLoader
125
+ status={pageStatus}
126
+ skeletonProps={{
127
+ height: 105,
128
+ width: 375,
129
+ }}
130
+ >
131
+ <JobInvocationSystemStatusChart
132
+ data={items}
133
+ isAlreadyStarted={isAlreadyStarted}
134
+ formattedStartDate={formattedStartDate}
135
+ onFilterChange={handleFilterChange}
136
+ />
137
+ </SkeletonLoader>
124
138
  <Divider
125
139
  orientation={{
126
140
  default: 'vertical',
@@ -130,35 +144,51 @@ const JobInvocationDetailPage = ({
130
144
  className="job-overview"
131
145
  alignItems={{ default: 'alignItemsCenter' }}
132
146
  >
133
- <JobInvocationOverview
134
- data={items}
135
- isAlreadyStarted={isAlreadyStarted}
136
- formattedStartDate={formattedStartDate}
137
- onFilterChange={handleFilterChange}
138
- />
147
+ <SkeletonLoader
148
+ status={pageStatus}
149
+ skeletonProps={{
150
+ height: 105,
151
+ width: 270,
152
+ }}
153
+ >
154
+ <JobInvocationOverview
155
+ data={items}
156
+ isAlreadyStarted={isAlreadyStarted}
157
+ formattedStartDate={formattedStartDate}
158
+ />
159
+ </SkeletonLoader>
139
160
  </Flex>
140
161
  </Flex>
141
162
  <PageSection
142
163
  variant={PageSectionVariants.light}
143
164
  className="job-additional-info"
144
165
  >
145
- {items.id !== undefined && <JobAdditionInfo data={items} />}
166
+ <SkeletonLoader
167
+ status={pageStatus}
168
+ skeletonProps={{ height: 150, width: '100%' }}
169
+ >
170
+ <JobAdditionInfo data={items} />
171
+ </SkeletonLoader>
146
172
  </PageSection>
147
173
  </PageLayout>
148
174
  <PageSection
149
175
  variant={PageSectionVariants.light}
150
176
  className="job-details-table-section table-section"
151
177
  >
152
- {items.id !== undefined && (
178
+ <SkeletonLoader
179
+ status={pageStatus}
180
+ skeletonProps={{ height: 400, width: '100%' }}
181
+ >
153
182
  <JobInvocationHostTable
154
183
  id={id}
155
184
  targeting={targeting}
185
+ failedCount={failed}
156
186
  finished={finished}
157
187
  autoRefresh={autoRefresh}
158
188
  initialFilter={selectedFilter}
159
189
  onFilterUpdate={handleFilterChange}
160
190
  />
161
- )}
191
+ </SkeletonLoader>
162
192
  </PageSection>
163
193
  </>
164
194
  );
@@ -1,12 +1,31 @@
1
1
  export const buildHostQuery = (selected, search) => {
2
+ const nameEscape = name => `"${name.replaceAll('"', '\\"')}"`;
2
3
  const { hosts, hostCollections, hostGroups } = selected;
3
- const hostsSearch = `id ^ (${hosts.map(({ id }) => id).join(',')})`;
4
- const hostCollectionsSearch = `host_collection_id ^ (${hostCollections
5
- .map(({ id }) => id)
6
- .join(',')})`;
7
- const hostGroupsSearch = `hostgroup_id ^ (${hostGroups
8
- .map(({ id }) => id)
9
- .join(',')})`;
4
+ const MAX_NAME_ITEMS = 50;
5
+ let hostsSearch;
6
+ if (hosts.length < MAX_NAME_ITEMS)
7
+ hostsSearch = `name ^ (${hosts.map(({ name }) => name).join(',')})`;
8
+ else hostsSearch = `id ^ (${hosts.map(({ id }) => id).join(',')})`;
9
+
10
+ let hostCollectionsSearch;
11
+ if (hostCollections.length < MAX_NAME_ITEMS)
12
+ hostCollectionsSearch = `host_collection ^ (${hostCollections
13
+ .map(({ name }) => nameEscape(name))
14
+ .join(',')})`;
15
+ else
16
+ hostCollectionsSearch = `host_collection_id ^ (${hostCollections
17
+ .map(({ id }) => id)
18
+ .join(',')})`;
19
+
20
+ let hostGroupsSearch;
21
+ if (hostCollections.length < MAX_NAME_ITEMS)
22
+ hostGroupsSearch = `hostgroup_fullname ^ (${hostGroups
23
+ .map(({ name }) => nameEscape(name))
24
+ .join(',')})`;
25
+ else
26
+ hostGroupsSearch = `hostgroup_id ^ (${hostGroups
27
+ .map(({ id }) => id)
28
+ .join(',')})`;
10
29
  const queryParts = [
11
30
  hosts.length ? hostsSearch : false,
12
31
  hostCollections.length ? hostCollectionsSearch : false,
@@ -16,7 +16,7 @@ const ForemanREXRoutes = [
16
16
  render: props => <JobWizardPageRerun {...props} />,
17
17
  },
18
18
  {
19
- path: '/experimental/job_invocations_detail/:id',
19
+ path: '/job_invocations/:id',
20
20
  exact: true,
21
21
  render: props => <JobInvocationDetailPage {...props} />,
22
22
  },
@@ -1,4 +1,4 @@
1
- @import '~@theforeman/vendor/scss/variables';
1
+ @import 'foremanReact/common/variables';
2
2
 
3
3
  .tasks-labels-row {
4
4
  margin: 0;
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_remote_execution
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.0.4
4
+ version: 16.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Remote Execution team
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-06-26 00:00:00.000000000 Z
10
+ date: 2025-08-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: deface
@@ -353,6 +353,7 @@ files:
353
353
  - extra/cockpit/settings.yml.example
354
354
  - lib/foreman_remote_execution.rb
355
355
  - lib/foreman_remote_execution/engine.rb
356
+ - lib/foreman_remote_execution/plugin.rb
356
357
  - lib/foreman_remote_execution/tasks/explain_proxy_selection.rake
357
358
  - lib/foreman_remote_execution/version.rb
358
359
  - lib/tasks/foreman_remote_execution_tasks.rake
@@ -419,17 +420,18 @@ files:
419
420
  - test/unit/renderer_scope_input_test.rb
420
421
  - test/unit/targeting_test.rb
421
422
  - test/unit/template_invocation_input_value_test.rb
423
+ - webpack/JobInvocationDetail/CheckboxesActions.js
424
+ - webpack/JobInvocationDetail/DropdownFilter.js
422
425
  - webpack/JobInvocationDetail/JobAdditionInfo.js
423
426
  - webpack/JobInvocationDetail/JobInvocationActions.js
424
427
  - webpack/JobInvocationDetail/JobInvocationConstants.js
425
428
  - webpack/JobInvocationDetail/JobInvocationDetail.scss
426
429
  - webpack/JobInvocationDetail/JobInvocationHostTable.js
427
- - webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js
428
430
  - webpack/JobInvocationDetail/JobInvocationOverview.js
429
431
  - webpack/JobInvocationDetail/JobInvocationSelectors.js
430
432
  - webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js
431
433
  - webpack/JobInvocationDetail/JobInvocationToolbarButtons.js
432
- - webpack/JobInvocationDetail/OpenAlInvocations.js
434
+ - webpack/JobInvocationDetail/OpenAllInvocationsModal.js
433
435
  - webpack/JobInvocationDetail/TemplateInvocation.js
434
436
  - webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js
435
437
  - webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js
@@ -438,8 +440,8 @@ files:
438
440
  - webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss
439
441
  - webpack/JobInvocationDetail/TemplateInvocationPage.js
440
442
  - webpack/JobInvocationDetail/__tests__/MainInformation.test.js
441
- - webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js
442
443
  - webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js
444
+ - webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js
443
445
  - webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
444
446
  - webpack/JobInvocationDetail/__tests__/fixtures.js
445
447
  - webpack/JobInvocationDetail/index.js
@@ -605,7 +607,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
605
607
  - !ruby/object:Gem::Version
606
608
  version: '0'
607
609
  requirements: []
608
- rubygems_version: 3.6.7
610
+ rubygems_version: 3.6.9
609
611
  specification_version: 4
610
612
  summary: A plugin bringing remote execution to the Foreman, completing the config
611
613
  management functionality with remote management functionality.
@@ -1,111 +0,0 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
3
- import {
4
- Alert,
5
- AlertActionCloseButton,
6
- Button,
7
- Modal,
8
- ModalVariant,
9
- } from '@patternfly/react-core';
10
- import { OutlinedWindowRestoreIcon } from '@patternfly/react-icons';
11
- import { translate as __ } from 'foremanReact/common/I18n';
12
- import { templateInvocationPageUrl } from './JobInvocationConstants';
13
-
14
- export const PopupAlert = ({ setShowAlert }) => (
15
- <Alert
16
- ouiaId="template-invocation-new-tab-popup-alert"
17
- variant="warning"
18
- actionClose={<AlertActionCloseButton onClose={() => setShowAlert(false)} />}
19
- title={__(
20
- 'Popups are blocked by your browser. Please allow popups for this site to open all invocations in new tabs.'
21
- )}
22
- />
23
- );
24
- export const OpenAlInvocations = ({ results, id, setShowAlert }) => {
25
- const [isModalOpen, setIsModalOpen] = React.useState(false);
26
- const handleModalToggle = () => {
27
- setIsModalOpen(!isModalOpen);
28
- };
29
-
30
- const openLink = url => {
31
- const newWin = window.open(url);
32
-
33
- if (!newWin || newWin.closed || typeof newWin.closed === 'undefined') {
34
- setShowAlert(true);
35
- }
36
- };
37
- const OpenAllButton = () => (
38
- <Button
39
- variant="link"
40
- isInline
41
- aria-label="open all template invocations in a new tab"
42
- ouiaId="template-invocation-new-tab-button"
43
- onClick={() => {
44
- if (results.length <= 3) {
45
- results.forEach(result => {
46
- openLink(templateInvocationPageUrl(result.id, id), '_blank');
47
- });
48
- } else {
49
- handleModalToggle();
50
- }
51
- }}
52
- >
53
- <OutlinedWindowRestoreIcon />
54
- </Button>
55
- );
56
- const OpenAllModal = () => (
57
- <Modal
58
- ouiaId="template-invocation-new-tab-modal"
59
- title={__('Open all invocations in new tabs')}
60
- isOpen={isModalOpen}
61
- onClose={handleModalToggle}
62
- variant={ModalVariant.small}
63
- titleIconVariant="warning"
64
- actions={[
65
- <Button
66
- ouiaId="template-invocation-new-tab-modal-confirm"
67
- key="confirm"
68
- variant="primary"
69
- onClick={() => {
70
- results.forEach(result => {
71
- openLink(templateInvocationPageUrl(result.id, id), '_blank');
72
- });
73
- handleModalToggle();
74
- }}
75
- >
76
- {__('Open all in new tabs')}
77
- </Button>,
78
- <Button
79
- ouiaId="template-invocation-new-tab-modal-cancel"
80
- key="cancel"
81
- variant="link"
82
- onClick={handleModalToggle}
83
- >
84
- {__('Cancel')}
85
- </Button>,
86
- ]}
87
- >
88
- {__('Are you sure you want to open all invocations in new tabs?')}
89
- <br />
90
- {__('This will open a new tab for each invocation.')}
91
- <br />
92
- {__('The number of invocations is:')} <b>{results.length}</b>
93
- </Modal>
94
- );
95
- return (
96
- <>
97
- <OpenAllButton />
98
- <OpenAllModal />
99
- </>
100
- );
101
- };
102
-
103
- OpenAlInvocations.propTypes = {
104
- results: PropTypes.array.isRequired,
105
- id: PropTypes.string.isRequired,
106
- setShowAlert: PropTypes.func.isRequired,
107
- };
108
-
109
- PopupAlert.propTypes = {
110
- setShowAlert: PropTypes.func.isRequired,
111
- };
@@ -1,110 +0,0 @@
1
- import React from 'react';
2
- import { render, screen, fireEvent } from '@testing-library/react';
3
- import '@testing-library/jest-dom/extend-expect';
4
- import { OpenAlInvocations, PopupAlert } from '../OpenAlInvocations';
5
- import { templateInvocationPageUrl } from '../JobInvocationConstants';
6
-
7
- // Mock the templateInvocationPageUrl function
8
- jest.mock('../JobInvocationConstants', () => ({
9
- ...jest.requireActual('../JobInvocationConstants'),
10
- templateInvocationPageUrl: jest.fn((resultId, id) => `url/${resultId}/${id}`),
11
- }));
12
-
13
- describe('OpenAlInvocations', () => {
14
- const mockResults = [{ id: 1 }, { id: 2 }, { id: 3 }];
15
- const mockSetShowAlert = jest.fn();
16
- let windowSpy;
17
- const windowOpen = window.open;
18
-
19
- beforeAll(() => {
20
- window.open = () => {};
21
- });
22
- afterAll(() => {
23
- windowSpy.mockRestore();
24
- jest.clearAllMocks();
25
- window.open = windowOpen;
26
- });
27
-
28
- test('renders without crashing', () => {
29
- render(
30
- <OpenAlInvocations
31
- results={mockResults}
32
- id="test-id"
33
- setShowAlert={mockSetShowAlert}
34
- />
35
- );
36
- });
37
-
38
- test('opens links when results length is less than or equal to 3', () => {
39
- render(
40
- <OpenAlInvocations
41
- results={mockResults}
42
- id="test-id"
43
- setShowAlert={mockSetShowAlert}
44
- />
45
- );
46
-
47
- const button = screen.getByRole('button', { name: /open all/i });
48
- fireEvent.click(button);
49
-
50
- expect(templateInvocationPageUrl).toHaveBeenCalledTimes(mockResults.length);
51
- mockResults.forEach(result => {
52
- expect(templateInvocationPageUrl).toHaveBeenCalledWith(
53
- result.id,
54
- 'test-id'
55
- );
56
- });
57
- });
58
-
59
- test('shows modal when results length is greater than 3', () => {
60
- const largeResults = [...mockResults, { id: 4 }];
61
- render(
62
- <OpenAlInvocations
63
- results={largeResults}
64
- id="test-id"
65
- setShowAlert={mockSetShowAlert}
66
- />
67
- );
68
-
69
- const button = screen.getByRole('button', { name: /open all/i });
70
- fireEvent.click(button);
71
-
72
- expect(
73
- screen.getAllByText(/open all invocations in new tabs/i)
74
- ).toHaveLength(2);
75
- });
76
-
77
- test('shows alert when popups are blocked', () => {
78
- window.open = jest.fn().mockReturnValueOnce(null);
79
-
80
- render(
81
- <OpenAlInvocations
82
- results={mockResults}
83
- id="test-id"
84
- setShowAlert={mockSetShowAlert}
85
- />
86
- );
87
-
88
- const button = screen.getByRole('button', { name: /open all/i });
89
- fireEvent.click(button);
90
-
91
- expect(mockSetShowAlert).toHaveBeenCalledWith(true);
92
- });
93
- });
94
-
95
- describe('PopupAlert', () => {
96
- const mockSetShowAlert = jest.fn();
97
-
98
- test('renders without crashing', () => {
99
- render(<PopupAlert setShowAlert={mockSetShowAlert} />);
100
- });
101
-
102
- test('closes alert when close button is clicked', () => {
103
- render(<PopupAlert setShowAlert={mockSetShowAlert} />);
104
-
105
- const closeButton = screen.getByRole('button', { name: /close/i });
106
- fireEvent.click(closeButton);
107
-
108
- expect(mockSetShowAlert).toHaveBeenCalledWith(false);
109
- });
110
- });