@cccsaurora/howler-ui 2.19.0-dev.1266 → 2.19.0-dev.1277
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.
- package/components/routes/cases/CaseViewer.test.js +26 -3
- package/components/routes/cases/detail/CreateRuleDialog.test.js +34 -100
- package/components/routes/cases/hooks/useCase.js +4 -3
- package/components/routes/cases/hooks/useCase.test.js +33 -4
- package/components/routes/hits/search/QuerySettings.test.js +3 -5
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { render, waitFor } from '@testing-library/react';
|
|
2
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
4
4
|
import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
@@ -40,10 +40,10 @@ vi.mock('api', () => ({
|
|
|
40
40
|
}
|
|
41
41
|
}));
|
|
42
42
|
vi.mock('./detail/CaseDetails', () => ({
|
|
43
|
-
default: () => _jsx("div", { id: "case-details" })
|
|
43
|
+
default: ({ case: _case }) => _jsx("div", { id: "case-details", children: _case?.title })
|
|
44
44
|
}));
|
|
45
45
|
vi.mock('./detail/CaseSidebar', () => ({
|
|
46
|
-
default: () => _jsx("div", { id: "case-sidebar" })
|
|
46
|
+
default: ({ case: _case }) => _jsx("div", { id: "case-sidebar", children: _case?.title })
|
|
47
47
|
}));
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
49
|
// Import after mocks
|
|
@@ -130,4 +130,27 @@ describe('CaseViewer', () => {
|
|
|
130
130
|
expect(mockFetchViewers).toHaveBeenCalledWith('case-1');
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
|
+
it('propagates socket case updates to the sidebar and details', async () => {
|
|
134
|
+
const initialCase = createMockCase({ case_id: 'case-1', title: 'Original title' });
|
|
135
|
+
const updatedCase = createMockCase({ case_id: 'case-1', title: 'Updated title' });
|
|
136
|
+
mockDispatchApi.mockResolvedValue(initialCase);
|
|
137
|
+
render(_jsx(CaseViewer, {}), { wrapper: createWrapper() });
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(mockAddListener).toHaveBeenCalledOnce();
|
|
140
|
+
});
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(screen.getByTestId('case-sidebar')).toHaveTextContent('Original title');
|
|
143
|
+
});
|
|
144
|
+
act(() => {
|
|
145
|
+
mockAddListener.mock.calls[0][1]({
|
|
146
|
+
type: 'cases',
|
|
147
|
+
case: updatedCase,
|
|
148
|
+
error: false,
|
|
149
|
+
message: '',
|
|
150
|
+
status: 200
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
expect(screen.getByTestId('case-sidebar')).toHaveTextContent('Updated title');
|
|
154
|
+
expect(screen.getByTestId('case-details')).toHaveTextContent('Updated title');
|
|
155
|
+
});
|
|
133
156
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
5
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -70,9 +70,7 @@ describe('CreateRuleDialog', () => {
|
|
|
70
70
|
it('disables submit when destination is empty', async () => {
|
|
71
71
|
const user = userEvent.setup();
|
|
72
72
|
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
73
|
-
await
|
|
74
|
-
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
75
|
-
});
|
|
73
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
76
74
|
expect(screen.getByTestId('rule-submit-button')).toBeDisabled();
|
|
77
75
|
});
|
|
78
76
|
it('enables submit when query, destination are filled and search has been run', async () => {
|
|
@@ -80,15 +78,9 @@ describe('CreateRuleDialog', () => {
|
|
|
80
78
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 5, offset: 0, rows: 0 });
|
|
81
79
|
api.search.hit.post.mockReturnValue('search-request');
|
|
82
80
|
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
83
|
-
await
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
await act(async () => {
|
|
87
|
-
await user.click(screen.getByTestId('rule-search-button'));
|
|
88
|
-
});
|
|
89
|
-
await act(async () => {
|
|
90
|
-
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
91
|
-
});
|
|
81
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
82
|
+
await user.click(screen.getByTestId('rule-search-button'));
|
|
83
|
+
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
92
84
|
await waitFor(() => {
|
|
93
85
|
expect(screen.getByTestId('rule-submit-button')).not.toBeDisabled();
|
|
94
86
|
});
|
|
@@ -99,18 +91,10 @@ describe('CreateRuleDialog', () => {
|
|
|
99
91
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
|
|
100
92
|
api.search.hit.post.mockReturnValue('search-request');
|
|
101
93
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
102
|
-
await
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
await
|
|
106
|
-
await user.click(screen.getByTestId('rule-search-button'));
|
|
107
|
-
});
|
|
108
|
-
await act(async () => {
|
|
109
|
-
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
110
|
-
});
|
|
111
|
-
await act(async () => {
|
|
112
|
-
await user.click(screen.getByTestId('rule-submit-button'));
|
|
113
|
-
});
|
|
94
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
95
|
+
await user.click(screen.getByTestId('rule-search-button'));
|
|
96
|
+
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
97
|
+
await user.click(screen.getByTestId('rule-submit-button'));
|
|
114
98
|
await waitFor(() => {
|
|
115
99
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
116
100
|
query: 'event.kind:alert',
|
|
@@ -123,9 +107,7 @@ describe('CreateRuleDialog', () => {
|
|
|
123
107
|
const user = userEvent.setup();
|
|
124
108
|
const onClose = vi.fn();
|
|
125
109
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onClose: onClose }));
|
|
126
|
-
await
|
|
127
|
-
await user.click(screen.getByText('cancel'));
|
|
128
|
-
});
|
|
110
|
+
await user.click(screen.getByText('cancel'));
|
|
129
111
|
expect(onClose).toHaveBeenCalled();
|
|
130
112
|
});
|
|
131
113
|
it('shows search results after triggering search', async () => {
|
|
@@ -133,12 +115,8 @@ describe('CreateRuleDialog', () => {
|
|
|
133
115
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 42, offset: 0, rows: 0 });
|
|
134
116
|
api.search.hit.post.mockReturnValue('search-request');
|
|
135
117
|
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
136
|
-
await
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
await act(async () => {
|
|
140
|
-
await user.click(screen.getByTestId('rule-search-button'));
|
|
141
|
-
});
|
|
118
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
119
|
+
await user.click(screen.getByTestId('rule-search-button'));
|
|
142
120
|
await waitFor(() => {
|
|
143
121
|
expect(screen.getByTestId('query-result-text')).toBeInTheDocument();
|
|
144
122
|
expect(screen.getByText(/42 results/)).toBeInTheDocument();
|
|
@@ -155,18 +133,10 @@ describe('CreateRuleDialog', () => {
|
|
|
155
133
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
|
|
156
134
|
api.search.hit.post.mockReturnValue('search-request');
|
|
157
135
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
158
|
-
await
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await
|
|
162
|
-
await user.click(screen.getByTestId('rule-search-button'));
|
|
163
|
-
});
|
|
164
|
-
await act(async () => {
|
|
165
|
-
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
166
|
-
});
|
|
167
|
-
await act(async () => {
|
|
168
|
-
await user.click(screen.getByTestId('rule-submit-button'));
|
|
169
|
-
});
|
|
136
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
137
|
+
await user.click(screen.getByTestId('rule-search-button'));
|
|
138
|
+
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
139
|
+
await user.click(screen.getByTestId('rule-submit-button'));
|
|
170
140
|
await waitFor(() => {
|
|
171
141
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
172
142
|
timeframe: 14
|
|
@@ -179,21 +149,11 @@ describe('CreateRuleDialog', () => {
|
|
|
179
149
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
|
|
180
150
|
api.search.hit.post.mockReturnValue('search-request');
|
|
181
151
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
await
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
await act(async () => {
|
|
189
|
-
await user.click(screen.getByTestId('rule-search-button'));
|
|
190
|
-
});
|
|
191
|
-
await act(async () => {
|
|
192
|
-
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
193
|
-
});
|
|
194
|
-
await act(async () => {
|
|
195
|
-
await user.click(screen.getByTestId('rule-submit-button'));
|
|
196
|
-
});
|
|
152
|
+
await user.click(screen.getByTestId('rule-no-expiry-checkbox'));
|
|
153
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
154
|
+
await user.click(screen.getByTestId('rule-search-button'));
|
|
155
|
+
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
156
|
+
await user.click(screen.getByTestId('rule-submit-button'));
|
|
197
157
|
await waitFor(() => {
|
|
198
158
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
199
159
|
query: 'event.kind:alert',
|
|
@@ -208,21 +168,11 @@ describe('CreateRuleDialog', () => {
|
|
|
208
168
|
mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
|
|
209
169
|
api.search.hit.post.mockReturnValue('search-request');
|
|
210
170
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
211
|
-
await
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
await act(async () => {
|
|
218
|
-
await user.click(screen.getByTestId('rule-search-button'));
|
|
219
|
-
});
|
|
220
|
-
await act(async () => {
|
|
221
|
-
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
222
|
-
});
|
|
223
|
-
await act(async () => {
|
|
224
|
-
await user.click(screen.getByTestId('rule-submit-button'));
|
|
225
|
-
});
|
|
171
|
+
await user.click(screen.getByTestId('rule-expire-after-resolved'));
|
|
172
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
173
|
+
await user.click(screen.getByTestId('rule-search-button'));
|
|
174
|
+
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
175
|
+
await user.click(screen.getByTestId('rule-submit-button'));
|
|
226
176
|
await waitFor(() => {
|
|
227
177
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
228
178
|
timeframe: 14,
|
|
@@ -244,24 +194,12 @@ describe('CreateRuleDialog', () => {
|
|
|
244
194
|
api.search.hit.post.mockReturnValue('search-request');
|
|
245
195
|
render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
|
|
246
196
|
// Open the index Autocomplete and select 'event'
|
|
247
|
-
await
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
await act(async () => {
|
|
254
|
-
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
255
|
-
});
|
|
256
|
-
await act(async () => {
|
|
257
|
-
await user.click(screen.getByTestId('rule-search-button'));
|
|
258
|
-
});
|
|
259
|
-
await act(async () => {
|
|
260
|
-
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
261
|
-
});
|
|
262
|
-
await act(async () => {
|
|
263
|
-
await user.click(screen.getByTestId('rule-submit-button'));
|
|
264
|
-
});
|
|
197
|
+
await user.click(screen.getByRole('combobox'));
|
|
198
|
+
await user.click(screen.getByRole('option', { name: 'hit.search.index.event' }));
|
|
199
|
+
await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
|
|
200
|
+
await user.click(screen.getByTestId('rule-search-button'));
|
|
201
|
+
await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
|
|
202
|
+
await user.click(screen.getByTestId('rule-submit-button'));
|
|
265
203
|
await waitFor(() => {
|
|
266
204
|
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
267
205
|
indexes: expect.arrayContaining(['hit', 'event'])
|
|
@@ -272,12 +210,8 @@ describe('CreateRuleDialog', () => {
|
|
|
272
210
|
const user = userEvent.setup();
|
|
273
211
|
render(_jsx(CreateRuleDialog, { ...defaultProps }));
|
|
274
212
|
// 'hit' is selected by default; open the dropdown and try to deselect it
|
|
275
|
-
await
|
|
276
|
-
|
|
277
|
-
});
|
|
278
|
-
await act(async () => {
|
|
279
|
-
await user.click(screen.getByRole('option', { name: 'hit.search.index.hit' }));
|
|
280
|
-
});
|
|
213
|
+
await user.click(screen.getByRole('combobox'));
|
|
214
|
+
await user.click(screen.getByRole('option', { name: 'hit.search.index.hit' }));
|
|
281
215
|
// 'hit' should still be selected because deselecting the last index is prevented
|
|
282
216
|
expect(screen.getAllByText('hit.search.index.hit').length).toBeGreaterThan(0);
|
|
283
217
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import api from '@cccsaurora/howler-ui/api';
|
|
2
2
|
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
3
3
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
4
|
-
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
4
|
+
import { useCallback, useContext, useEffect, useId, useState } from 'react';
|
|
5
5
|
import { isCaseUpdate } from '@cccsaurora/howler-ui/utils/socketUtils';
|
|
6
6
|
const useCase = ({ caseId, case: providedCase }) => {
|
|
7
7
|
const { dispatchApi } = useMyApi();
|
|
8
8
|
const { addListener, removeListener } = useContext(SocketContext);
|
|
9
|
+
const listenerId = useId();
|
|
9
10
|
const [loading, setLoading] = useState(false);
|
|
10
11
|
const [missing, setMissing] = useState(false);
|
|
11
12
|
const [_case, setCase] = useState(providedCase);
|
|
@@ -27,7 +28,7 @@ const useCase = ({ caseId, case: providedCase }) => {
|
|
|
27
28
|
if (!activeCaseId) {
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
|
-
const listenerKey = `case-update-${activeCaseId}`;
|
|
31
|
+
const listenerKey = `case-update-${activeCaseId}-${listenerId}`;
|
|
31
32
|
addListener(listenerKey, data => {
|
|
32
33
|
if (isCaseUpdate(data) && data.case.case_id === activeCaseId) {
|
|
33
34
|
setCase(data.case);
|
|
@@ -36,7 +37,7 @@ const useCase = ({ caseId, case: providedCase }) => {
|
|
|
36
37
|
return () => {
|
|
37
38
|
removeListener(listenerKey);
|
|
38
39
|
};
|
|
39
|
-
}, [activeCaseId, addListener, removeListener]);
|
|
40
|
+
}, [activeCaseId, addListener, listenerId, removeListener]);
|
|
40
41
|
const update = useCallback(async (_updatedCase, publish = true) => {
|
|
41
42
|
if (!activeCaseId) {
|
|
42
43
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
1
|
+
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { createElement } from 'react';
|
|
2
3
|
import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
@@ -47,6 +48,12 @@ import useCase from './useCase';
|
|
|
47
48
|
const renderUseCaseHook = (args) => {
|
|
48
49
|
return renderHook(() => useCase(args));
|
|
49
50
|
};
|
|
51
|
+
const CaseConsumers = ({ case: providedCase }) => {
|
|
52
|
+
const sidebarCase = useCase({ case: providedCase });
|
|
53
|
+
const dashboardCase = useCase({ case: providedCase });
|
|
54
|
+
const detailsCase = useCase({ case: providedCase });
|
|
55
|
+
return createElement('div', null, createElement('span', { id: 'sidebar-case-title' }, sidebarCase.case.title), createElement('span', { id: 'dashboard-case-title' }, dashboardCase.case.title), createElement('span', { id: 'details-case-title' }, detailsCase.case.title));
|
|
56
|
+
};
|
|
50
57
|
// ---------------------------------------------------------------------------
|
|
51
58
|
// Setup
|
|
52
59
|
// ---------------------------------------------------------------------------
|
|
@@ -78,10 +85,10 @@ describe('useCase', () => {
|
|
|
78
85
|
});
|
|
79
86
|
});
|
|
80
87
|
describe('socket listener', () => {
|
|
81
|
-
it('registers a listener keyed by case ID', () => {
|
|
88
|
+
it('registers a listener keyed by case ID and hook instance', () => {
|
|
82
89
|
const mockCase = createMockCase({ case_id: 'c3' });
|
|
83
90
|
renderUseCaseHook({ case: mockCase });
|
|
84
|
-
expect(mockAddListener).toHaveBeenCalledWith(
|
|
91
|
+
expect(mockAddListener).toHaveBeenCalledWith(expect.stringMatching(/^case-update-c3-/), expect.any(Function));
|
|
85
92
|
});
|
|
86
93
|
it('updates state when a matching case update is received', () => {
|
|
87
94
|
const mockCase = createMockCase({ case_id: 'c4', title: 'Original' });
|
|
@@ -135,7 +142,29 @@ describe('useCase', () => {
|
|
|
135
142
|
const mockCase = createMockCase({ case_id: 'c7' });
|
|
136
143
|
const { unmount } = renderUseCaseHook({ case: mockCase });
|
|
137
144
|
unmount();
|
|
138
|
-
expect(mockRemoveListener).toHaveBeenCalledWith(
|
|
145
|
+
expect(mockRemoveListener).toHaveBeenCalledWith(expect.stringMatching(/^case-update-c7-/));
|
|
146
|
+
});
|
|
147
|
+
it('updates every concurrent case consumer for the same case', () => {
|
|
148
|
+
const mockCase = createMockCase({ case_id: 'c8', title: 'Original' });
|
|
149
|
+
render(createElement(CaseConsumers, { case: mockCase }));
|
|
150
|
+
const listenerKeys = mockAddListener.mock.calls.map(([key]) => key);
|
|
151
|
+
expect(listenerKeys).toHaveLength(3);
|
|
152
|
+
expect([...new Set(listenerKeys)]).toHaveLength(3);
|
|
153
|
+
const updatedCase = createMockCase({ case_id: 'c8', title: 'Updated via socket' });
|
|
154
|
+
act(() => {
|
|
155
|
+
mockAddListener.mock.calls.forEach(([, listener]) => {
|
|
156
|
+
listener({
|
|
157
|
+
type: 'cases',
|
|
158
|
+
case: updatedCase,
|
|
159
|
+
error: false,
|
|
160
|
+
message: '',
|
|
161
|
+
status: 200
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
expect(screen.getByTestId('sidebar-case-title')).toHaveTextContent('Updated via socket');
|
|
166
|
+
expect(screen.getByTestId('dashboard-case-title')).toHaveTextContent('Updated via socket');
|
|
167
|
+
expect(screen.getByTestId('details-case-title')).toHaveTextContent('Updated via socket');
|
|
139
168
|
});
|
|
140
169
|
});
|
|
141
170
|
});
|
|
@@ -254,11 +254,9 @@ describe('QuerySettings', () => {
|
|
|
254
254
|
const chipButton = screen.getByRole('button');
|
|
255
255
|
await user.click(chipButton);
|
|
256
256
|
const addFilterButton = screen.getByLabelText(i18n.t('hit.search.filter.add'));
|
|
257
|
-
await
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await user.click(addFilterButton);
|
|
261
|
-
});
|
|
257
|
+
await user.click(addFilterButton);
|
|
258
|
+
await user.click(addFilterButton);
|
|
259
|
+
await user.click(addFilterButton);
|
|
262
260
|
expect(mockAddFilter).toHaveBeenCalledTimes(3);
|
|
263
261
|
expect(mockAddFilter).toHaveBeenCalledWith('howler.assessment:*');
|
|
264
262
|
});
|