@cccsaurora/howler-ui 2.18.0-dev.736 → 2.18.0-dev.737

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 (273) hide show
  1. package/api/index.d.ts +0 -2
  2. package/api/index.js +2 -4
  3. package/api/search/facet/hit.d.ts +3 -1
  4. package/api/search/facet/index.d.ts +1 -3
  5. package/api/search/index.d.ts +1 -2
  6. package/api/search/index.js +1 -2
  7. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  8. package/components/app/App.js +7 -39
  9. package/components/app/hooks/useMatchers.d.ts +1 -1
  10. package/components/app/hooks/useMatchers.js +11 -23
  11. package/components/app/hooks/useMatchers.test.js +22 -22
  12. package/components/app/hooks/useTitle.js +3 -3
  13. package/components/app/providers/FavouritesProvider.js +2 -2
  14. package/components/app/providers/HitProvider.d.ts +22 -0
  15. package/components/app/providers/{RecordProvider.js → HitProvider.js} +41 -41
  16. package/components/app/providers/{RecordSearchProvider.d.ts → HitSearchProvider.d.ts} +6 -6
  17. package/components/app/providers/{RecordSearchProvider.js → HitSearchProvider.js} +17 -12
  18. package/components/app/providers/{RecordSearchProvider.test.js → HitSearchProvider.test.js} +70 -51
  19. package/components/app/providers/ModalProvider.d.ts +0 -1
  20. package/components/app/providers/ParameterProvider.d.ts +2 -9
  21. package/components/app/providers/ParameterProvider.js +240 -165
  22. package/components/app/providers/ParameterProvider.test.js +94 -346
  23. package/components/app/providers/UserListProvider.js +8 -28
  24. package/components/elements/PluginTypography.d.ts +1 -2
  25. package/components/elements/PluginTypography.js +2 -3
  26. package/components/elements/UserList.d.ts +2 -5
  27. package/components/elements/UserList.js +8 -18
  28. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  29. package/components/elements/display/ChipPopper.d.ts +1 -1
  30. package/components/elements/display/HowlerCard.js +1 -1
  31. package/components/elements/display/Modal.js +0 -2
  32. package/components/elements/display/icons/BundleButton.d.ts +6 -0
  33. package/components/elements/display/icons/BundleButton.js +32 -0
  34. package/components/elements/hit/HitActions.js +4 -4
  35. package/components/elements/hit/HitBanner.d.ts +0 -1
  36. package/components/elements/hit/HitBanner.js +49 -29
  37. package/components/elements/hit/HitCard.d.ts +0 -2
  38. package/components/elements/hit/HitCard.js +7 -7
  39. package/components/elements/{record/RecordComments.d.ts → hit/HitComments.d.ts} +4 -5
  40. package/components/elements/{record/RecordComments.js → hit/HitComments.js} +28 -29
  41. package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
  42. package/components/elements/hit/HitLabels.js +2 -2
  43. package/components/elements/hit/HitOutline.d.ts +0 -1
  44. package/components/elements/hit/HitOutline.js +3 -3
  45. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  46. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  47. package/components/elements/hit/HitRelated.d.ts +6 -0
  48. package/components/elements/hit/HitRelated.js +7 -0
  49. package/components/elements/hit/HitSummary.d.ts +1 -2
  50. package/components/elements/hit/HitSummary.js +5 -6
  51. package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
  52. package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
  53. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  54. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  55. package/components/elements/view/ViewTitle.d.ts +0 -1
  56. package/components/elements/view/ViewTitle.js +2 -9
  57. package/components/hooks/useHitActions.d.ts +1 -1
  58. package/components/hooks/useHitActions.js +4 -4
  59. package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
  60. package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
  61. package/components/hooks/useMyPreferences.js +1 -10
  62. package/components/hooks/useMySearch.js +2 -2
  63. package/components/hooks/useMySitemap.js +1 -4
  64. package/components/hooks/useMyTheme.js +2 -9
  65. package/components/hooks/useParamState.test.js +4 -3
  66. package/components/routes/action/edit/ActionEditor.js +2 -2
  67. package/components/routes/action/view/ActionSearch.js +1 -1
  68. package/components/routes/advanced/QueryBuilder.js +1 -1
  69. package/components/routes/advanced/QueryEditor.js +3 -3
  70. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  71. package/components/routes/analytics/AnalyticDetails.js +2 -2
  72. package/components/routes/analytics/AnalyticSearch.js +1 -1
  73. package/components/routes/dossiers/DossierEditor.js +2 -2
  74. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  75. package/components/routes/help/ApiDocumentation.js +1 -1
  76. package/components/routes/help/BundleDocumentation.d.ts +3 -0
  77. package/components/routes/help/BundleDocumentation.js +12 -0
  78. package/components/routes/help/HitBannerDocumentation.js +0 -1
  79. package/components/routes/help/HitDocumentation.js +3 -1
  80. package/components/routes/help/markdown/en/bundles.md.js +1 -0
  81. package/components/routes/help/markdown/fr/bundles.md.js +1 -0
  82. package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
  83. package/components/routes/hits/search/BundleParentMenu.js +32 -0
  84. package/components/routes/hits/search/BundleScroller.d.ts +2 -0
  85. package/components/routes/hits/search/BundleScroller.js +6 -0
  86. package/components/routes/hits/search/{RecordBrowser.js → HitBrowser.js} +9 -9
  87. package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts} +3 -3
  88. package/components/routes/hits/search/HitContextMenu.js +227 -0
  89. package/components/{elements/record/RecordContextMenu.test.js → routes/hits/search/HitContextMenu.test.js} +39 -94
  90. package/components/routes/hits/search/{RecordQuery.d.ts → HitQuery.d.ts} +2 -2
  91. package/components/routes/hits/search/{RecordQuery.js → HitQuery.js} +6 -6
  92. package/components/routes/hits/search/InformationPane.d.ts +0 -1
  93. package/components/routes/hits/search/InformationPane.js +60 -47
  94. package/components/routes/hits/search/LayoutSettings.js +3 -3
  95. package/components/routes/hits/search/QuerySettings.js +1 -2
  96. package/components/routes/hits/search/QuerySettings.test.js +9 -14
  97. package/components/routes/hits/search/SearchPane.js +49 -26
  98. package/components/routes/hits/search/ViewLink.js +3 -3
  99. package/components/routes/hits/search/ViewLink.test.js +8 -8
  100. package/components/routes/hits/search/grid/AddColumnModal.js +4 -5
  101. package/components/routes/hits/search/grid/EnhancedCell.d.ts +1 -2
  102. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  103. package/components/routes/hits/search/grid/HitGrid.js +18 -20
  104. package/components/routes/hits/search/grid/{RecordRow.d.ts → HitRow.d.ts} +2 -3
  105. package/components/routes/hits/search/grid/{RecordRow.js → HitRow.js} +8 -10
  106. package/components/routes/hits/view/HitViewer.js +13 -12
  107. package/components/routes/home/ViewCard.js +41 -47
  108. package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
  109. package/components/routes/overviews/OverviewViewer.js +2 -2
  110. package/components/routes/views/ViewComposer.js +19 -46
  111. package/locales/en/translation.json +3 -89
  112. package/locales/fr/translation.json +3 -87
  113. package/models/WithMetadata.d.ts +1 -2
  114. package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
  115. package/models/entities/generated/Hit.d.ts +0 -1
  116. package/models/entities/generated/Howler.d.ts +4 -0
  117. package/models/entities/generated/Rule.d.ts +10 -2
  118. package/models/entities/generated/Threat.d.ts +2 -2
  119. package/models/entities/generated/View.d.ts +0 -1
  120. package/package.json +2 -19
  121. package/plugins/clue/components/ClueTypography.js +2 -2
  122. package/plugins/clue/utils.d.ts +1 -2
  123. package/tests/mocks.d.ts +1 -11
  124. package/tests/mocks.js +7 -12
  125. package/tests/server-handlers.js +1 -6
  126. package/tests/utils.d.ts +0 -4
  127. package/tests/utils.js +0 -20
  128. package/utils/constants.d.ts +3 -3
  129. package/utils/hitFunctions.d.ts +1 -2
  130. package/utils/hitFunctions.js +4 -4
  131. package/utils/viewUtils.js +0 -3
  132. package/api/search/case.d.ts +0 -4
  133. package/api/search/case.js +0 -8
  134. package/api/v2/case/index.d.ts +0 -8
  135. package/api/v2/case/index.js +0 -20
  136. package/api/v2/case/items.d.ts +0 -6
  137. package/api/v2/case/items.js +0 -18
  138. package/api/v2/index.d.ts +0 -4
  139. package/api/v2/index.js +0 -6
  140. package/api/v2/search/facet.d.ts +0 -3
  141. package/api/v2/search/facet.js +0 -12
  142. package/api/v2/search/index.d.ts +0 -5
  143. package/api/v2/search/index.js +0 -24
  144. package/components/app/providers/RecordProvider.d.ts +0 -23
  145. package/components/elements/ContextMenu.d.ts +0 -56
  146. package/components/elements/ContextMenu.js +0 -109
  147. package/components/elements/ContextMenu.test.js +0 -215
  148. package/components/elements/ObjectDetails.d.ts +0 -6
  149. package/components/elements/case/CaseCard.d.ts +0 -12
  150. package/components/elements/case/CaseCard.js +0 -42
  151. package/components/elements/case/CasePreview.d.ts +0 -6
  152. package/components/elements/case/CasePreview.js +0 -17
  153. package/components/elements/case/StatusIcon.d.ts +0 -5
  154. package/components/elements/case/StatusIcon.js +0 -13
  155. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -9
  156. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  157. package/components/elements/hit/related/RelatedRecords.js +0 -63
  158. package/components/elements/observable/ObservableCard.d.ts +0 -6
  159. package/components/elements/observable/ObservableCard.js +0 -22
  160. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  161. package/components/elements/observable/ObservablePreview.js +0 -12
  162. package/components/elements/record/RecordContextMenu.js +0 -247
  163. package/components/elements/record/RecordContextMenu.test.d.ts +0 -1
  164. package/components/elements/record/RecordRelated.d.ts +0 -7
  165. package/components/elements/record/RecordRelated.js +0 -34
  166. package/components/hooks/useRelatedRecords.d.ts +0 -13
  167. package/components/hooks/useRelatedRecords.js +0 -32
  168. package/components/routes/cases/CaseViewer.d.ts +0 -2
  169. package/components/routes/cases/CaseViewer.js +0 -22
  170. package/components/routes/cases/Cases.d.ts +0 -2
  171. package/components/routes/cases/Cases.js +0 -101
  172. package/components/routes/cases/constants.d.ts +0 -5
  173. package/components/routes/cases/constants.js +0 -5
  174. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  175. package/components/routes/cases/detail/AlertPanel.js +0 -33
  176. package/components/routes/cases/detail/CaseAssets.d.ts +0 -11
  177. package/components/routes/cases/detail/CaseAssets.js +0 -104
  178. package/components/routes/cases/detail/CaseAssets.test.d.ts +0 -1
  179. package/components/routes/cases/detail/CaseAssets.test.js +0 -167
  180. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  181. package/components/routes/cases/detail/CaseDashboard.js +0 -66
  182. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  183. package/components/routes/cases/detail/CaseDetails.js +0 -61
  184. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  185. package/components/routes/cases/detail/CaseOverview.js +0 -43
  186. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
  187. package/components/routes/cases/detail/CaseSidebar.js +0 -107
  188. package/components/routes/cases/detail/CaseSidebar.test.d.ts +0 -1
  189. package/components/routes/cases/detail/CaseSidebar.test.js +0 -246
  190. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  191. package/components/routes/cases/detail/CaseTask.js +0 -57
  192. package/components/routes/cases/detail/CaseTimeline.d.ts +0 -12
  193. package/components/routes/cases/detail/CaseTimeline.js +0 -106
  194. package/components/routes/cases/detail/CaseTimeline.test.d.ts +0 -1
  195. package/components/routes/cases/detail/CaseTimeline.test.js +0 -227
  196. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  197. package/components/routes/cases/detail/ItemPage.js +0 -99
  198. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  199. package/components/routes/cases/detail/RelatedCasePanel.js +0 -34
  200. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  201. package/components/routes/cases/detail/TaskPanel.js +0 -52
  202. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -11
  203. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -24
  204. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  205. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -26
  206. package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
  207. package/components/routes/cases/detail/assets/Asset.js +0 -12
  208. package/components/routes/cases/detail/assets/Asset.test.d.ts +0 -1
  209. package/components/routes/cases/detail/assets/Asset.test.js +0 -72
  210. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -20
  211. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -83
  212. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +0 -1
  213. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +0 -295
  214. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
  215. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -103
  216. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +0 -1
  217. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -363
  218. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +0 -25
  219. package/components/routes/cases/detail/sidebar/FolderEntry.js +0 -88
  220. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +0 -1
  221. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +0 -206
  222. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +0 -5
  223. package/components/routes/cases/detail/sidebar/RootDropZone.js +0 -33
  224. package/components/routes/cases/detail/sidebar/types.d.ts +0 -9
  225. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  226. package/components/routes/cases/detail/sidebar/utils.js +0 -29
  227. package/components/routes/cases/detail/sidebar/utils.test.d.ts +0 -1
  228. package/components/routes/cases/detail/sidebar/utils.test.js +0 -82
  229. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  230. package/components/routes/cases/hooks/useCase.js +0 -51
  231. package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
  232. package/components/routes/cases/modals/AddToCaseModal.js +0 -62
  233. package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
  234. package/components/routes/cases/modals/RenameItemModal.js +0 -48
  235. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  236. package/components/routes/cases/modals/ResolveModal.js +0 -115
  237. package/components/routes/cases/modals/ResolveModal.test.d.ts +0 -1
  238. package/components/routes/cases/modals/ResolveModal.test.js +0 -384
  239. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  240. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  241. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  242. package/components/routes/observables/ObservableViewer.js +0 -27
  243. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  244. package/models/entities/generated/Case.d.ts +0 -28
  245. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  246. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  247. package/models/entities/generated/EmailParent.d.ts +0 -19
  248. package/models/entities/generated/Enrichments.d.ts +0 -7
  249. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  250. package/models/entities/generated/HttpResponse.d.ts +0 -11
  251. package/models/entities/generated/Item.d.ts +0 -9
  252. package/models/entities/generated/Observable.d.ts +0 -85
  253. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  254. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  255. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  256. package/models/entities/generated/ObservableFile.d.ts +0 -36
  257. package/models/entities/generated/ObservableHowler.d.ts +0 -43
  258. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  259. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  260. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  261. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  262. package/models/entities/generated/ObservableSource.d.ts +0 -23
  263. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  264. package/models/entities/generated/ObservableTls.d.ts +0 -12
  265. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  266. package/models/entities/generated/Task.d.ts +0 -10
  267. package/utils/typeUtils.d.ts +0 -7
  268. package/utils/typeUtils.js +0 -27
  269. /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
  270. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  271. /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
  272. /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
  273. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -1,18 +1,23 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { act, renderHook, waitFor } from '@testing-library/react';
3
- import { setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
4
3
  import { useContextSelector } from 'use-context-selector';
5
4
  import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
6
5
  import ParameterProvider, { ParameterContext } from './ParameterProvider';
7
6
  // Mock dependencies
8
- const { mockParams, mockLocation, mockSetParams, mockSearchParams } = setupReactRouterMock();
7
+ const mockSetParams = vi.fn();
8
+ const mockLocation = { pathname: '/hits', search: '' };
9
+ const mockParams = { id: undefined };
10
+ let mockSearchParams = new URLSearchParams();
11
+ vi.mock('react-router-dom', () => ({
12
+ useLocation: vi.fn(() => mockLocation),
13
+ useParams: vi.fn(() => mockParams),
14
+ useSearchParams: vi.fn(() => [mockSearchParams, mockSetParams])
15
+ }));
9
16
  const Wrapper = ({ children }) => {
10
17
  return _jsx(ParameterProvider, { children: children });
11
18
  };
12
19
  beforeEach(() => {
13
- for (const key of [...mockSearchParams.keys()]) {
14
- mockSearchParams.delete(key);
15
- }
20
+ mockSearchParams = new URLSearchParams();
16
21
  mockSetParams.mockClear();
17
22
  mockLocation.pathname = '/hits';
18
23
  mockLocation.search = '';
@@ -34,13 +39,15 @@ describe('ParameterContext', () => {
34
39
  expect(hook.result.current.trackTotalHits).toBe(false);
35
40
  });
36
41
  it('should initialize with values from URL params', async () => {
37
- mockSearchParams.set('query', 'test query');
38
- mockSearchParams.set('sort', 'test.field asc');
39
- mockSearchParams.set('span', 'date.range.1.week');
40
- mockSearchParams.set('offset', '25');
41
- mockSearchParams.set('selected', 'test_id');
42
- mockSearchParams.set('filter', 'status:open');
43
- mockSearchParams.set('track_total_hits', 'true');
42
+ mockSearchParams = new URLSearchParams({
43
+ query: 'test query',
44
+ sort: 'test.field asc',
45
+ span: 'date.range.1.week',
46
+ offset: '25',
47
+ selected: 'test_id',
48
+ filter: 'status:open',
49
+ track_total_hits: 'true'
50
+ });
44
51
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
45
52
  query: ctx.query,
46
53
  sort: ctx.sort,
@@ -59,9 +66,11 @@ describe('ParameterContext', () => {
59
66
  expect(hook.result.current.trackTotalHits).toBe(true);
60
67
  });
61
68
  it('should handle custom date span with start and end dates', async () => {
62
- mockSearchParams.set('span', 'date.range.custom');
63
- mockSearchParams.set('start_date', '2025-01-01');
64
- mockSearchParams.set('end_date', '2025-12-31');
69
+ mockSearchParams = new URLSearchParams({
70
+ span: 'date.range.custom',
71
+ start_date: '2025-01-01',
72
+ end_date: '2025-12-31'
73
+ });
65
74
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
66
75
  span: ctx.span,
67
76
  startDate: ctx.startDate,
@@ -85,7 +94,7 @@ describe('ParameterContext', () => {
85
94
  });
86
95
  });
87
96
  it('should not update if the value is the same', async () => {
88
- mockSearchParams.set('query', 'existing query');
97
+ mockSearchParams = new URLSearchParams({ query: 'existing query' });
89
98
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
90
99
  query: ctx.query,
91
100
  setQuery: ctx.setQuery
@@ -125,9 +134,11 @@ describe('ParameterContext', () => {
125
134
  });
126
135
  });
127
136
  it('should clear startDate and endDate when span does not end with custom', async () => {
128
- mockSearchParams.set('span', 'date.range.custom');
129
- mockSearchParams.set('start_date', '2025-01-01');
130
- mockSearchParams.set('end_date', '2025-12-31');
137
+ mockSearchParams = new URLSearchParams({
138
+ span: 'date.range.custom',
139
+ start_date: '2025-01-01',
140
+ end_date: '2025-12-31'
141
+ });
131
142
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
132
143
  span: ctx.span,
133
144
  startDate: ctx.startDate,
@@ -150,17 +161,19 @@ describe('ParameterContext', () => {
150
161
  expect(hook.result.current).toEqual([]);
151
162
  });
152
163
  it('should initialize with single filter from URL', async () => {
153
- mockSearchParams.set('filter', 'status:open');
164
+ mockSearchParams = new URLSearchParams({ filter: 'status:open' });
154
165
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.filters), { wrapper: Wrapper });
155
166
  expect(hook.result.current).toEqual(['status:open']);
156
167
  });
157
168
  it('should initialize with multiple filters from URL', async () => {
169
+ mockSearchParams = new URLSearchParams();
158
170
  mockSearchParams.append('filter', 'howler.escalation:hit');
159
171
  mockSearchParams.append('filter', 'howler.assignment:someuser');
160
172
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.filters), { wrapper: Wrapper });
161
173
  expect(hook.result.current).toEqual(['howler.escalation:hit', 'howler.assignment:someuser']);
162
174
  });
163
175
  it('should preserve filter order from URL', async () => {
176
+ mockSearchParams = new URLSearchParams();
164
177
  mockSearchParams.append('filter', 'c');
165
178
  mockSearchParams.append('filter', 'a');
166
179
  mockSearchParams.append('filter', 'b');
@@ -168,6 +181,7 @@ describe('ParameterContext', () => {
168
181
  expect(hook.result.current).toEqual(['c', 'a', 'b']);
169
182
  });
170
183
  it('should deduplicate multiple empty filter params to single empty string', async () => {
184
+ mockSearchParams = new URLSearchParams();
171
185
  mockSearchParams.append('filter', '');
172
186
  mockSearchParams.append('filter', '');
173
187
  mockSearchParams.append('filter', '');
@@ -188,7 +202,7 @@ describe('ParameterContext', () => {
188
202
  });
189
203
  });
190
204
  it('should append filter to existing filters', async () => {
191
- mockSearchParams.set('filter', 'existing:filter');
205
+ mockSearchParams = new URLSearchParams({ filter: 'existing:filter' });
192
206
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
193
207
  filters: ctx.filters,
194
208
  addFilter: ctx.addFilter
@@ -201,7 +215,7 @@ describe('ParameterContext', () => {
201
215
  });
202
216
  });
203
217
  it('should not add duplicate filters', async () => {
204
- mockSearchParams.set('filter', 'status:open');
218
+ mockSearchParams = new URLSearchParams({ filter: 'status:open' });
205
219
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
206
220
  filters: ctx.filters,
207
221
  addFilter: ctx.addFilter
@@ -217,6 +231,7 @@ describe('ParameterContext', () => {
217
231
  });
218
232
  describe('removeFilter', () => {
219
233
  it('should remove first matching filter', async () => {
234
+ mockSearchParams = new URLSearchParams();
220
235
  mockSearchParams.append('filter', 'filter1');
221
236
  mockSearchParams.append('filter', 'filter2');
222
237
  mockSearchParams.append('filter', 'filter3');
@@ -232,7 +247,7 @@ describe('ParameterContext', () => {
232
247
  });
233
248
  });
234
249
  it('should do nothing when removing nonexistent filter', async () => {
235
- mockSearchParams.set('filter', 'existing');
250
+ mockSearchParams = new URLSearchParams({ filter: 'existing' });
236
251
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
237
252
  filters: ctx.filters,
238
253
  removeFilter: ctx.removeFilter
@@ -257,16 +272,17 @@ describe('ParameterContext', () => {
257
272
  });
258
273
  });
259
274
  });
260
- describe('resetFilters', () => {
275
+ describe('clearFilters', () => {
261
276
  it('should clear all filters', async () => {
277
+ mockSearchParams = new URLSearchParams();
262
278
  mockSearchParams.append('filter', 'filter1');
263
279
  mockSearchParams.append('filter', 'filter2');
264
280
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
265
281
  filters: ctx.filters,
266
- resetFilters: ctx.resetFilters
282
+ clearFilters: ctx.clearFilters
267
283
  })), { wrapper: Wrapper });
268
284
  await act(async () => {
269
- hook.result.current.resetFilters();
285
+ hook.result.current.clearFilters();
270
286
  });
271
287
  await waitFor(() => {
272
288
  expect(hook.result.current.filters).toEqual([]);
@@ -275,10 +291,10 @@ describe('ParameterContext', () => {
275
291
  it('should be no-op when already empty', async () => {
276
292
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
277
293
  filters: ctx.filters,
278
- resetFilters: ctx.resetFilters
294
+ clearFilters: ctx.clearFilters
279
295
  })), { wrapper: Wrapper });
280
296
  await act(async () => {
281
- hook.result.current.resetFilters();
297
+ hook.result.current.clearFilters();
282
298
  });
283
299
  await waitFor(() => {
284
300
  expect(hook.result.current.filters).toEqual([]);
@@ -287,6 +303,7 @@ describe('ParameterContext', () => {
287
303
  });
288
304
  describe('setFilter', () => {
289
305
  it('should update filter at specified index', async () => {
306
+ mockSearchParams = new URLSearchParams();
290
307
  mockSearchParams.append('filter', 'filter1');
291
308
  mockSearchParams.append('filter', 'filter2');
292
309
  mockSearchParams.append('filter', 'filter3');
@@ -302,6 +319,7 @@ describe('ParameterContext', () => {
302
319
  });
303
320
  });
304
321
  it('should update filter at index 0', async () => {
322
+ mockSearchParams = new URLSearchParams();
305
323
  mockSearchParams.append('filter', 'old:filter');
306
324
  mockSearchParams.append('filter', 'filter2');
307
325
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
@@ -316,6 +334,7 @@ describe('ParameterContext', () => {
316
334
  });
317
335
  });
318
336
  it('should update filter at last index', async () => {
337
+ mockSearchParams = new URLSearchParams();
319
338
  mockSearchParams.append('filter', 'filter1');
320
339
  mockSearchParams.append('filter', 'old:last');
321
340
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
@@ -330,7 +349,7 @@ describe('ParameterContext', () => {
330
349
  });
331
350
  });
332
351
  it('should do nothing when index is out of bounds', async () => {
333
- mockSearchParams.set('filter', 'existing');
352
+ mockSearchParams = new URLSearchParams({ filter: 'existing' });
334
353
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
335
354
  filters: ctx.filters,
336
355
  setFilter: ctx.setFilter
@@ -343,7 +362,7 @@ describe('ParameterContext', () => {
343
362
  });
344
363
  });
345
364
  it('should do nothing when index is negative', async () => {
346
- mockSearchParams.set('filter', 'existing');
365
+ mockSearchParams = new URLSearchParams({ filter: 'existing' });
347
366
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
348
367
  filters: ctx.filters,
349
368
  setFilter: ctx.setFilter
@@ -368,6 +387,7 @@ describe('ParameterContext', () => {
368
387
  });
369
388
  });
370
389
  it('should sync updated filter to URL', async () => {
390
+ mockSearchParams = new URLSearchParams();
371
391
  mockSearchParams.append('filter', 'filter1');
372
392
  mockSearchParams.append('filter', 'filter2');
373
393
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
@@ -417,13 +437,14 @@ describe('ParameterContext', () => {
417
437
  });
418
438
  });
419
439
  it('should remove all filter params when filters is empty', async () => {
440
+ mockSearchParams = new URLSearchParams();
420
441
  mockSearchParams.append('filter', 'filter1');
421
442
  mockSearchParams.append('filter', 'filter2');
422
443
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
423
- resetFilters: ctx.resetFilters
444
+ clearFilters: ctx.clearFilters
424
445
  })), { wrapper: Wrapper });
425
446
  await act(async () => {
426
- hook.result.current.resetFilters();
447
+ hook.result.current.clearFilters();
427
448
  });
428
449
  await waitFor(() => {
429
450
  expect(mockSetParams).toHaveBeenCalled();
@@ -546,11 +567,11 @@ describe('ParameterContext', () => {
546
567
  });
547
568
  });
548
569
  it('should read changes from URL params', async () => {
549
- mockSearchParams.set('query', 'initial query');
570
+ mockSearchParams = new URLSearchParams({ query: 'initial query' });
550
571
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.query), { wrapper: Wrapper });
551
572
  expect(hook.result.current).toBe('initial query');
552
573
  // Simulate URL change
553
- mockSearchParams.set('query', 'updated query');
574
+ mockSearchParams = new URLSearchParams({ query: 'updated query' });
554
575
  mockLocation.search = '?query=updated%20query';
555
576
  hook.rerender();
556
577
  await waitFor(() => {
@@ -568,22 +589,24 @@ describe('ParameterContext', () => {
568
589
  it('should handle selected parameter in bundle when it matches bundle id', async () => {
569
590
  mockLocation.pathname = '/bundles/bundle_123';
570
591
  mockParams.id = 'bundle_123';
571
- mockSearchParams.set('selected', 'bundle_123');
592
+ mockSearchParams = new URLSearchParams({ selected: 'bundle_123' });
572
593
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.selected), { wrapper: Wrapper });
573
594
  expect(hook.result.current).toBe('bundle_123');
574
595
  });
575
596
  it('should handle selected parameter in bundle when it differs from bundle id', async () => {
576
597
  mockLocation.pathname = '/bundles/bundle_123';
577
598
  mockParams.id = 'bundle_123';
578
- mockSearchParams.set('selected', 'different_hit_id');
599
+ mockSearchParams = new URLSearchParams({ selected: 'different_hit_id' });
579
600
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.selected), { wrapper: Wrapper });
580
601
  expect(hook.result.current).toBe('different_hit_id');
581
602
  });
582
603
  });
583
604
  describe('useParameterContextSelector', () => {
584
605
  it('should allow selecting specific values from context', async () => {
585
- mockSearchParams.set('query', 'test query');
586
- mockSearchParams.set('sort', 'test.field asc');
606
+ mockSearchParams = new URLSearchParams({
607
+ query: 'test query',
608
+ sort: 'test.field asc'
609
+ });
587
610
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
588
611
  query: ctx.query,
589
612
  sort: ctx.sort
@@ -594,17 +617,17 @@ describe('ParameterContext', () => {
594
617
  });
595
618
  describe('edge cases', () => {
596
619
  it('should handle offset of 0 in URL params', async () => {
597
- mockSearchParams.set('offset', '0');
620
+ mockSearchParams = new URLSearchParams({ offset: '0' });
598
621
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.offset), { wrapper: Wrapper });
599
622
  expect(hook.result.current).toBe(0);
600
623
  });
601
624
  it('should handle trackTotalHits with various values', async () => {
602
- mockSearchParams.set('track_total_hits', 'false');
625
+ mockSearchParams = new URLSearchParams({ track_total_hits: 'false' });
603
626
  let hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.trackTotalHits), {
604
627
  wrapper: Wrapper
605
628
  });
606
629
  expect(hook.result.current).toBe(false);
607
- mockSearchParams.set('track_total_hits', 'true');
630
+ mockSearchParams = new URLSearchParams({ track_total_hits: 'true' });
608
631
  hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.trackTotalHits), { wrapper: Wrapper });
609
632
  expect(hook.result.current).toBe(true);
610
633
  });
@@ -622,9 +645,11 @@ describe('ParameterContext', () => {
622
645
  expect(hook.result.current.endDate).toBeNull();
623
646
  });
624
647
  it('should fallback to default values when URL params are cleared', async () => {
625
- mockSearchParams.set('query', 'custom query');
626
- mockSearchParams.set('sort', 'custom.sort asc');
627
- mockSearchParams.set('span', 'date.range.1.week');
648
+ mockSearchParams = new URLSearchParams({
649
+ query: 'custom query',
650
+ sort: 'custom.sort asc',
651
+ span: 'date.range.1.week'
652
+ });
628
653
  mockLocation.search = mockSearchParams.toString();
629
654
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
630
655
  query: ctx.query,
@@ -633,9 +658,7 @@ describe('ParameterContext', () => {
633
658
  })), { wrapper: Wrapper });
634
659
  expect(hook.result.current.query).toBe('custom query');
635
660
  // Simulate clearing URL params
636
- for (const key of [...mockSearchParams.keys()]) {
637
- mockSearchParams.delete(key);
638
- }
661
+ mockSearchParams = new URLSearchParams();
639
662
  mockLocation.search = '';
640
663
  hook.rerender();
641
664
  await waitFor(() => {
@@ -663,7 +686,6 @@ describe('ParameterContext', () => {
663
686
  hook.result.current.setSpan('date.range.1.week');
664
687
  hook.result.current.addFilter('status:resolved');
665
688
  });
666
- hook.rerender();
667
689
  await waitFor(() => {
668
690
  expect(hook.result.current.query).toBe('multi query');
669
691
  expect(hook.result.current.sort).toBe('multi.sort desc');
@@ -684,308 +706,25 @@ describe('ParameterContext', () => {
684
706
  });
685
707
  });
686
708
  });
687
- describe('indexes (multi-index support)', () => {
688
- it('should initialize with default ["hit"] when no index params are present', async () => {
689
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
690
- expect(hook.result.current).toEqual(['hit']);
691
- });
692
- it('should initialize with single index from URL', async () => {
693
- mockSearchParams.set('index', 'observable');
694
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
695
- expect(hook.result.current).toEqual(['observable']);
696
- });
697
- it('should initialize with multiple indexes from URL', async () => {
698
- mockSearchParams.append('index', 'hit');
699
- mockSearchParams.append('index', 'observable');
700
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
701
- expect(hook.result.current).toEqual(['hit', 'observable']);
702
- });
703
- it('should deduplicate repeated index values from URL', async () => {
704
- mockSearchParams.append('index', 'hit');
705
- mockSearchParams.append('index', 'hit');
706
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
707
- expect(hook.result.current).toEqual(['hit']);
708
- });
709
- describe('addIndex', () => {
710
- it('should add an index to the default array', async () => {
711
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
712
- indexes: ctx.indexes,
713
- addIndex: ctx.addIndex
714
- })), { wrapper: Wrapper });
715
- await act(async () => {
716
- hook.result.current.addIndex('observable');
717
- });
718
- await waitFor(() => {
719
- expect(hook.result.current.indexes).toEqual(['hit', 'observable']);
720
- });
721
- });
722
- it('should not add a duplicate index', async () => {
723
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
724
- indexes: ctx.indexes,
725
- addIndex: ctx.addIndex
726
- })), { wrapper: Wrapper });
727
- await act(async () => {
728
- hook.result.current.addIndex('hit');
729
- });
730
- await waitFor(() => {
731
- expect(hook.result.current.indexes).toEqual(['hit']);
732
- });
733
- });
734
- });
735
- describe('removeIndex', () => {
736
- it('should remove an index from the list', async () => {
737
- mockSearchParams.append('index', 'hit');
738
- mockSearchParams.append('index', 'observable');
739
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
740
- indexes: ctx.indexes,
741
- removeIndex: ctx.removeIndex
742
- })), { wrapper: Wrapper });
743
- await act(async () => {
744
- hook.result.current.removeIndex('hit');
745
- });
746
- await waitFor(() => {
747
- expect(hook.result.current.indexes).toEqual(['observable']);
748
- });
749
- });
750
- it('should do nothing when removing a nonexistent index', async () => {
751
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
752
- indexes: ctx.indexes,
753
- removeIndex: ctx.removeIndex
754
- })), { wrapper: Wrapper });
755
- await act(async () => {
756
- hook.result.current.removeIndex('observable');
757
- });
758
- await waitFor(() => {
759
- expect(hook.result.current.indexes).toEqual(['hit']);
760
- });
761
- });
762
- it('should handle removing from empty array', async () => {
763
- mockSearchParams.append('index', 'hit');
764
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
765
- indexes: ctx.indexes,
766
- removeIndex: ctx.removeIndex
767
- })), { wrapper: Wrapper });
768
- await act(async () => {
769
- hook.result.current.removeIndex('hit');
770
- });
771
- await waitFor(() => {
772
- expect(hook.result.current.indexes).toEqual([]);
773
- });
774
- });
775
- });
776
- describe('setIndex', () => {
777
- it('should update the index at the specified position', async () => {
778
- mockSearchParams.append('index', 'hit');
779
- mockSearchParams.append('index', 'observable');
780
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
781
- indexes: ctx.indexes,
782
- setIndex: ctx.setIndex
783
- })), { wrapper: Wrapper });
784
- await act(async () => {
785
- hook.result.current.setIndex(0, 'observable');
786
- });
787
- await waitFor(() => {
788
- expect(hook.result.current.indexes).toEqual(['observable', 'observable']);
789
- });
790
- });
791
- it('should do nothing when index is out of bounds', async () => {
792
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
793
- indexes: ctx.indexes,
794
- setIndex: ctx.setIndex
795
- })), { wrapper: Wrapper });
796
- await act(async () => {
797
- hook.result.current.setIndex(5, 'observable');
798
- });
799
- await waitFor(() => {
800
- expect(hook.result.current.indexes).toEqual(['hit']);
801
- });
802
- });
803
- it('should do nothing when position is negative', async () => {
804
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
805
- indexes: ctx.indexes,
806
- setIndex: ctx.setIndex
807
- })), { wrapper: Wrapper });
808
- await act(async () => {
809
- hook.result.current.setIndex(-1, 'observable');
810
- });
811
- await waitFor(() => {
812
- expect(hook.result.current.indexes).toEqual(['hit']);
813
- });
814
- });
815
- });
816
- describe('setIndexes', () => {
817
- it('should replace all indexes with the provided list', async () => {
818
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
819
- indexes: ctx.indexes,
820
- setIndexes: ctx.setIndexes
821
- })), { wrapper: Wrapper });
822
- await act(async () => {
823
- hook.result.current.setIndexes(['observable']);
824
- });
825
- await waitFor(() => {
826
- expect(hook.result.current.indexes).toEqual(['observable']);
827
- });
828
- });
829
- it('should deduplicate values when setting all indexes', async () => {
830
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
831
- indexes: ctx.indexes,
832
- setIndexes: ctx.setIndexes
833
- })), { wrapper: Wrapper });
834
- await act(async () => {
835
- hook.result.current.setIndexes(['hit', 'hit', 'observable']);
836
- });
837
- await waitFor(() => {
838
- expect(hook.result.current.indexes).toEqual(['hit', 'observable']);
839
- });
840
- });
841
- it('should set to empty array', async () => {
842
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
843
- indexes: ctx.indexes,
844
- setIndexes: ctx.setIndexes
845
- })), { wrapper: Wrapper });
846
- await act(async () => {
847
- hook.result.current.setIndexes([]);
848
- });
849
- await waitFor(() => {
850
- expect(hook.result.current.indexes).toEqual([]);
851
- });
852
- });
853
- });
854
- describe('resetIndexes', () => {
855
- it('should reset indexes to default ["hit"]', async () => {
856
- mockSearchParams.append('index', 'hit');
857
- mockSearchParams.append('index', 'observable');
858
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
859
- indexes: ctx.indexes,
860
- resetIndexes: ctx.resetIndexes
861
- })), { wrapper: Wrapper });
862
- await act(async () => {
863
- hook.result.current.resetIndexes();
864
- });
865
- await waitFor(() => {
866
- expect(hook.result.current.indexes).toEqual(['hit']);
867
- });
868
- });
869
- it('should reset to default even when called on empty array', async () => {
870
- mockSearchParams.append('index', 'hit');
871
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
872
- indexes: ctx.indexes,
873
- removeIndex: ctx.removeIndex,
874
- resetIndexes: ctx.resetIndexes
875
- })), { wrapper: Wrapper });
876
- // First empty it
877
- await act(async () => {
878
- hook.result.current.removeIndex('hit');
879
- });
880
- await waitFor(() => {
881
- expect(hook.result.current.indexes).toEqual([]);
882
- });
883
- // Resetting always returns to default ['hit']
884
- await act(async () => {
885
- hook.result.current.resetIndexes();
886
- });
887
- await waitFor(() => {
888
- expect(hook.result.current.indexes).toEqual(['hit']);
889
- });
890
- });
891
- });
892
- describe('URL synchronization', () => {
893
- it('should not write the default ["hit"] index to the URL', async () => {
894
- renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
895
- // Allow any effects to flush
896
- await waitFor(() => {
897
- // If setParams was called, the URL must not contain ?index=hit
898
- if (mockSetParams.mock.calls.length > 0) {
899
- const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
900
- const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
901
- expect(urlParams.getAll('index')).toEqual([]);
902
- }
903
- else {
904
- // setParams not called at all is also fine
905
- expect(true).toBe(true);
906
- }
907
- });
908
- });
909
- it('should write a non-default index to the URL', async () => {
910
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
911
- addIndex: ctx.addIndex,
912
- resetIndexes: ctx.resetIndexes
913
- })), { wrapper: Wrapper });
914
- await act(async () => {
915
- hook.result.current.resetIndexes();
916
- hook.result.current.addIndex('observable');
917
- });
918
- await waitFor(() => {
919
- expect(mockSetParams).toHaveBeenCalled();
920
- const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
921
- const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
922
- expect(urlParams.getAll('index')).toEqual(['hit', 'observable']);
923
- });
924
- });
925
- it('should sync multiple indexes to URL as multiple index params', async () => {
926
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
927
- addIndex: ctx.addIndex
928
- })), { wrapper: Wrapper });
929
- await act(async () => {
930
- hook.result.current.addIndex('observable');
931
- });
932
- await waitFor(() => {
933
- expect(mockSetParams).toHaveBeenCalled();
934
- const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
935
- const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
936
- expect(urlParams.getAll('index')).toEqual(['hit', 'observable']);
937
- });
938
- });
939
- it('should remove all index params from URL when state resets to default', async () => {
940
- mockSearchParams.append('index', 'hit');
941
- mockSearchParams.append('index', 'observable');
942
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
943
- resetIndexes: ctx.resetIndexes
944
- })), { wrapper: Wrapper });
945
- await act(async () => {
946
- hook.result.current.resetIndexes();
947
- });
948
- await waitFor(() => {
949
- expect(mockSetParams).toHaveBeenCalled();
950
- const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
951
- const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
952
- expect(urlParams.getAll('index')).toEqual([]);
953
- });
954
- });
955
- it('should remove index param from URL when state returns to default', async () => {
956
- mockSearchParams.set('index', 'observable');
957
- const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
958
- setIndexes: ctx.setIndexes
959
- })), { wrapper: Wrapper });
960
- await act(async () => {
961
- hook.result.current.setIndexes(['hit']);
962
- });
963
- await waitFor(() => {
964
- expect(mockSetParams).toHaveBeenCalled();
965
- const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
966
- const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
967
- expect(urlParams.getAll('index')).toEqual([]);
968
- });
969
- });
970
- });
971
- });
972
709
  describe('views (multi-view support)', () => {
973
710
  it('should initialize with empty array when no view params present', async () => {
974
711
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
975
712
  expect(hook.result.current).toEqual([]);
976
713
  });
977
714
  it('should initialize with single view from URL', async () => {
978
- mockSearchParams.set('view', 'view_1');
715
+ mockSearchParams = new URLSearchParams({ view: 'view_1' });
979
716
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
980
717
  expect(hook.result.current).toEqual(['view_1']);
981
718
  });
982
719
  it('should initialize with multiple views from URL', async () => {
720
+ mockSearchParams = new URLSearchParams();
983
721
  mockSearchParams.append('view', 'view_1');
984
722
  mockSearchParams.append('view', 'view_2');
985
723
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
986
724
  expect(hook.result.current).toEqual(['view_1', 'view_2']);
987
725
  });
988
726
  it('should preserve view order from URL', async () => {
727
+ mockSearchParams = new URLSearchParams();
989
728
  mockSearchParams.append('view', 'view_c');
990
729
  mockSearchParams.append('view', 'view_a');
991
730
  mockSearchParams.append('view', 'view_b');
@@ -993,6 +732,7 @@ describe('ParameterContext', () => {
993
732
  expect(hook.result.current).toEqual(['view_c', 'view_a', 'view_b']);
994
733
  });
995
734
  it('should deduplicate multiple empty view params to single empty string', async () => {
735
+ mockSearchParams = new URLSearchParams();
996
736
  mockSearchParams.append('view', '');
997
737
  mockSearchParams.append('view', '');
998
738
  mockSearchParams.append('view', '');
@@ -1013,7 +753,7 @@ describe('ParameterContext', () => {
1013
753
  });
1014
754
  });
1015
755
  it('should append view to existing views', async () => {
1016
- mockSearchParams.set('view', 'existing_view');
756
+ mockSearchParams = new URLSearchParams({ view: 'existing_view' });
1017
757
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1018
758
  views: ctx.views,
1019
759
  addView: ctx.addView
@@ -1026,7 +766,7 @@ describe('ParameterContext', () => {
1026
766
  });
1027
767
  });
1028
768
  it('should not add duplicate views', async () => {
1029
- mockSearchParams.set('view', 'view_1');
769
+ mockSearchParams = new URLSearchParams({ view: 'view_1' });
1030
770
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1031
771
  views: ctx.views,
1032
772
  addView: ctx.addView
@@ -1042,6 +782,7 @@ describe('ParameterContext', () => {
1042
782
  });
1043
783
  describe('removeView', () => {
1044
784
  it('should remove first matching view', async () => {
785
+ mockSearchParams = new URLSearchParams();
1045
786
  mockSearchParams.append('view', 'view_1');
1046
787
  mockSearchParams.append('view', 'view_2');
1047
788
  mockSearchParams.append('view', 'view_3');
@@ -1057,6 +798,7 @@ describe('ParameterContext', () => {
1057
798
  });
1058
799
  });
1059
800
  it('should remove only first occurrence of duplicate views', async () => {
801
+ mockSearchParams = new URLSearchParams();
1060
802
  mockSearchParams.append('view', 'dup');
1061
803
  mockSearchParams.append('view', 'dup');
1062
804
  mockSearchParams.append('view', 'other');
@@ -1072,7 +814,7 @@ describe('ParameterContext', () => {
1072
814
  });
1073
815
  });
1074
816
  it('should do nothing when removing nonexistent view', async () => {
1075
- mockSearchParams.set('view', 'existing');
817
+ mockSearchParams = new URLSearchParams({ view: 'existing' });
1076
818
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1077
819
  views: ctx.views,
1078
820
  removeView: ctx.removeView
@@ -1097,16 +839,17 @@ describe('ParameterContext', () => {
1097
839
  });
1098
840
  });
1099
841
  });
1100
- describe('resetViews', () => {
842
+ describe('clearViews', () => {
1101
843
  it('should clear all views', async () => {
844
+ mockSearchParams = new URLSearchParams();
1102
845
  mockSearchParams.append('view', 'view_1');
1103
846
  mockSearchParams.append('view', 'view_2');
1104
847
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1105
848
  views: ctx.views,
1106
- resetViews: ctx.resetViews
849
+ clearViews: ctx.clearViews
1107
850
  })), { wrapper: Wrapper });
1108
851
  await act(async () => {
1109
- hook.result.current.resetViews();
852
+ hook.result.current.clearViews();
1110
853
  });
1111
854
  await waitFor(() => {
1112
855
  expect(hook.result.current.views).toEqual([]);
@@ -1115,10 +858,10 @@ describe('ParameterContext', () => {
1115
858
  it('should be no-op when already empty', async () => {
1116
859
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1117
860
  views: ctx.views,
1118
- resetViews: ctx.resetViews
861
+ clearViews: ctx.clearViews
1119
862
  })), { wrapper: Wrapper });
1120
863
  await act(async () => {
1121
- hook.result.current.resetViews();
864
+ hook.result.current.clearViews();
1122
865
  });
1123
866
  await waitFor(() => {
1124
867
  expect(hook.result.current.views).toEqual([]);
@@ -1127,6 +870,7 @@ describe('ParameterContext', () => {
1127
870
  });
1128
871
  describe('setView', () => {
1129
872
  it('should update view at specified index', async () => {
873
+ mockSearchParams = new URLSearchParams();
1130
874
  mockSearchParams.append('view', 'view_1');
1131
875
  mockSearchParams.append('view', 'view_2');
1132
876
  mockSearchParams.append('view', 'view_3');
@@ -1142,6 +886,7 @@ describe('ParameterContext', () => {
1142
886
  });
1143
887
  });
1144
888
  it('should update view at index 0', async () => {
889
+ mockSearchParams = new URLSearchParams();
1145
890
  mockSearchParams.append('view', 'old_view');
1146
891
  mockSearchParams.append('view', 'view_2');
1147
892
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
@@ -1156,6 +901,7 @@ describe('ParameterContext', () => {
1156
901
  });
1157
902
  });
1158
903
  it('should update view at last index', async () => {
904
+ mockSearchParams = new URLSearchParams();
1159
905
  mockSearchParams.append('view', 'view_1');
1160
906
  mockSearchParams.append('view', 'old_last');
1161
907
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
@@ -1170,7 +916,7 @@ describe('ParameterContext', () => {
1170
916
  });
1171
917
  });
1172
918
  it('should do nothing when index is out of bounds', async () => {
1173
- mockSearchParams.set('view', 'existing');
919
+ mockSearchParams = new URLSearchParams({ view: 'existing' });
1174
920
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1175
921
  views: ctx.views,
1176
922
  setView: ctx.setView
@@ -1183,7 +929,7 @@ describe('ParameterContext', () => {
1183
929
  });
1184
930
  });
1185
931
  it('should do nothing when index is negative', async () => {
1186
- mockSearchParams.set('view', 'existing');
932
+ mockSearchParams = new URLSearchParams({ view: 'existing' });
1187
933
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1188
934
  views: ctx.views,
1189
935
  setView: ctx.setView
@@ -1208,6 +954,7 @@ describe('ParameterContext', () => {
1208
954
  });
1209
955
  });
1210
956
  it('should sync updated view to URL', async () => {
957
+ mockSearchParams = new URLSearchParams();
1211
958
  mockSearchParams.append('view', 'view_1');
1212
959
  mockSearchParams.append('view', 'view_2');
1213
960
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
@@ -1257,13 +1004,14 @@ describe('ParameterContext', () => {
1257
1004
  });
1258
1005
  });
1259
1006
  it('should remove all view params when views is empty', async () => {
1007
+ mockSearchParams = new URLSearchParams();
1260
1008
  mockSearchParams.append('view', 'view_1');
1261
1009
  mockSearchParams.append('view', 'view_2');
1262
1010
  const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
1263
- resetViews: ctx.resetViews
1011
+ clearViews: ctx.clearViews
1264
1012
  })), { wrapper: Wrapper });
1265
1013
  await act(async () => {
1266
- hook.result.current.resetViews();
1014
+ hook.result.current.clearViews();
1267
1015
  });
1268
1016
  await waitFor(() => {
1269
1017
  expect(mockSetParams).toHaveBeenCalled();