@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.
- package/components/app/App.js +2 -0
- package/components/app/hooks/useMatchers.js +0 -4
- package/components/app/providers/FavouritesProvider.js +2 -1
- package/components/app/providers/FieldProvider.d.ts +2 -2
- package/components/app/providers/HitProvider.d.ts +3 -3
- package/components/app/providers/HitSearchProvider.d.ts +7 -8
- package/components/app/providers/HitSearchProvider.js +64 -39
- package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
- package/components/app/providers/HitSearchProvider.test.js +505 -0
- package/components/app/providers/ParameterProvider.d.ts +13 -5
- package/components/app/providers/ParameterProvider.js +240 -84
- package/components/app/providers/ParameterProvider.test.d.ts +1 -0
- package/components/app/providers/ParameterProvider.test.js +1041 -0
- package/components/app/providers/ViewProvider.d.ts +3 -2
- package/components/app/providers/ViewProvider.js +21 -14
- package/components/app/providers/ViewProvider.test.js +19 -29
- package/components/elements/display/ChipPopper.d.ts +21 -0
- package/components/elements/display/ChipPopper.js +36 -0
- package/components/elements/display/ChipPopper.test.d.ts +1 -0
- package/components/elements/display/ChipPopper.test.js +309 -0
- package/components/elements/hit/HitActions.js +3 -3
- package/components/elements/hit/HitSummary.d.ts +0 -1
- package/components/elements/hit/HitSummary.js +11 -21
- package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
- package/components/elements/hit/aggregate/HitGraph.js +9 -15
- package/components/routes/dossiers/DossierCard.test.js +0 -2
- package/components/routes/dossiers/DossierEditor.test.js +27 -33
- package/components/routes/hits/search/HitBrowser.js +7 -48
- package/components/routes/hits/search/HitContextMenu.test.js +11 -29
- package/components/routes/hits/search/InformationPane.js +1 -1
- package/components/routes/hits/search/QuerySettings.js +30 -0
- package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
- package/components/routes/hits/search/QuerySettings.test.js +553 -0
- package/components/routes/hits/search/SearchPane.js +8 -10
- package/components/routes/hits/search/ViewLink.d.ts +4 -1
- package/components/routes/hits/search/ViewLink.js +37 -19
- package/components/routes/hits/search/ViewLink.test.js +349 -303
- package/components/routes/hits/search/grid/HitGrid.js +2 -6
- package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
- package/components/routes/hits/search/shared/HitFilter.js +31 -23
- package/components/routes/hits/search/shared/HitSort.js +16 -8
- package/components/routes/hits/search/shared/SearchSpan.js +19 -10
- package/components/routes/views/ViewComposer.js +7 -6
- package/components/routes/views/Views.js +2 -1
- package/locales/en/translation.json +6 -0
- package/locales/fr/translation.json +6 -0
- package/package.json +2 -2
- package/setupTests.js +4 -1
- package/tests/mocks.d.ts +18 -0
- package/tests/mocks.js +65 -0
- package/tests/server-handlers.js +10 -28
- package/utils/viewUtils.d.ts +2 -0
- package/utils/viewUtils.js +11 -0
- package/components/routes/hits/search/shared/QuerySettings.js +0 -22
- /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
- /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
- /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
|
|
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
|
-
|
|
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<
|
|
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:
|
|
33
|
+
export declare const useParameterContextSelector: <Selected>(selector: (value: ParameterContextType) => Selected) => Selected;
|
|
26
34
|
export default ParameterProvider;
|