@cccsaurora/howler-ui 2.16.0-dev.378 → 2.16.0-dev.381

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 (57) hide show
  1. package/components/app/App.js +2 -0
  2. package/components/app/hooks/useMatchers.js +0 -4
  3. package/components/app/providers/FavouritesProvider.js +2 -1
  4. package/components/app/providers/FieldProvider.d.ts +2 -2
  5. package/components/app/providers/HitProvider.d.ts +3 -3
  6. package/components/app/providers/HitSearchProvider.d.ts +7 -8
  7. package/components/app/providers/HitSearchProvider.js +64 -39
  8. package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
  9. package/components/app/providers/HitSearchProvider.test.js +505 -0
  10. package/components/app/providers/ParameterProvider.d.ts +13 -5
  11. package/components/app/providers/ParameterProvider.js +240 -84
  12. package/components/app/providers/ParameterProvider.test.d.ts +1 -0
  13. package/components/app/providers/ParameterProvider.test.js +1041 -0
  14. package/components/app/providers/ViewProvider.d.ts +3 -2
  15. package/components/app/providers/ViewProvider.js +21 -14
  16. package/components/app/providers/ViewProvider.test.js +19 -29
  17. package/components/elements/display/ChipPopper.d.ts +21 -0
  18. package/components/elements/display/ChipPopper.js +36 -0
  19. package/components/elements/display/ChipPopper.test.d.ts +1 -0
  20. package/components/elements/display/ChipPopper.test.js +309 -0
  21. package/components/elements/hit/HitActions.js +3 -3
  22. package/components/elements/hit/HitSummary.d.ts +0 -1
  23. package/components/elements/hit/HitSummary.js +11 -21
  24. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
  25. package/components/elements/hit/aggregate/HitGraph.js +9 -15
  26. package/components/routes/dossiers/DossierCard.test.js +0 -2
  27. package/components/routes/dossiers/DossierEditor.test.js +27 -33
  28. package/components/routes/hits/search/HitBrowser.js +7 -48
  29. package/components/routes/hits/search/HitContextMenu.test.js +11 -29
  30. package/components/routes/hits/search/InformationPane.js +1 -1
  31. package/components/routes/hits/search/QuerySettings.js +30 -0
  32. package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
  33. package/components/routes/hits/search/QuerySettings.test.js +553 -0
  34. package/components/routes/hits/search/SearchPane.js +8 -10
  35. package/components/routes/hits/search/ViewLink.d.ts +4 -1
  36. package/components/routes/hits/search/ViewLink.js +37 -19
  37. package/components/routes/hits/search/ViewLink.test.js +349 -303
  38. package/components/routes/hits/search/grid/HitGrid.js +2 -6
  39. package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
  40. package/components/routes/hits/search/shared/HitFilter.js +31 -23
  41. package/components/routes/hits/search/shared/HitSort.js +16 -8
  42. package/components/routes/hits/search/shared/SearchSpan.js +19 -10
  43. package/components/routes/views/ViewComposer.js +7 -6
  44. package/components/routes/views/Views.js +2 -1
  45. package/locales/en/translation.json +6 -0
  46. package/locales/fr/translation.json +6 -0
  47. package/package.json +2 -2
  48. package/setupTests.js +4 -1
  49. package/tests/mocks.d.ts +18 -0
  50. package/tests/mocks.js +65 -0
  51. package/tests/server-handlers.js +10 -28
  52. package/utils/viewUtils.d.ts +2 -0
  53. package/utils/viewUtils.js +11 -0
  54. package/components/routes/hits/search/shared/QuerySettings.js +0 -22
  55. /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
  56. /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
  57. /package/components/routes/hits/search/{CustomSort.js → shared/CustomSort.js} +0 -0
@@ -0,0 +1,505 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { act, renderHook, waitFor } from '@testing-library/react';
3
+ import { hpost } from '@cccsaurora/howler-ui/api';
4
+ import { cloneDeep } from 'lodash-es';
5
+ import { setupContextSelectorMock, setupLocalStorageMock } from '@cccsaurora/howler-ui/tests/mocks';
6
+ import { useContextSelector } from 'use-context-selector';
7
+ import { DEFAULT_QUERY, MY_LOCAL_STORAGE_PREFIX, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
8
+ import { HitContext } from './HitProvider';
9
+ import HitSearchProvider, { HitSearchContext } from './HitSearchProvider';
10
+ import { ParameterContext } from './ParameterProvider';
11
+ import { ViewContext } from './ViewProvider';
12
+ vi.mock('api', { spy: true });
13
+ setupContextSelectorMock();
14
+ const mockLocalStorage = setupLocalStorageMock();
15
+ import { useLocation, useParams, useSearchParams } from 'react-router-dom';
16
+ const mockSetParams = vi.fn();
17
+ const mockParams = vi.mocked(useParams);
18
+ const mockLocation = vi.mocked(useLocation());
19
+ const mockViewContext = {
20
+ getCurrentViews: ({ viewId } = {}) => Promise.resolve([{ view_id: viewId || 'test_view_id', query: 'howler.id:*' }])
21
+ };
22
+ let mockParameterContext = {
23
+ filters: [],
24
+ span: 'date.range.1.week',
25
+ sort: 'event.created desc',
26
+ query: 'howler.analytic:*',
27
+ setQuery: query => (mockParameterContext.query = query),
28
+ offset: 0,
29
+ setOffset: offset => {
30
+ mockParameterContext.offset = parseInt(offset);
31
+ },
32
+ views: [],
33
+ addView: vi.fn()
34
+ };
35
+ const originalMockParameterContext = cloneDeep(mockParameterContext);
36
+ const mockHitContext = {
37
+ hits: {},
38
+ loadHits: hits => {
39
+ mockHitContext.hits = {
40
+ ...mockHitContext.hits,
41
+ ...Object.fromEntries(hits.map(hit => [hit.howler.id, hit]))
42
+ };
43
+ }
44
+ };
45
+ const Wrapper = ({ children }) => {
46
+ return (_jsx(ViewContext.Provider, { value: mockViewContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: _jsx(HitContext.Provider, { value: mockHitContext, children: _jsx(HitSearchProvider, { children: children }) }) }) }));
47
+ };
48
+ beforeEach(() => {
49
+ mockParameterContext = cloneDeep(originalMockParameterContext);
50
+ vi.mocked(originalMockParameterContext.addView).mockClear();
51
+ mockLocalStorage.clear();
52
+ mockSetParams.mockClear();
53
+ mockLocation.pathname = '/hits';
54
+ mockLocation.search = '';
55
+ mockParams.mockReturnValue({ id: undefined });
56
+ vi.mocked(hpost).mockClear();
57
+ let mockSearchParams = new URLSearchParams();
58
+ vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
59
+ });
60
+ describe('HitSearchContext', () => {
61
+ it('should initialize with default values', async () => {
62
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
63
+ displayType: ctx.displayType,
64
+ searching: ctx.searching,
65
+ error: ctx.error,
66
+ response: ctx.response,
67
+ bundleId: ctx.bundleId,
68
+ fzfSearch: ctx.fzfSearch
69
+ })), { wrapper: Wrapper });
70
+ expect(hook.result.current.displayType).toBe('list');
71
+ expect(hook.result.current.searching).toBe(false);
72
+ expect(hook.result.current.error).toBeNull();
73
+ expect(hook.result.current.response).toBeNull();
74
+ expect(hook.result.current.bundleId).toBeNull();
75
+ expect(hook.result.current.fzfSearch).toBe(false);
76
+ });
77
+ it('should set bundleId when on bundles route', () => {
78
+ mockLocation.pathname = '/bundles/test_bundle_id';
79
+ mockParams.mockReturnValue({ id: 'test_bundle_id' });
80
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.bundleId), { wrapper: Wrapper });
81
+ expect(hook.result.current).toBe('test_bundle_id');
82
+ });
83
+ it('should initialize queryHistory from localStorage', () => {
84
+ const mockHistory = { 'test:query': new Date().toISOString() };
85
+ mockLocalStorage.setItem(`${MY_LOCAL_STORAGE_PREFIX}.${StorageKey.QUERY_HISTORY}`, JSON.stringify(mockHistory));
86
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.queryHistory), { wrapper: Wrapper });
87
+ expect(hook.result.current).toEqual(mockHistory);
88
+ });
89
+ describe('setDisplayType', () => {
90
+ it('should update display type', () => {
91
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
92
+ displayType: ctx.displayType,
93
+ setDisplayType: ctx.setDisplayType
94
+ })), { wrapper: Wrapper });
95
+ expect(hook.result.current.displayType).toBe('list');
96
+ act(() => {
97
+ hook.result.current.setDisplayType('grid');
98
+ });
99
+ expect(hook.result.current.displayType).toBe('grid');
100
+ });
101
+ });
102
+ describe('setFzfSearch', () => {
103
+ it('should update fzfSearch state', () => {
104
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
105
+ fzfSearch: ctx.fzfSearch,
106
+ setFzfSearch: ctx.setFzfSearch
107
+ })), { wrapper: Wrapper });
108
+ expect(hook.result.current.fzfSearch).toBe(false);
109
+ act(() => {
110
+ hook.result.current.setFzfSearch(true);
111
+ });
112
+ expect(hook.result.current.fzfSearch).toBe(true);
113
+ });
114
+ });
115
+ describe('setQueryHistory', () => {
116
+ it('should update query history', () => {
117
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
118
+ queryHistory: ctx.queryHistory,
119
+ setQueryHistory: ctx.setQueryHistory
120
+ })), { wrapper: Wrapper });
121
+ const newHistory = { 'new:query': new Date().toISOString() };
122
+ act(() => {
123
+ hook.result.current.setQueryHistory(newHistory);
124
+ });
125
+ expect(hook.result.current.queryHistory).toEqual(newHistory);
126
+ });
127
+ });
128
+ describe('search', () => {
129
+ it('should perform a search and update response', async () => {
130
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
131
+ search: ctx.search,
132
+ searching: ctx.searching,
133
+ response: ctx.response,
134
+ error: ctx.error
135
+ })), { wrapper: Wrapper });
136
+ act(() => {
137
+ hook.result.current.search('test query');
138
+ });
139
+ await waitFor(() => {
140
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
141
+ query: expect.stringContaining('test query')
142
+ }));
143
+ });
144
+ });
145
+ it('should set searching state during search', async () => {
146
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
147
+ search: ctx.search,
148
+ searching: ctx.searching
149
+ })), { wrapper: Wrapper });
150
+ expect(hook.result.current.searching).toBe(false);
151
+ let res;
152
+ vi.mocked(hpost).mockReturnValue(new Promise(_res => (res = _res)));
153
+ act(() => {
154
+ hook.result.current.search('test query');
155
+ });
156
+ hook.rerender();
157
+ // Searching should be true immediately after calling search
158
+ await waitFor(() => {
159
+ expect(hook.result.current.searching).toBe(true);
160
+ });
161
+ res({
162
+ items: [{ howler: { id: 'hit1' } }],
163
+ offset: 0,
164
+ rows: 1,
165
+ total: 10
166
+ });
167
+ hook.rerender();
168
+ // Searching should be true immediately after calling search
169
+ await waitFor(() => {
170
+ expect(hook.result.current.searching).toBe(false);
171
+ });
172
+ });
173
+ it('should handle search errors', async () => {
174
+ vi.mocked(hpost).mockRejectedValueOnce(new Error('Search failed'));
175
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
176
+ search: ctx.search,
177
+ error: ctx.error,
178
+ searching: ctx.searching
179
+ })), { wrapper: Wrapper });
180
+ act(() => {
181
+ hook.result.current.search('test query');
182
+ });
183
+ await waitFor(() => {
184
+ expect(hook.result.current.error).toBe('Search failed');
185
+ expect(hook.result.current.searching).toBe(false);
186
+ });
187
+ });
188
+ it('should append results when appendResults is true', async () => {
189
+ const mockResponse = {
190
+ items: [{ howler: { id: 'hit1' } }],
191
+ offset: 0,
192
+ rows: 1,
193
+ total: 10
194
+ };
195
+ vi.mocked(hpost).mockResolvedValueOnce(mockResponse);
196
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
197
+ search: ctx.search,
198
+ response: ctx.response
199
+ })), { wrapper: Wrapper });
200
+ // First search
201
+ act(() => {
202
+ hook.result.current.search('test query');
203
+ });
204
+ await waitFor(() => {
205
+ expect(hook.result.current.response).toBeDefined();
206
+ expect(hook.result.current.response).not.toBeNull();
207
+ });
208
+ // Mock second response
209
+ vi.mocked(hpost).mockResolvedValueOnce({
210
+ items: [{ howler: { id: 'hit2' } }],
211
+ offset: 1,
212
+ rows: 1,
213
+ total: 10
214
+ });
215
+ // Append results
216
+ act(() => {
217
+ hook.result.current.search('test query', true);
218
+ });
219
+ hook.rerender();
220
+ await waitFor(() => {
221
+ expect(hook.result.current.response?.items.length).toBe(2);
222
+ });
223
+ });
224
+ it('should include bundle filter when on bundles route', async () => {
225
+ mockLocation.pathname = '/bundles/test_bundle_id';
226
+ mockParams.mockReturnValue({ id: 'test_bundle_id' });
227
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
228
+ act(() => {
229
+ hook.result.current('test query');
230
+ });
231
+ await waitFor(() => {
232
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
233
+ query: 'test query',
234
+ filters: ['event.created:[now-1w TO now]', 'howler.bundles:test_bundle_id']
235
+ }));
236
+ });
237
+ });
238
+ it('should apply date range filter from span', async () => {
239
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
240
+ act(() => {
241
+ hook.result.current('test query');
242
+ });
243
+ await waitFor(() => {
244
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
245
+ filters: expect.arrayContaining([expect.stringContaining('event.created:')])
246
+ }));
247
+ });
248
+ });
249
+ it('should apply custom date range when span is custom', async () => {
250
+ mockParameterContext.span = 'date.range.custom';
251
+ mockParameterContext.startDate = '2025-01-01';
252
+ mockParameterContext.endDate = '2025-12-31';
253
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
254
+ act(() => {
255
+ hook.result.current('test query');
256
+ });
257
+ await waitFor(() => {
258
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
259
+ filters: expect.arrayContaining([expect.stringContaining('event.created:')])
260
+ }));
261
+ });
262
+ });
263
+ it('should exclude filters ending with * from search', async () => {
264
+ mockParameterContext.filters = ['status:open', 'howler.escalation:*'];
265
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
266
+ act(() => {
267
+ hook.result.current('test query');
268
+ });
269
+ await waitFor(() => {
270
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
271
+ filters: expect.not.arrayContaining([expect.stringContaining('howler.escalation:*')])
272
+ }));
273
+ });
274
+ });
275
+ it('should reset offset if response total is less than current offset', async () => {
276
+ mockParameterContext.offset = 100;
277
+ vi.mocked(hpost).mockResolvedValueOnce({
278
+ items: [],
279
+ offset: 0,
280
+ rows: 0,
281
+ total: 50
282
+ });
283
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
284
+ search: ctx.search
285
+ })), { wrapper: Wrapper });
286
+ act(() => {
287
+ hook.result.current.search('test query');
288
+ });
289
+ hook.rerender();
290
+ await waitFor(() => {
291
+ expect(mockParameterContext.offset).toBe(0);
292
+ });
293
+ });
294
+ it('should not search when sort or span is null', async () => {
295
+ mockParameterContext.sort = null;
296
+ mockParameterContext.span = null;
297
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
298
+ act(() => {
299
+ hook.result.current('test query');
300
+ });
301
+ // Should not make API call
302
+ await waitFor(() => {
303
+ expect(hpost).not.toHaveBeenCalled();
304
+ });
305
+ });
306
+ });
307
+ describe('automatic search on parameter changes', () => {
308
+ it('should trigger search when filters change', async () => {
309
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
310
+ response: ctx.response
311
+ })), { wrapper: Wrapper });
312
+ await waitFor(() => {
313
+ expect(hpost).toHaveBeenCalled();
314
+ });
315
+ vi.mocked(hpost).mockClear();
316
+ // Change filters via ParameterContext
317
+ mockParameterContext.filters = [...mockParameterContext.filters, 'howler.status:open'];
318
+ hook.rerender();
319
+ await waitFor(() => {
320
+ expect(hpost).toHaveBeenCalled();
321
+ }, { timeout: 2000 });
322
+ });
323
+ it('should not trigger search when query is DEFAULT_QUERY and no bundleId', async () => {
324
+ mockParameterContext.query = DEFAULT_QUERY;
325
+ renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
326
+ await waitFor(() => {
327
+ expect(hpost).not.toHaveBeenCalled();
328
+ });
329
+ });
330
+ it('should not trigger search when span is custom but dates are missing', async () => {
331
+ renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
332
+ await waitFor(() => {
333
+ expect(hpost).not.toHaveBeenCalled();
334
+ });
335
+ });
336
+ });
337
+ describe('useHitSearchContextSelector', () => {
338
+ it('should allow selecting specific values from context', async () => {
339
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
340
+ searching: ctx.searching,
341
+ error: ctx.error
342
+ })), { wrapper: Wrapper });
343
+ expect(hook.result.current.searching).toBe(false);
344
+ expect(hook.result.current.error).toBeNull();
345
+ });
346
+ });
347
+ describe('edge cases', () => {
348
+ it('should handle concurrent search calls with throttling', async () => {
349
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
350
+ // Make multiple rapid calls
351
+ act(() => {
352
+ hook.result.current('query1');
353
+ hook.result.current('query2');
354
+ hook.result.current('query3');
355
+ });
356
+ // Should only call API once due to throttling
357
+ await waitFor(() => {
358
+ expect(hpost).toHaveBeenCalledTimes(1);
359
+ }, { timeout: 2000 });
360
+ });
361
+ it('should clear response when query becomes DEFAULT_QUERY without viewId or bundleId', async () => {
362
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
363
+ await waitFor(() => {
364
+ expect(hook.result.current).toBeDefined();
365
+ }, { timeout: 2000 });
366
+ // Change to default query
367
+ mockParameterContext.query = DEFAULT_QUERY;
368
+ hook.rerender();
369
+ await waitFor(() => {
370
+ expect(hook.result.current).toBeNull();
371
+ });
372
+ });
373
+ });
374
+ describe('multiple views support', () => {
375
+ describe('AND logic for multiple view queries', () => {
376
+ it('should combine two view queries with AND logic', async () => {
377
+ mockParameterContext.views = ['view_1', 'view_2'];
378
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
379
+ { view_id: 'view_1', query: 'howler.status:open' },
380
+ { view_id: 'view_2', query: 'howler.priority:high' }
381
+ ]);
382
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
383
+ act(() => {
384
+ hook.result.current('test query');
385
+ });
386
+ await waitFor(() => {
387
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
388
+ query: 'test query',
389
+ filters: expect.arrayContaining(['howler.status:open', 'howler.priority:high'])
390
+ }));
391
+ });
392
+ });
393
+ it('should combine three view queries with AND logic', async () => {
394
+ mockParameterContext.views = ['view_1', 'view_2', 'view_3'];
395
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
396
+ { view_id: 'view_1', query: 'howler.status:open' },
397
+ { view_id: 'view_2', query: 'howler.priority:high' },
398
+ { view_id: 'view_3', query: 'howler.analytic:sigma' }
399
+ ]);
400
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
401
+ act(() => {
402
+ hook.result.current('test query');
403
+ });
404
+ await waitFor(() => {
405
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
406
+ query: 'test query',
407
+ filters: [
408
+ 'event.created:[now-1w TO now]',
409
+ 'howler.status:open',
410
+ 'howler.priority:high',
411
+ 'howler.analytic:sigma'
412
+ ]
413
+ }));
414
+ });
415
+ });
416
+ });
417
+ describe('default view URL injection', () => {
418
+ it('should inject default view into URL when no views and not on /views route', async () => {
419
+ mockViewContext.defaultView = 'default_view_id';
420
+ mockLocation.pathname = '/search';
421
+ mockParameterContext.views = [];
422
+ const mockSearchParams = new URLSearchParams();
423
+ vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
424
+ renderHook(() => useContextSelector(HitSearchContext, () => { }), { wrapper: Wrapper });
425
+ await waitFor(() => {
426
+ expect(mockParameterContext.addView).toBeCalledWith('default_view_id');
427
+ });
428
+ });
429
+ it('should not inject default view when views already present', async () => {
430
+ mockViewContext.defaultView = 'default_view_id';
431
+ mockLocation.pathname = '/search';
432
+ mockParameterContext.views = ['existing_view'];
433
+ const mockSearchParams = new URLSearchParams();
434
+ mockSearchParams.append('view', 'existing_view');
435
+ vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
436
+ renderHook(() => useContextSelector(HitSearchContext, () => { }), { wrapper: Wrapper });
437
+ await waitFor(() => {
438
+ expect(mockParameterContext.addView).not.toBeCalled();
439
+ });
440
+ });
441
+ it('should not inject when no default view exists', async () => {
442
+ mockViewContext.defaultView = null;
443
+ mockLocation.pathname = '/search';
444
+ mockParameterContext.views = [];
445
+ const mockSearchParams = new URLSearchParams();
446
+ vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
447
+ renderHook(() => useContextSelector(HitSearchContext, () => { }), { wrapper: Wrapper });
448
+ await waitFor(() => {
449
+ expect(mockSetParams).not.toHaveBeenCalled();
450
+ });
451
+ });
452
+ });
453
+ describe('invalid view IDs', () => {
454
+ it('should not break when view ID does not exist', async () => {
455
+ mockParameterContext.views = ['non_existent_view'];
456
+ mockViewContext.getCurrentViews = vi.fn(() => Promise.resolve([null]));
457
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
458
+ act(() => {
459
+ hook.result.current('test query');
460
+ });
461
+ await waitFor(() => {
462
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
463
+ query: expect.stringContaining('test query'),
464
+ filters: ['event.created:[now-1w TO now]']
465
+ }));
466
+ }, { timeout: 2000 });
467
+ });
468
+ it('should skip null views and combine valid ones', async () => {
469
+ mockParameterContext.views = ['view_1', 'invalid_view', 'view_2'];
470
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
471
+ { view_id: 'view_1', query: 'howler.status:open' },
472
+ { view_id: 'view_2', query: 'howler.priority:high' }
473
+ ]);
474
+ const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
475
+ act(() => {
476
+ hook.result.current('test query');
477
+ });
478
+ await waitFor(() => {
479
+ expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
480
+ query: 'test query',
481
+ filters: ['event.created:[now-1w TO now]', 'howler.status:open', 'howler.priority:high']
482
+ }));
483
+ });
484
+ });
485
+ });
486
+ describe('automatic search triggering', () => {
487
+ it('should not trigger search when views is empty and query is DEFAULT_QUERY', async () => {
488
+ mockParameterContext.query = DEFAULT_QUERY;
489
+ mockParameterContext.views = [];
490
+ renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
491
+ await waitFor(() => {
492
+ expect(hpost).not.toHaveBeenCalled();
493
+ });
494
+ });
495
+ it('should trigger search when views.length > 0 even with DEFAULT_QUERY', async () => {
496
+ mockParameterContext.query = DEFAULT_QUERY;
497
+ mockParameterContext.views = ['view_1'];
498
+ renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
499
+ await waitFor(() => {
500
+ expect(hpost).toHaveBeenCalled();
501
+ });
502
+ });
503
+ });
504
+ });
505
+ });
@@ -1,26 +1,34 @@
1
1
  import type { FC, PropsWithChildren } from 'react';
2
- interface ParameterProviderType {
2
+ export interface ParameterContextType {
3
3
  selected?: string;
4
4
  query?: string;
5
5
  offset: number;
6
6
  trackTotalHits: boolean;
7
7
  sort?: string;
8
8
  span?: string;
9
- filter?: string;
9
+ filters?: string[];
10
10
  startDate?: string;
11
11
  endDate?: string;
12
+ views?: string[];
12
13
  setSelected: (id: string) => void;
13
14
  setQuery: (id: string) => void;
14
15
  setOffset: (offset: string | number) => void;
15
16
  setSort: (sort: string) => void;
16
17
  setSpan: (span: string) => void;
17
- setFilter: (filter: string) => void;
18
18
  setCustomSpan: (startDate: string, endDate: string) => void;
19
+ addFilter: (filter: string) => void;
20
+ removeFilter: (filter: string) => void;
21
+ setFilter: (index: number, filter: string) => void;
22
+ clearFilters: () => void;
23
+ addView: (view: string) => void;
24
+ removeView: (view: string) => void;
25
+ setView: (index: number, view: string) => void;
26
+ clearViews: () => void;
19
27
  }
20
- export declare const ParameterContext: import("use-context-selector").Context<ParameterProviderType>;
28
+ export declare const ParameterContext: import("use-context-selector").Context<ParameterContextType>;
21
29
  /**
22
30
  * Context responsible for tracking updates to query operations in hit and view search.
23
31
  */
24
32
  declare const ParameterProvider: FC<PropsWithChildren>;
25
- export declare const useParameterContextSelector: <Selected>(selector: (value: ParameterProviderType) => Selected) => Selected;
33
+ export declare const useParameterContextSelector: <Selected>(selector: (value: ParameterContextType) => Selected) => Selected;
26
34
  export default ParameterProvider;