@cccsaurora/howler-ui 2.15.0-dev.318 → 2.15.0-dev.326
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/api/search/count/hit.js +2 -1
- package/api/search/explain/hit.js +2 -1
- package/api/search/facet/hit.js +2 -1
- package/api/search/grouped/hit.js +2 -1
- package/api/search/histogram/hit.js +2 -1
- package/api/search/hit.d.ts +1 -1
- package/api/search/hit.js +2 -1
- package/components/app/providers/HitSearchProvider.js +4 -4
- package/components/app/providers/ParameterProvider.js +2 -1
- package/components/app/providers/ViewProvider.test.js +4 -4
- package/components/elements/display/modals/RationaleModal.d.ts +2 -0
- package/components/elements/display/modals/RationaleModal.js +44 -7
- package/components/elements/hit/aggregate/HitGraph.js +2 -1
- package/components/hooks/useHitActions.js +1 -1
- package/components/routes/advanced/luceneCompletionProvider.js +2 -1
- package/components/routes/dossiers/DossierEditor.test.js +3 -2
- package/components/routes/help/ActionIntroductionDocumentation.js +3 -3
- package/components/routes/hits/search/HitBrowser.js +2 -2
- package/components/routes/hits/search/HitContextMenu.d.ts +15 -0
- package/components/routes/hits/search/HitContextMenu.js +100 -12
- package/components/routes/hits/search/HitContextMenu.test.d.ts +1 -0
- package/components/routes/hits/search/HitContextMenu.test.js +774 -0
- package/components/routes/hits/search/HitQuery.js +4 -3
- package/components/routes/views/ViewComposer.js +2 -2
- package/locales/en/translation.json +3 -0
- package/locales/fr/translation.json +3 -0
- package/package.json +1 -1
- package/setupTests.js +1 -0
- package/tests/server-handlers.js +7 -1
- package/tests/utils.d.ts +12 -0
- package/tests/utils.js +41 -0
- package/utils/constants.d.ts +1 -0
- package/utils/constants.js +1 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable react/jsx-no-literals */
|
|
3
|
+
/* eslint-disable import/imports-first */
|
|
4
|
+
/// <reference types="vitest" />
|
|
5
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
7
|
+
import { omit } from 'lodash-es';
|
|
8
|
+
import { act, createContext, useContext } from 'react';
|
|
9
|
+
import { vi } from 'vitest';
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
// Mock API
|
|
12
|
+
vi.mock('api', { spy: true });
|
|
13
|
+
vi.mock('use-context-selector', async () => {
|
|
14
|
+
return {
|
|
15
|
+
createContext,
|
|
16
|
+
useContextSelector: (context, selector) => {
|
|
17
|
+
return selector(useContext(context));
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
// Mock react-router-dom
|
|
22
|
+
const mockNavigate = vi.fn();
|
|
23
|
+
vi.mock('react-router-dom', async () => {
|
|
24
|
+
const actual = await vi.importActual('react-router-dom');
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
useNavigate: () => mockNavigate,
|
|
28
|
+
Link: ({ to, children, ...props }) => (_jsx("a", { href: to, ...props, children: children }))
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
// Mock custom hooks
|
|
32
|
+
const mockAssess = vi.hoisted(() => vi.fn());
|
|
33
|
+
const mockVote = vi.hoisted(() => vi.fn());
|
|
34
|
+
const mockTransitionFunction = vi.hoisted(() => vi.fn());
|
|
35
|
+
const mockAvailableTransitions = vi.hoisted(() => [
|
|
36
|
+
{
|
|
37
|
+
type: 'action',
|
|
38
|
+
name: 'escalate',
|
|
39
|
+
actionFunction: mockTransitionFunction,
|
|
40
|
+
i18nKey: 'hit.actions.escalate'
|
|
41
|
+
}
|
|
42
|
+
]);
|
|
43
|
+
vi.mock('components/hooks/useHitActions', () => ({
|
|
44
|
+
default: vi.fn(() => ({
|
|
45
|
+
availableTransitions: mockAvailableTransitions,
|
|
46
|
+
canVote: true,
|
|
47
|
+
canAssess: true,
|
|
48
|
+
assess: mockAssess,
|
|
49
|
+
vote: mockVote
|
|
50
|
+
}))
|
|
51
|
+
}));
|
|
52
|
+
const mockGetMatchingAnalytic = vi.hoisted(() => vi.fn());
|
|
53
|
+
const mockGetMatchingTemplate = vi.hoisted(() => vi.fn());
|
|
54
|
+
vi.mock('components/app/hooks/useMatchers', () => ({
|
|
55
|
+
default: vi.fn(() => ({
|
|
56
|
+
getMatchingAnalytic: mockGetMatchingAnalytic,
|
|
57
|
+
getMatchingTemplate: mockGetMatchingTemplate
|
|
58
|
+
}))
|
|
59
|
+
}));
|
|
60
|
+
const mockDispatchApi = vi.fn();
|
|
61
|
+
vi.mock('components/hooks/useMyApi', () => ({
|
|
62
|
+
default: vi.fn(() => ({
|
|
63
|
+
dispatchApi: mockDispatchApi
|
|
64
|
+
}))
|
|
65
|
+
}));
|
|
66
|
+
const mockExecuteAction = vi.hoisted(() => vi.fn());
|
|
67
|
+
vi.mock('components/routes/action/useMyActionFunctions', () => ({
|
|
68
|
+
default: vi.fn(() => ({
|
|
69
|
+
executeAction: mockExecuteAction
|
|
70
|
+
}))
|
|
71
|
+
}));
|
|
72
|
+
// Mock context providers
|
|
73
|
+
// Mock plugin store
|
|
74
|
+
const mockPluginStoreExecuteFunction = vi.hoisted(() => vi.fn(() => []));
|
|
75
|
+
vi.mock('react-pluggable', () => ({
|
|
76
|
+
usePluginStore: () => ({
|
|
77
|
+
executeFunction: mockPluginStoreExecuteFunction
|
|
78
|
+
})
|
|
79
|
+
}));
|
|
80
|
+
vi.mock('plugins/store', () => ({
|
|
81
|
+
default: {
|
|
82
|
+
plugins: ['plugin1']
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
// Mock MUI components
|
|
86
|
+
vi.mock('@mui/material', async () => {
|
|
87
|
+
const actual = await vi.importActual('@mui/material');
|
|
88
|
+
return {
|
|
89
|
+
...actual,
|
|
90
|
+
Menu: ({ children, open, onClose, ...props }) => open ? (_jsx("div", { role: "menu", onClick: onClose, ...omit(props, ['sx', 'slotProps', 'MenuListProps', 'anchorOrigin', 'anchorEl']), children: children })) : null,
|
|
91
|
+
MenuItem: ({ children, onClick, disabled, component, to, ...props }) => {
|
|
92
|
+
const Component = component || 'div';
|
|
93
|
+
return (_jsx(Component, { role: "menuitem", onClick: onClick, "aria-disabled": disabled, href: to, ...omit(props, ['sx', 'component']), children: children }));
|
|
94
|
+
},
|
|
95
|
+
Fade: ({ children, in: inProp }) => (inProp ? _jsx(_Fragment, { children: children }) : null),
|
|
96
|
+
ListItemIcon: ({ children }) => _jsx("div", { children: children }),
|
|
97
|
+
ListItemText: ({ children }) => _jsx("div", { children: children }),
|
|
98
|
+
Divider: () => _jsx("hr", {}),
|
|
99
|
+
Box: ({ children, ...props }) => _jsx("div", { ...omit(props, ['sx']), children: children })
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
// Import component after mocks
|
|
103
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
104
|
+
import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
|
|
105
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
106
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
107
|
+
import { I18nextProvider } from 'react-i18next';
|
|
108
|
+
import { createMockAction, createMockAnalytic, createMockHit, createMockTemplate } from '@cccsaurora/howler-ui/tests/utils';
|
|
109
|
+
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
110
|
+
import HitContextMenu from './HitContextMenu';
|
|
111
|
+
const mockGetSelectedId = vi.fn(() => 'test-hit-1');
|
|
112
|
+
const mockConfig = {
|
|
113
|
+
lookups: {
|
|
114
|
+
'howler.assessment': ['legitimate', 'false_positive', 'unrelated']
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const mockApiContext = { config: mockConfig };
|
|
118
|
+
const mockHitContext = {
|
|
119
|
+
hits: {
|
|
120
|
+
'test-hit-1': createMockHit()
|
|
121
|
+
},
|
|
122
|
+
selectedHits: []
|
|
123
|
+
};
|
|
124
|
+
const mockParameterContext = { query: DEFAULT_QUERY, setQuery: vi.fn() };
|
|
125
|
+
// Test wrapper
|
|
126
|
+
const Wrapper = ({ children }) => {
|
|
127
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ApiConfigContext.Provider, { value: mockApiContext, children: _jsx(HitContext.Provider, { value: mockHitContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: children }) }) }) }));
|
|
128
|
+
};
|
|
129
|
+
describe('HitContextMenu', () => {
|
|
130
|
+
let user;
|
|
131
|
+
let rerender;
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
user = userEvent.setup();
|
|
134
|
+
vi.clearAllMocks();
|
|
135
|
+
mockHitContext.selectedHits.length = 0;
|
|
136
|
+
mockHitContext.hits['test-hit-1'] = createMockHit();
|
|
137
|
+
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic());
|
|
138
|
+
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate());
|
|
139
|
+
rerender = render(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) })).rerender;
|
|
140
|
+
});
|
|
141
|
+
describe('Context Menu Initialization', () => {
|
|
142
|
+
it('should open menu on right-click', async () => {
|
|
143
|
+
act(() => {
|
|
144
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
145
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
146
|
+
});
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it('should call getSelectedId with mouse event on right-click', async () => {
|
|
152
|
+
act(() => {
|
|
153
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
154
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
155
|
+
});
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
158
|
+
expect(mockGetSelectedId.mock.calls[0][0]).toBeInstanceOf(Object);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
it('should close menu when clicking on it', async () => {
|
|
162
|
+
act(() => {
|
|
163
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
164
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
165
|
+
});
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
act(() => {
|
|
170
|
+
const menu = screen.getByRole('menu');
|
|
171
|
+
user.click(menu);
|
|
172
|
+
});
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
it('should close and reopen when right-clicking while menu is open', async () => {
|
|
178
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
179
|
+
act(() => {
|
|
180
|
+
// Open menu
|
|
181
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
182
|
+
});
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
act(() => {
|
|
187
|
+
// Right-click again
|
|
188
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
189
|
+
});
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
it('should render base menu items', async () => {
|
|
195
|
+
act(() => {
|
|
196
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
197
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
198
|
+
});
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(screen.getAllByText(/open/i)).toHaveLength(2);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
it('should disable "Open Hit" when hit is null', async () => {
|
|
204
|
+
act(() => {
|
|
205
|
+
mockHitContext.hits['test-hit-1'] = null;
|
|
206
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
207
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
208
|
+
});
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
const menuItems = screen.getAllByRole('menuitem');
|
|
211
|
+
const openHitItem = menuItems.find(item => item.textContent?.toLowerCase().includes('open hit viewer'));
|
|
212
|
+
expect(openHitItem).toHaveAttribute('aria-disabled', 'true');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
it('should fetch actions from API on menu open', async () => {
|
|
216
|
+
const mockActions = [createMockAction()];
|
|
217
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
218
|
+
act(() => {
|
|
219
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
220
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
221
|
+
});
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(mockDispatchApi).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ throwError: false }));
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe('Action Type Submenus', () => {
|
|
228
|
+
it('should show assessment submenu on hover when canAssess is true', async () => {
|
|
229
|
+
act(() => {
|
|
230
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
231
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
232
|
+
});
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
const assessmentMenuItem = screen.getByText('Assess');
|
|
237
|
+
expect(assessmentMenuItem).toBeInTheDocument();
|
|
238
|
+
act(() => {
|
|
239
|
+
fireEvent.mouseEnter(assessmentMenuItem);
|
|
240
|
+
});
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(screen.getByTestId('assessment-submenu')).toBeInTheDocument();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
it('should filter assessments by analytic valid_assessments', async () => {
|
|
246
|
+
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic({
|
|
247
|
+
triage_settings: {
|
|
248
|
+
valid_assessments: ['legitimate'],
|
|
249
|
+
skip_rationale: false
|
|
250
|
+
}
|
|
251
|
+
}));
|
|
252
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
253
|
+
act(() => {
|
|
254
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
255
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
256
|
+
});
|
|
257
|
+
act(() => {
|
|
258
|
+
const assessmentMenuItem = screen.getByText('Assess');
|
|
259
|
+
fireEvent.mouseEnter(assessmentMenuItem);
|
|
260
|
+
});
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
const submenu = screen.getByTestId('assessment-submenu');
|
|
263
|
+
expect(submenu).toBeInTheDocument();
|
|
264
|
+
expect(submenu.textContent).toContain('Legitimate');
|
|
265
|
+
expect(submenu.textContent).not.toContain('False_positive');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
it('should show vote submenu when canVote is true', async () => {
|
|
269
|
+
act(() => {
|
|
270
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
271
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
272
|
+
});
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
const voteMenuItem = screen.getByText('Vote');
|
|
277
|
+
expect(voteMenuItem).toBeInTheDocument();
|
|
278
|
+
act(() => {
|
|
279
|
+
fireEvent.mouseEnter(voteMenuItem);
|
|
280
|
+
});
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(screen.getByTestId('vote-submenu')).toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
it('should show transition submenu with available transitions', async () => {
|
|
286
|
+
act(() => {
|
|
287
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
288
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
289
|
+
});
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
const actionMenuItem = screen.getByText('Run Action');
|
|
294
|
+
expect(actionMenuItem).toBeInTheDocument();
|
|
295
|
+
act(() => {
|
|
296
|
+
fireEvent.mouseEnter(actionMenuItem);
|
|
297
|
+
});
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
const submenu = screen.getByTestId('actions-submenu');
|
|
300
|
+
expect(submenu).toBeInTheDocument();
|
|
301
|
+
expect(submenu.textContent).toContain('Test Action');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
it('should show custom actions submenu with fetched actions', async () => {
|
|
305
|
+
const mockActions = [
|
|
306
|
+
createMockAction({ action_id: 'action-1', name: 'Custom Action 1' }),
|
|
307
|
+
createMockAction({ action_id: 'action-2', name: 'Custom Action 2' })
|
|
308
|
+
];
|
|
309
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
310
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
311
|
+
act(() => {
|
|
312
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
313
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
314
|
+
});
|
|
315
|
+
await waitFor(() => {
|
|
316
|
+
expect(mockDispatchApi).toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
const actionsMenuItem = screen.getByText('Run Action');
|
|
319
|
+
expect(actionsMenuItem).toBeInTheDocument();
|
|
320
|
+
act(() => {
|
|
321
|
+
fireEvent.mouseEnter(actionsMenuItem);
|
|
322
|
+
});
|
|
323
|
+
await waitFor(() => {
|
|
324
|
+
const submenu = screen.getByTestId('actions-submenu');
|
|
325
|
+
expect(submenu).toBeInTheDocument();
|
|
326
|
+
expect(submenu.textContent).toContain('Custom Action 1');
|
|
327
|
+
expect(submenu.textContent).toContain('Custom Action 2');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
it('should hide submenu on mouse leave', async () => {
|
|
331
|
+
act(() => {
|
|
332
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
333
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
334
|
+
});
|
|
335
|
+
await waitFor(() => {
|
|
336
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
const assessmentMenuItem = screen.getByText('Assess');
|
|
339
|
+
act(() => {
|
|
340
|
+
fireEvent.mouseEnter(assessmentMenuItem);
|
|
341
|
+
});
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(screen.getByTestId('assessment-submenu')).toBeInTheDocument();
|
|
344
|
+
});
|
|
345
|
+
act(() => {
|
|
346
|
+
fireEvent.mouseLeave(assessmentMenuItem);
|
|
347
|
+
});
|
|
348
|
+
await waitFor(() => {
|
|
349
|
+
expect(screen.queryByTestId('assessment-submenu')).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
it('should disable custom actions menu when no actions are available', async () => {
|
|
353
|
+
mockDispatchApi.mockResolvedValueOnce({ items: [] });
|
|
354
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
355
|
+
act(() => {
|
|
356
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
357
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
358
|
+
});
|
|
359
|
+
await waitFor(() => {
|
|
360
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
361
|
+
expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
describe('Action Execution', () => {
|
|
366
|
+
it('should call assess with selected assessment', async () => {
|
|
367
|
+
act(() => {
|
|
368
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
369
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
370
|
+
});
|
|
371
|
+
await waitFor(() => {
|
|
372
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
373
|
+
});
|
|
374
|
+
act(() => {
|
|
375
|
+
const assessmentMenuItem = screen.getByText('Assess');
|
|
376
|
+
fireEvent.mouseEnter(assessmentMenuItem);
|
|
377
|
+
});
|
|
378
|
+
await waitFor(() => {
|
|
379
|
+
expect(screen.getByTestId('assessment-submenu')).toBeInTheDocument();
|
|
380
|
+
});
|
|
381
|
+
await act(async () => {
|
|
382
|
+
const legitimateOption = screen.getByText('Legitimate');
|
|
383
|
+
await user.click(legitimateOption);
|
|
384
|
+
});
|
|
385
|
+
await waitFor(() => {
|
|
386
|
+
expect(mockAssess).toHaveBeenCalledWith('legitimate', false);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
it('should call assess with skip_rationale from analytic settings', async () => {
|
|
390
|
+
mockGetMatchingAnalytic.mockResolvedValue(createMockAnalytic({
|
|
391
|
+
triage_settings: {
|
|
392
|
+
valid_assessments: ['legitimate'],
|
|
393
|
+
skip_rationale: true
|
|
394
|
+
}
|
|
395
|
+
}));
|
|
396
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
397
|
+
act(() => {
|
|
398
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
399
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
400
|
+
});
|
|
401
|
+
await waitFor(() => {
|
|
402
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
act(() => {
|
|
405
|
+
const assessmentMenuItem = screen.getByText('Assess');
|
|
406
|
+
fireEvent.mouseEnter(assessmentMenuItem);
|
|
407
|
+
});
|
|
408
|
+
await waitFor(() => {
|
|
409
|
+
expect(screen.getByTestId('assessment-submenu')).toBeInTheDocument();
|
|
410
|
+
});
|
|
411
|
+
const legitimateOption = screen.getByText('Legitimate');
|
|
412
|
+
await user.click(legitimateOption);
|
|
413
|
+
await waitFor(() => {
|
|
414
|
+
expect(mockAssess).toHaveBeenCalledWith('legitimate', true);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
it('should call vote with lowercased vote option', async () => {
|
|
418
|
+
act(() => {
|
|
419
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
420
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
421
|
+
});
|
|
422
|
+
await waitFor(() => {
|
|
423
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
424
|
+
});
|
|
425
|
+
act(() => {
|
|
426
|
+
const voteMenuItem = screen.getByText('Vote');
|
|
427
|
+
fireEvent.mouseEnter(voteMenuItem);
|
|
428
|
+
});
|
|
429
|
+
await waitFor(() => {
|
|
430
|
+
expect(screen.getByTestId('vote-submenu')).toBeInTheDocument();
|
|
431
|
+
});
|
|
432
|
+
const upvoteOption = screen.getByText('Benign');
|
|
433
|
+
await user.click(upvoteOption);
|
|
434
|
+
await waitFor(() => {
|
|
435
|
+
expect(mockVote).toHaveBeenCalledWith('benign');
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
it('should call transition actionFunction on click', async () => {
|
|
439
|
+
act(() => {
|
|
440
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
441
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
442
|
+
});
|
|
443
|
+
await waitFor(() => {
|
|
444
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
445
|
+
});
|
|
446
|
+
act(() => {
|
|
447
|
+
const actionsMenuItem = screen.getByText('Run Action');
|
|
448
|
+
fireEvent.mouseEnter(actionsMenuItem);
|
|
449
|
+
});
|
|
450
|
+
await waitFor(() => {
|
|
451
|
+
expect(screen.getByTestId('actions-submenu').childNodes[0]).not.toBeEmptyDOMElement();
|
|
452
|
+
});
|
|
453
|
+
await act(async () => {
|
|
454
|
+
const escalateOption = screen.getByText('Custom Action 1');
|
|
455
|
+
await user.click(escalateOption);
|
|
456
|
+
});
|
|
457
|
+
await waitFor(() => {
|
|
458
|
+
expect(mockExecuteAction).toHaveBeenCalled();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
it('should call executeAction with action_id and hit query', async () => {
|
|
462
|
+
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action' })];
|
|
463
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
464
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
465
|
+
act(() => {
|
|
466
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
467
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
468
|
+
});
|
|
469
|
+
let actionsMenuItem;
|
|
470
|
+
await waitFor(() => {
|
|
471
|
+
actionsMenuItem = screen.getByText('Run Action');
|
|
472
|
+
expect(actionsMenuItem).toBeInTheDocument();
|
|
473
|
+
});
|
|
474
|
+
act(() => {
|
|
475
|
+
fireEvent.mouseEnter(actionsMenuItem);
|
|
476
|
+
});
|
|
477
|
+
await waitFor(() => {
|
|
478
|
+
expect(screen.getByTestId('actions-submenu')).toBeInTheDocument();
|
|
479
|
+
});
|
|
480
|
+
await act(async () => {
|
|
481
|
+
const customActionOption = screen.getByText('Custom Action');
|
|
482
|
+
await user.click(customActionOption);
|
|
483
|
+
});
|
|
484
|
+
await waitFor(() => {
|
|
485
|
+
expect(mockExecuteAction).toHaveBeenCalledWith('action-1', 'howler.id:test-hit-1');
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
it('should close menu after action execution', async () => {
|
|
489
|
+
act(() => {
|
|
490
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
491
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
492
|
+
});
|
|
493
|
+
await waitFor(() => {
|
|
494
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
495
|
+
});
|
|
496
|
+
act(() => {
|
|
497
|
+
const voteMenuItem = screen.getByText('Vote');
|
|
498
|
+
fireEvent.mouseEnter(voteMenuItem);
|
|
499
|
+
});
|
|
500
|
+
await waitFor(() => {
|
|
501
|
+
expect(screen.getByTestId('vote-submenu')).toBeInTheDocument();
|
|
502
|
+
});
|
|
503
|
+
await act(async () => {
|
|
504
|
+
const benignOption = screen.getByText('Benign');
|
|
505
|
+
await user.click(benignOption);
|
|
506
|
+
});
|
|
507
|
+
await waitFor(() => {
|
|
508
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
describe('Exclusion Filter Functionality', () => {
|
|
513
|
+
beforeEach(() => {
|
|
514
|
+
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
515
|
+
keys: ['howler.detection', 'event.id']
|
|
516
|
+
}));
|
|
517
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
518
|
+
});
|
|
519
|
+
it('should render exclusion submenu with template keys', async () => {
|
|
520
|
+
act(() => {
|
|
521
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
522
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
523
|
+
});
|
|
524
|
+
await waitFor(() => {
|
|
525
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
526
|
+
});
|
|
527
|
+
act(() => {
|
|
528
|
+
const excludesMenuItem = screen.getByText('Exclude By');
|
|
529
|
+
fireEvent.mouseEnter(excludesMenuItem);
|
|
530
|
+
});
|
|
531
|
+
await waitFor(() => {
|
|
532
|
+
const submenu = screen.getByTestId('excludes-submenu');
|
|
533
|
+
expect(submenu).toBeInTheDocument();
|
|
534
|
+
expect(submenu.textContent).toContain('howler.detection');
|
|
535
|
+
expect(submenu.textContent).toContain('event.id');
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
it('should generate exclusion query for single value', async () => {
|
|
539
|
+
act(() => {
|
|
540
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
541
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
542
|
+
});
|
|
543
|
+
await waitFor(() => {
|
|
544
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
545
|
+
});
|
|
546
|
+
act(() => {
|
|
547
|
+
const excludesMenuItem = screen.getByText('Exclude By');
|
|
548
|
+
fireEvent.mouseEnter(excludesMenuItem);
|
|
549
|
+
});
|
|
550
|
+
await waitFor(() => {
|
|
551
|
+
expect(screen.getByTestId('excludes-submenu')).toBeInTheDocument();
|
|
552
|
+
});
|
|
553
|
+
await act(async () => {
|
|
554
|
+
const detectionKey = screen.getByText('howler.detection');
|
|
555
|
+
await user.click(detectionKey);
|
|
556
|
+
});
|
|
557
|
+
await waitFor(() => {
|
|
558
|
+
expect(mockParameterContext.setQuery).toHaveBeenCalledWith('-howler.detection:"Test Detection"');
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
it('should generate exclusion query for array values', async () => {
|
|
562
|
+
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
563
|
+
keys: ['howler.outline.indicators']
|
|
564
|
+
}));
|
|
565
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
566
|
+
act(() => {
|
|
567
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
568
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
569
|
+
});
|
|
570
|
+
await waitFor(() => {
|
|
571
|
+
const excludesMenuItem = screen.getByText('Exclude By');
|
|
572
|
+
fireEvent.mouseEnter(excludesMenuItem);
|
|
573
|
+
});
|
|
574
|
+
await waitFor(() => {
|
|
575
|
+
expect(screen.getByTestId('excludes-submenu')).toBeInTheDocument();
|
|
576
|
+
});
|
|
577
|
+
await act(async () => {
|
|
578
|
+
const tagsKey = screen.getByText('howler.outline.indicators');
|
|
579
|
+
await user.click(tagsKey);
|
|
580
|
+
});
|
|
581
|
+
await waitFor(() => {
|
|
582
|
+
expect(mockParameterContext.setQuery).toHaveBeenCalledWith('-howler.outline.indicators:("a" OR "b" OR "c")');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
it('should preserve existing query when adding exclusion', async () => {
|
|
586
|
+
mockParameterContext.query = 'howler.status:open';
|
|
587
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
588
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
589
|
+
await waitFor(() => {
|
|
590
|
+
const excludesMenuItem = screen.getByText('Exclude By');
|
|
591
|
+
fireEvent.mouseEnter(excludesMenuItem);
|
|
592
|
+
});
|
|
593
|
+
await waitFor(() => {
|
|
594
|
+
expect(screen.getByTestId('excludes-submenu')).toBeInTheDocument();
|
|
595
|
+
});
|
|
596
|
+
await act(async () => {
|
|
597
|
+
const detectionKey = screen.getByText('howler.detection');
|
|
598
|
+
await user.click(detectionKey);
|
|
599
|
+
});
|
|
600
|
+
await waitFor(() => {
|
|
601
|
+
expect(mockParameterContext.setQuery).toHaveBeenCalledWith('(howler.status:open) AND -howler.detection:"Test Detection"');
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
it('should not render exclusion menu when template has no keys', async () => {
|
|
605
|
+
mockGetMatchingTemplate.mockResolvedValue(createMockTemplate({
|
|
606
|
+
keys: []
|
|
607
|
+
}));
|
|
608
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
609
|
+
act(() => {
|
|
610
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
611
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
612
|
+
});
|
|
613
|
+
await waitFor(() => {
|
|
614
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
615
|
+
});
|
|
616
|
+
expect(screen.queryByText('Exclude By')).toBeNull();
|
|
617
|
+
});
|
|
618
|
+
it('should skip null field values in exclusion menu', async () => {
|
|
619
|
+
act(() => {
|
|
620
|
+
mockHitContext.hits['test-hit-1'].event = {};
|
|
621
|
+
});
|
|
622
|
+
act(() => {
|
|
623
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
624
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
625
|
+
});
|
|
626
|
+
await waitFor(() => {
|
|
627
|
+
const excludesMenuItem = screen.getByText('Exclude By');
|
|
628
|
+
fireEvent.mouseEnter(excludesMenuItem);
|
|
629
|
+
});
|
|
630
|
+
await waitFor(() => {
|
|
631
|
+
const submenu = screen.getByTestId('excludes-submenu');
|
|
632
|
+
expect(submenu).toBeInTheDocument();
|
|
633
|
+
expect(submenu.textContent).toContain('howler.detection');
|
|
634
|
+
expect(submenu.textContent).not.toContain('event.id');
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
describe('Multiple Hit Selection', () => {
|
|
639
|
+
it('should use selectedHits when current hit is included', async () => {
|
|
640
|
+
act(() => {
|
|
641
|
+
mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
642
|
+
mockHitContext.hits['hit-2'] = createMockHit({ howler: { id: 'hit-2' } });
|
|
643
|
+
mockHitContext.selectedHits.push(mockHitContext.hits['hit-1'], mockHitContext.hits['hit-2']);
|
|
644
|
+
mockGetSelectedId.mockReturnValue('hit-1');
|
|
645
|
+
});
|
|
646
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
647
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
648
|
+
await waitFor(() => {
|
|
649
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
650
|
+
});
|
|
651
|
+
// The component should use selectedHits for actions
|
|
652
|
+
// We can verify this indirectly through the useHitActions hook receiving the right data
|
|
653
|
+
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
654
|
+
});
|
|
655
|
+
it('should use only current hit when not in selectedHits', async () => {
|
|
656
|
+
act(() => {
|
|
657
|
+
mockHitContext.hits['hit-1'] = createMockHit({ howler: { id: 'hit-1' } });
|
|
658
|
+
mockHitContext.selectedHits.push(mockHitContext.hits['hit-1']);
|
|
659
|
+
mockGetSelectedId.mockReturnValue('test-hit-1');
|
|
660
|
+
});
|
|
661
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
662
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
663
|
+
await waitFor(() => {
|
|
664
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
665
|
+
});
|
|
666
|
+
expect(mockGetSelectedId).toHaveBeenCalled();
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
describe('Dynamic Data Loading', () => {
|
|
670
|
+
let contextMenuWrapper;
|
|
671
|
+
beforeEach(() => {
|
|
672
|
+
contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
673
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
674
|
+
});
|
|
675
|
+
it('should call getMatchingAnalytic when hit has analytic', async () => {
|
|
676
|
+
await waitFor(() => {
|
|
677
|
+
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
it('should call getMatchingTemplate when menu opens', async () => {
|
|
681
|
+
await waitFor(() => {
|
|
682
|
+
expect(mockGetMatchingTemplate).toHaveBeenCalledWith(mockHitContext.hits['test-hit-1']);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
it('should reset state when menu closes', async () => {
|
|
686
|
+
await waitFor(() => {
|
|
687
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
688
|
+
});
|
|
689
|
+
act(() => {
|
|
690
|
+
// Hover to show submenu
|
|
691
|
+
const assessmentMenuItem = screen.getByText('Assess');
|
|
692
|
+
fireEvent.mouseEnter(assessmentMenuItem);
|
|
693
|
+
});
|
|
694
|
+
await waitFor(() => {
|
|
695
|
+
expect(screen.getByTestId('assessment-submenu')).toBeInTheDocument();
|
|
696
|
+
});
|
|
697
|
+
await act(async () => {
|
|
698
|
+
// Close menu
|
|
699
|
+
const menu = screen.getByRole('menu');
|
|
700
|
+
await user.click(menu);
|
|
701
|
+
});
|
|
702
|
+
await waitFor(() => {
|
|
703
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
704
|
+
});
|
|
705
|
+
act(() => {
|
|
706
|
+
// Reopen to verify state was reset
|
|
707
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
708
|
+
});
|
|
709
|
+
await waitFor(() => {
|
|
710
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
711
|
+
// Submenu should not be visible without hover
|
|
712
|
+
expect(screen.queryByTestId('assessment-submenu')).toBeNull();
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
describe('Edge Cases and Error Handling', () => {
|
|
717
|
+
it('should not crash when hit is null', async () => {
|
|
718
|
+
act(() => {
|
|
719
|
+
mockHitContext.hits = {};
|
|
720
|
+
});
|
|
721
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
722
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
723
|
+
await waitFor(() => {
|
|
724
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
725
|
+
});
|
|
726
|
+
expect(screen.queryByRole('menu')).toBeInTheDocument();
|
|
727
|
+
});
|
|
728
|
+
it('should not render exclusion menu when template is null', async () => {
|
|
729
|
+
mockGetMatchingTemplate.mockResolvedValue(null);
|
|
730
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
731
|
+
act(() => {
|
|
732
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
733
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
734
|
+
});
|
|
735
|
+
await waitFor(() => {
|
|
736
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
737
|
+
});
|
|
738
|
+
await waitFor(() => {
|
|
739
|
+
expect(screen.queryByText('Exclude By')).toBeNull();
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
it('should handle API failure gracefully', async () => {
|
|
743
|
+
mockDispatchApi.mockResolvedValue(null);
|
|
744
|
+
rerender(_jsx(Wrapper, { children: _jsx(HitContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
745
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
746
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
747
|
+
await waitFor(() => {
|
|
748
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
749
|
+
});
|
|
750
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
751
|
+
expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
|
|
752
|
+
});
|
|
753
|
+
it('should not call getMatchingAnalytic or getMatchingTemplate when hit has no analytic', async () => {
|
|
754
|
+
act(() => {
|
|
755
|
+
mockHitContext.hits['test-hit-1'].howler.analytic = null;
|
|
756
|
+
});
|
|
757
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
758
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
759
|
+
await waitFor(() => {
|
|
760
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
761
|
+
});
|
|
762
|
+
expect(mockGetMatchingAnalytic).not.toHaveBeenCalled();
|
|
763
|
+
});
|
|
764
|
+
it('should call plugin store with hits array', async () => {
|
|
765
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
766
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
767
|
+
await waitFor(() => {
|
|
768
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
769
|
+
});
|
|
770
|
+
// Plugin store should be called during menu render
|
|
771
|
+
expect(mockPluginStoreExecuteFunction).toHaveBeenCalled();
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
});
|