@cccsaurora/howler-ui 2.18.0-dev.740 → 2.18.0-dev.748
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/index.d.ts +2 -0
- package/api/index.js +4 -2
- package/api/search/case.d.ts +4 -0
- package/api/search/case.js +8 -0
- package/api/search/facet/hit.d.ts +1 -3
- package/api/search/facet/index.d.ts +3 -1
- package/api/search/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/v2/case/index.d.ts +8 -0
- package/api/v2/case/index.js +20 -0
- package/api/v2/case/items.d.ts +6 -0
- package/api/v2/case/items.js +18 -0
- package/api/v2/index.d.ts +4 -0
- package/api/v2/index.js +6 -0
- package/api/v2/search/facet.d.ts +3 -0
- package/api/v2/search/facet.js +12 -0
- package/api/v2/search/index.d.ts +5 -0
- package/api/v2/search/index.js +24 -0
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +39 -7
- package/components/app/hooks/useMatchers.d.ts +1 -1
- package/components/app/hooks/useMatchers.js +23 -11
- package/components/app/hooks/useMatchers.test.js +22 -22
- package/components/app/hooks/useTitle.js +3 -3
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/ModalProvider.d.ts +1 -0
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +346 -94
- package/components/app/providers/RecordProvider.d.ts +23 -0
- package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
- package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
- package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
- package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +52 -71
- package/components/app/providers/UserListProvider.js +28 -8
- package/components/elements/ContextMenu.d.ts +56 -0
- package/components/elements/ContextMenu.js +109 -0
- package/components/elements/ContextMenu.test.js +215 -0
- package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
- package/components/elements/ObjectDetails.d.ts +6 -0
- package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
- package/components/elements/PluginTypography.d.ts +2 -1
- package/components/elements/PluginTypography.js +3 -2
- package/components/elements/UserList.d.ts +5 -2
- package/components/elements/UserList.js +18 -8
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/case/CaseCard.d.ts +12 -0
- package/components/elements/case/CaseCard.js +42 -0
- package/components/elements/case/CasePreview.d.ts +6 -0
- package/components/elements/case/CasePreview.js +17 -0
- package/components/elements/case/StatusIcon.d.ts +5 -0
- package/components/elements/case/StatusIcon.js +13 -0
- package/components/elements/display/ChipPopper.d.ts +1 -1
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +2 -0
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.d.ts +1 -0
- package/components/elements/hit/HitBanner.js +29 -49
- package/components/elements/hit/HitCard.d.ts +2 -0
- package/components/elements/hit/HitCard.js +7 -7
- package/components/elements/hit/HitLabels.js +2 -2
- package/components/elements/hit/HitOutline.d.ts +1 -0
- package/components/elements/hit/HitOutline.js +3 -3
- package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
- package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
- package/components/elements/hit/HitSummary.d.ts +2 -1
- package/components/elements/hit/HitSummary.js +6 -5
- package/components/elements/hit/aggregate/HitGraph.js +8 -8
- package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/hit/related/RelatedRecords.js +63 -0
- package/components/elements/observable/ObservableCard.d.ts +6 -0
- package/components/elements/observable/ObservableCard.js +22 -0
- package/components/elements/observable/ObservablePreview.d.ts +6 -0
- package/components/elements/observable/ObservablePreview.js +12 -0
- package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
- package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
- package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
- package/components/elements/record/RecordContextMenu.js +247 -0
- package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
- package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +94 -39
- package/components/elements/record/RecordRelated.d.ts +7 -0
- package/components/elements/record/RecordRelated.js +34 -0
- package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
- package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
- package/components/elements/view/ViewTitle.d.ts +1 -0
- package/components/elements/view/ViewTitle.js +9 -2
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +4 -4
- package/components/hooks/useMyPreferences.js +10 -1
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +4 -1
- package/components/hooks/useMyTheme.js +9 -2
- package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
- package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
- package/components/hooks/useRelatedRecords.d.ts +13 -0
- package/components/hooks/useRelatedRecords.js +32 -0
- package/components/routes/action/edit/ActionEditor.js +2 -2
- package/components/routes/action/view/ActionSearch.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/advanced/QueryEditor.js +3 -3
- package/components/routes/advanced/historyCompletionProvider.js +3 -3
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +22 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +101 -0
- package/components/routes/cases/constants.d.ts +5 -0
- package/components/routes/cases/constants.js +5 -0
- package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
- package/components/routes/cases/detail/AlertPanel.js +33 -0
- package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
- package/components/routes/cases/detail/CaseAssets.js +104 -0
- package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseAssets.test.js +167 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +66 -0
- package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
- package/components/routes/cases/detail/CaseDetails.js +61 -0
- package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
- package/components/routes/cases/detail/CaseOverview.js +43 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
- package/components/routes/cases/detail/CaseSidebar.js +107 -0
- package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseSidebar.test.js +246 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +57 -0
- package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
- package/components/routes/cases/detail/CaseTimeline.js +106 -0
- package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseTimeline.test.js +227 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +99 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
- package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
- package/components/routes/cases/detail/TaskPanel.js +52 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
- package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
- package/components/routes/cases/detail/assets/Asset.js +12 -0
- package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
- package/components/routes/cases/detail/assets/Asset.test.js +72 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
- package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
- package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.js +29 -0
- package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
- package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +51 -0
- package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
- package/components/routes/cases/modals/AddToCaseModal.js +62 -0
- package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
- package/components/routes/cases/modals/RenameItemModal.js +48 -0
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +115 -0
- package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
- package/components/routes/cases/modals/ResolveModal.test.js +384 -0
- package/components/routes/dossiers/DossierEditor.js +2 -2
- package/components/routes/dossiers/DossierEditor.test.js +1 -1
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitBannerDocumentation.js +1 -0
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +47 -60
- package/components/routes/hits/search/LayoutSettings.js +3 -3
- package/components/routes/hits/search/QuerySettings.js +2 -1
- package/components/routes/hits/search/QuerySettings.test.js +14 -9
- package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
- package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
- package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
- package/components/routes/hits/search/SearchPane.js +26 -49
- package/components/routes/hits/search/ViewLink.js +3 -3
- package/components/routes/hits/search/ViewLink.test.js +8 -8
- package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitGrid.js +20 -18
- package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
- package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
- package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
- package/components/routes/hits/search/shared/IndexPicker.js +20 -0
- package/components/routes/hits/view/HitViewer.js +12 -13
- package/components/routes/home/ViewCard.js +47 -41
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/components/routes/views/ViewComposer.js +46 -19
- package/locales/en/translation.json +89 -3
- package/locales/fr/translation.json +87 -3
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +28 -0
- package/models/entities/generated/DestinationOriginal.d.ts +19 -0
- package/models/entities/generated/EmailAttachment.d.ts +8 -0
- package/models/entities/generated/EmailParent.d.ts +19 -0
- package/models/entities/generated/Enrichments.d.ts +7 -0
- package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
- package/models/entities/generated/Hit.d.ts +1 -0
- package/models/entities/generated/Howler.d.ts +0 -4
- package/models/entities/generated/HttpResponse.d.ts +11 -0
- package/models/entities/generated/Item.d.ts +9 -0
- package/models/entities/generated/Observable.d.ts +85 -0
- package/models/entities/generated/ObservableCloud.d.ts +20 -0
- package/models/entities/generated/ObservableDestination.d.ts +23 -0
- package/models/entities/generated/ObservableEmail.d.ts +30 -0
- package/models/entities/generated/ObservableFile.d.ts +36 -0
- package/models/entities/generated/ObservableHowler.d.ts +43 -0
- package/models/entities/generated/ObservableHttp.d.ts +11 -0
- package/models/entities/generated/ObservableObserver.d.ts +21 -0
- package/models/entities/generated/ObservableOrganization.d.ts +7 -0
- package/models/entities/generated/ObservableProcess.d.ts +34 -0
- package/models/entities/generated/ObservableSource.d.ts +23 -0
- package/models/entities/generated/ObservableThreat.d.ts +21 -0
- package/models/entities/generated/ObservableTls.d.ts +12 -0
- package/models/entities/generated/ObserverIngress.d.ts +9 -0
- package/models/entities/generated/Rule.d.ts +2 -10
- package/models/entities/generated/Task.d.ts +10 -0
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
- package/models/entities/generated/View.d.ts +1 -0
- package/package.json +18 -1
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/tests/mocks.d.ts +11 -1
- package/tests/mocks.js +12 -7
- package/tests/server-handlers.js +6 -1
- package/tests/utils.d.ts +4 -0
- package/tests/utils.js +20 -0
- package/utils/constants.d.ts +3 -3
- package/utils/hitFunctions.d.ts +2 -1
- package/utils/hitFunctions.js +4 -4
- package/utils/typeUtils.d.ts +7 -0
- package/utils/typeUtils.js +27 -0
- package/utils/viewUtils.js +3 -0
- package/components/app/providers/HitProvider.d.ts +0 -22
- package/components/elements/display/icons/BundleButton.d.ts +0 -6
- package/components/elements/display/icons/BundleButton.js +0 -32
- package/components/elements/hit/HitRelated.d.ts +0 -6
- package/components/elements/hit/HitRelated.js +0 -7
- package/components/routes/help/BundleDocumentation.d.ts +0 -3
- package/components/routes/help/BundleDocumentation.js +0 -12
- package/components/routes/help/markdown/en/bundles.md.js +0 -1
- package/components/routes/help/markdown/fr/bundles.md.js +0 -1
- package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
- package/components/routes/hits/search/BundleParentMenu.js +0 -32
- package/components/routes/hits/search/BundleScroller.d.ts +0 -2
- package/components/routes/hits/search/BundleScroller.js +0 -6
- package/components/routes/hits/search/HitContextMenu.js +0 -227
- /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
- /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
- /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
- /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
- /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
|
@@ -1,23 +1,18 @@
|
|
|
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';
|
|
3
4
|
import { useContextSelector } from 'use-context-selector';
|
|
4
5
|
import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
|
|
5
6
|
import ParameterProvider, { ParameterContext } from './ParameterProvider';
|
|
6
7
|
// Mock dependencies
|
|
7
|
-
const mockSetParams =
|
|
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
|
-
}));
|
|
8
|
+
const { mockParams, mockLocation, mockSetParams, mockSearchParams } = setupReactRouterMock();
|
|
16
9
|
const Wrapper = ({ children }) => {
|
|
17
10
|
return _jsx(ParameterProvider, { children: children });
|
|
18
11
|
};
|
|
19
12
|
beforeEach(() => {
|
|
20
|
-
|
|
13
|
+
for (const key of [...mockSearchParams.keys()]) {
|
|
14
|
+
mockSearchParams.delete(key);
|
|
15
|
+
}
|
|
21
16
|
mockSetParams.mockClear();
|
|
22
17
|
mockLocation.pathname = '/hits';
|
|
23
18
|
mockLocation.search = '';
|
|
@@ -39,15 +34,13 @@ describe('ParameterContext', () => {
|
|
|
39
34
|
expect(hook.result.current.trackTotalHits).toBe(false);
|
|
40
35
|
});
|
|
41
36
|
it('should initialize with values from URL params', async () => {
|
|
42
|
-
mockSearchParams
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
track_total_hits: 'true'
|
|
50
|
-
});
|
|
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');
|
|
51
44
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
52
45
|
query: ctx.query,
|
|
53
46
|
sort: ctx.sort,
|
|
@@ -66,11 +59,9 @@ describe('ParameterContext', () => {
|
|
|
66
59
|
expect(hook.result.current.trackTotalHits).toBe(true);
|
|
67
60
|
});
|
|
68
61
|
it('should handle custom date span with start and end dates', async () => {
|
|
69
|
-
mockSearchParams
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end_date: '2025-12-31'
|
|
73
|
-
});
|
|
62
|
+
mockSearchParams.set('span', 'date.range.custom');
|
|
63
|
+
mockSearchParams.set('start_date', '2025-01-01');
|
|
64
|
+
mockSearchParams.set('end_date', '2025-12-31');
|
|
74
65
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
75
66
|
span: ctx.span,
|
|
76
67
|
startDate: ctx.startDate,
|
|
@@ -94,7 +85,7 @@ describe('ParameterContext', () => {
|
|
|
94
85
|
});
|
|
95
86
|
});
|
|
96
87
|
it('should not update if the value is the same', async () => {
|
|
97
|
-
mockSearchParams
|
|
88
|
+
mockSearchParams.set('query', 'existing query');
|
|
98
89
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
99
90
|
query: ctx.query,
|
|
100
91
|
setQuery: ctx.setQuery
|
|
@@ -134,11 +125,9 @@ describe('ParameterContext', () => {
|
|
|
134
125
|
});
|
|
135
126
|
});
|
|
136
127
|
it('should clear startDate and endDate when span does not end with custom', async () => {
|
|
137
|
-
mockSearchParams
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
end_date: '2025-12-31'
|
|
141
|
-
});
|
|
128
|
+
mockSearchParams.set('span', 'date.range.custom');
|
|
129
|
+
mockSearchParams.set('start_date', '2025-01-01');
|
|
130
|
+
mockSearchParams.set('end_date', '2025-12-31');
|
|
142
131
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
143
132
|
span: ctx.span,
|
|
144
133
|
startDate: ctx.startDate,
|
|
@@ -161,19 +150,17 @@ describe('ParameterContext', () => {
|
|
|
161
150
|
expect(hook.result.current).toEqual([]);
|
|
162
151
|
});
|
|
163
152
|
it('should initialize with single filter from URL', async () => {
|
|
164
|
-
mockSearchParams
|
|
153
|
+
mockSearchParams.set('filter', 'status:open');
|
|
165
154
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.filters), { wrapper: Wrapper });
|
|
166
155
|
expect(hook.result.current).toEqual(['status:open']);
|
|
167
156
|
});
|
|
168
157
|
it('should initialize with multiple filters from URL', async () => {
|
|
169
|
-
mockSearchParams = new URLSearchParams();
|
|
170
158
|
mockSearchParams.append('filter', 'howler.escalation:hit');
|
|
171
159
|
mockSearchParams.append('filter', 'howler.assignment:someuser');
|
|
172
160
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.filters), { wrapper: Wrapper });
|
|
173
161
|
expect(hook.result.current).toEqual(['howler.escalation:hit', 'howler.assignment:someuser']);
|
|
174
162
|
});
|
|
175
163
|
it('should preserve filter order from URL', async () => {
|
|
176
|
-
mockSearchParams = new URLSearchParams();
|
|
177
164
|
mockSearchParams.append('filter', 'c');
|
|
178
165
|
mockSearchParams.append('filter', 'a');
|
|
179
166
|
mockSearchParams.append('filter', 'b');
|
|
@@ -181,7 +168,6 @@ describe('ParameterContext', () => {
|
|
|
181
168
|
expect(hook.result.current).toEqual(['c', 'a', 'b']);
|
|
182
169
|
});
|
|
183
170
|
it('should deduplicate multiple empty filter params to single empty string', async () => {
|
|
184
|
-
mockSearchParams = new URLSearchParams();
|
|
185
171
|
mockSearchParams.append('filter', '');
|
|
186
172
|
mockSearchParams.append('filter', '');
|
|
187
173
|
mockSearchParams.append('filter', '');
|
|
@@ -202,7 +188,7 @@ describe('ParameterContext', () => {
|
|
|
202
188
|
});
|
|
203
189
|
});
|
|
204
190
|
it('should append filter to existing filters', async () => {
|
|
205
|
-
mockSearchParams
|
|
191
|
+
mockSearchParams.set('filter', 'existing:filter');
|
|
206
192
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
207
193
|
filters: ctx.filters,
|
|
208
194
|
addFilter: ctx.addFilter
|
|
@@ -215,7 +201,7 @@ describe('ParameterContext', () => {
|
|
|
215
201
|
});
|
|
216
202
|
});
|
|
217
203
|
it('should not add duplicate filters', async () => {
|
|
218
|
-
mockSearchParams
|
|
204
|
+
mockSearchParams.set('filter', 'status:open');
|
|
219
205
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
220
206
|
filters: ctx.filters,
|
|
221
207
|
addFilter: ctx.addFilter
|
|
@@ -231,7 +217,6 @@ describe('ParameterContext', () => {
|
|
|
231
217
|
});
|
|
232
218
|
describe('removeFilter', () => {
|
|
233
219
|
it('should remove first matching filter', async () => {
|
|
234
|
-
mockSearchParams = new URLSearchParams();
|
|
235
220
|
mockSearchParams.append('filter', 'filter1');
|
|
236
221
|
mockSearchParams.append('filter', 'filter2');
|
|
237
222
|
mockSearchParams.append('filter', 'filter3');
|
|
@@ -247,7 +232,7 @@ describe('ParameterContext', () => {
|
|
|
247
232
|
});
|
|
248
233
|
});
|
|
249
234
|
it('should do nothing when removing nonexistent filter', async () => {
|
|
250
|
-
mockSearchParams
|
|
235
|
+
mockSearchParams.set('filter', 'existing');
|
|
251
236
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
252
237
|
filters: ctx.filters,
|
|
253
238
|
removeFilter: ctx.removeFilter
|
|
@@ -272,17 +257,16 @@ describe('ParameterContext', () => {
|
|
|
272
257
|
});
|
|
273
258
|
});
|
|
274
259
|
});
|
|
275
|
-
describe('
|
|
260
|
+
describe('resetFilters', () => {
|
|
276
261
|
it('should clear all filters', async () => {
|
|
277
|
-
mockSearchParams = new URLSearchParams();
|
|
278
262
|
mockSearchParams.append('filter', 'filter1');
|
|
279
263
|
mockSearchParams.append('filter', 'filter2');
|
|
280
264
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
281
265
|
filters: ctx.filters,
|
|
282
|
-
|
|
266
|
+
resetFilters: ctx.resetFilters
|
|
283
267
|
})), { wrapper: Wrapper });
|
|
284
268
|
await act(async () => {
|
|
285
|
-
hook.result.current.
|
|
269
|
+
hook.result.current.resetFilters();
|
|
286
270
|
});
|
|
287
271
|
await waitFor(() => {
|
|
288
272
|
expect(hook.result.current.filters).toEqual([]);
|
|
@@ -291,10 +275,10 @@ describe('ParameterContext', () => {
|
|
|
291
275
|
it('should be no-op when already empty', async () => {
|
|
292
276
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
293
277
|
filters: ctx.filters,
|
|
294
|
-
|
|
278
|
+
resetFilters: ctx.resetFilters
|
|
295
279
|
})), { wrapper: Wrapper });
|
|
296
280
|
await act(async () => {
|
|
297
|
-
hook.result.current.
|
|
281
|
+
hook.result.current.resetFilters();
|
|
298
282
|
});
|
|
299
283
|
await waitFor(() => {
|
|
300
284
|
expect(hook.result.current.filters).toEqual([]);
|
|
@@ -303,7 +287,6 @@ describe('ParameterContext', () => {
|
|
|
303
287
|
});
|
|
304
288
|
describe('setFilter', () => {
|
|
305
289
|
it('should update filter at specified index', async () => {
|
|
306
|
-
mockSearchParams = new URLSearchParams();
|
|
307
290
|
mockSearchParams.append('filter', 'filter1');
|
|
308
291
|
mockSearchParams.append('filter', 'filter2');
|
|
309
292
|
mockSearchParams.append('filter', 'filter3');
|
|
@@ -319,7 +302,6 @@ describe('ParameterContext', () => {
|
|
|
319
302
|
});
|
|
320
303
|
});
|
|
321
304
|
it('should update filter at index 0', async () => {
|
|
322
|
-
mockSearchParams = new URLSearchParams();
|
|
323
305
|
mockSearchParams.append('filter', 'old:filter');
|
|
324
306
|
mockSearchParams.append('filter', 'filter2');
|
|
325
307
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
@@ -334,7 +316,6 @@ describe('ParameterContext', () => {
|
|
|
334
316
|
});
|
|
335
317
|
});
|
|
336
318
|
it('should update filter at last index', async () => {
|
|
337
|
-
mockSearchParams = new URLSearchParams();
|
|
338
319
|
mockSearchParams.append('filter', 'filter1');
|
|
339
320
|
mockSearchParams.append('filter', 'old:last');
|
|
340
321
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
@@ -349,7 +330,7 @@ describe('ParameterContext', () => {
|
|
|
349
330
|
});
|
|
350
331
|
});
|
|
351
332
|
it('should do nothing when index is out of bounds', async () => {
|
|
352
|
-
mockSearchParams
|
|
333
|
+
mockSearchParams.set('filter', 'existing');
|
|
353
334
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
354
335
|
filters: ctx.filters,
|
|
355
336
|
setFilter: ctx.setFilter
|
|
@@ -362,7 +343,7 @@ describe('ParameterContext', () => {
|
|
|
362
343
|
});
|
|
363
344
|
});
|
|
364
345
|
it('should do nothing when index is negative', async () => {
|
|
365
|
-
mockSearchParams
|
|
346
|
+
mockSearchParams.set('filter', 'existing');
|
|
366
347
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
367
348
|
filters: ctx.filters,
|
|
368
349
|
setFilter: ctx.setFilter
|
|
@@ -387,7 +368,6 @@ describe('ParameterContext', () => {
|
|
|
387
368
|
});
|
|
388
369
|
});
|
|
389
370
|
it('should sync updated filter to URL', async () => {
|
|
390
|
-
mockSearchParams = new URLSearchParams();
|
|
391
371
|
mockSearchParams.append('filter', 'filter1');
|
|
392
372
|
mockSearchParams.append('filter', 'filter2');
|
|
393
373
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
@@ -437,14 +417,13 @@ describe('ParameterContext', () => {
|
|
|
437
417
|
});
|
|
438
418
|
});
|
|
439
419
|
it('should remove all filter params when filters is empty', async () => {
|
|
440
|
-
mockSearchParams = new URLSearchParams();
|
|
441
420
|
mockSearchParams.append('filter', 'filter1');
|
|
442
421
|
mockSearchParams.append('filter', 'filter2');
|
|
443
422
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
444
|
-
|
|
423
|
+
resetFilters: ctx.resetFilters
|
|
445
424
|
})), { wrapper: Wrapper });
|
|
446
425
|
await act(async () => {
|
|
447
|
-
hook.result.current.
|
|
426
|
+
hook.result.current.resetFilters();
|
|
448
427
|
});
|
|
449
428
|
await waitFor(() => {
|
|
450
429
|
expect(mockSetParams).toHaveBeenCalled();
|
|
@@ -567,11 +546,11 @@ describe('ParameterContext', () => {
|
|
|
567
546
|
});
|
|
568
547
|
});
|
|
569
548
|
it('should read changes from URL params', async () => {
|
|
570
|
-
mockSearchParams
|
|
549
|
+
mockSearchParams.set('query', 'initial query');
|
|
571
550
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.query), { wrapper: Wrapper });
|
|
572
551
|
expect(hook.result.current).toBe('initial query');
|
|
573
552
|
// Simulate URL change
|
|
574
|
-
mockSearchParams
|
|
553
|
+
mockSearchParams.set('query', 'updated query');
|
|
575
554
|
mockLocation.search = '?query=updated%20query';
|
|
576
555
|
hook.rerender();
|
|
577
556
|
await waitFor(() => {
|
|
@@ -589,24 +568,22 @@ describe('ParameterContext', () => {
|
|
|
589
568
|
it('should handle selected parameter in bundle when it matches bundle id', async () => {
|
|
590
569
|
mockLocation.pathname = '/bundles/bundle_123';
|
|
591
570
|
mockParams.id = 'bundle_123';
|
|
592
|
-
mockSearchParams
|
|
571
|
+
mockSearchParams.set('selected', 'bundle_123');
|
|
593
572
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.selected), { wrapper: Wrapper });
|
|
594
573
|
expect(hook.result.current).toBe('bundle_123');
|
|
595
574
|
});
|
|
596
575
|
it('should handle selected parameter in bundle when it differs from bundle id', async () => {
|
|
597
576
|
mockLocation.pathname = '/bundles/bundle_123';
|
|
598
577
|
mockParams.id = 'bundle_123';
|
|
599
|
-
mockSearchParams
|
|
578
|
+
mockSearchParams.set('selected', 'different_hit_id');
|
|
600
579
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.selected), { wrapper: Wrapper });
|
|
601
580
|
expect(hook.result.current).toBe('different_hit_id');
|
|
602
581
|
});
|
|
603
582
|
});
|
|
604
583
|
describe('useParameterContextSelector', () => {
|
|
605
584
|
it('should allow selecting specific values from context', async () => {
|
|
606
|
-
mockSearchParams
|
|
607
|
-
|
|
608
|
-
sort: 'test.field asc'
|
|
609
|
-
});
|
|
585
|
+
mockSearchParams.set('query', 'test query');
|
|
586
|
+
mockSearchParams.set('sort', 'test.field asc');
|
|
610
587
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
611
588
|
query: ctx.query,
|
|
612
589
|
sort: ctx.sort
|
|
@@ -617,17 +594,17 @@ describe('ParameterContext', () => {
|
|
|
617
594
|
});
|
|
618
595
|
describe('edge cases', () => {
|
|
619
596
|
it('should handle offset of 0 in URL params', async () => {
|
|
620
|
-
mockSearchParams
|
|
597
|
+
mockSearchParams.set('offset', '0');
|
|
621
598
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.offset), { wrapper: Wrapper });
|
|
622
599
|
expect(hook.result.current).toBe(0);
|
|
623
600
|
});
|
|
624
601
|
it('should handle trackTotalHits with various values', async () => {
|
|
625
|
-
mockSearchParams
|
|
602
|
+
mockSearchParams.set('track_total_hits', 'false');
|
|
626
603
|
let hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.trackTotalHits), {
|
|
627
604
|
wrapper: Wrapper
|
|
628
605
|
});
|
|
629
606
|
expect(hook.result.current).toBe(false);
|
|
630
|
-
mockSearchParams
|
|
607
|
+
mockSearchParams.set('track_total_hits', 'true');
|
|
631
608
|
hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.trackTotalHits), { wrapper: Wrapper });
|
|
632
609
|
expect(hook.result.current).toBe(true);
|
|
633
610
|
});
|
|
@@ -645,11 +622,9 @@ describe('ParameterContext', () => {
|
|
|
645
622
|
expect(hook.result.current.endDate).toBeNull();
|
|
646
623
|
});
|
|
647
624
|
it('should fallback to default values when URL params are cleared', async () => {
|
|
648
|
-
mockSearchParams
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
span: 'date.range.1.week'
|
|
652
|
-
});
|
|
625
|
+
mockSearchParams.set('query', 'custom query');
|
|
626
|
+
mockSearchParams.set('sort', 'custom.sort asc');
|
|
627
|
+
mockSearchParams.set('span', 'date.range.1.week');
|
|
653
628
|
mockLocation.search = mockSearchParams.toString();
|
|
654
629
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
655
630
|
query: ctx.query,
|
|
@@ -658,7 +633,9 @@ describe('ParameterContext', () => {
|
|
|
658
633
|
})), { wrapper: Wrapper });
|
|
659
634
|
expect(hook.result.current.query).toBe('custom query');
|
|
660
635
|
// Simulate clearing URL params
|
|
661
|
-
|
|
636
|
+
for (const key of [...mockSearchParams.keys()]) {
|
|
637
|
+
mockSearchParams.delete(key);
|
|
638
|
+
}
|
|
662
639
|
mockLocation.search = '';
|
|
663
640
|
hook.rerender();
|
|
664
641
|
await waitFor(() => {
|
|
@@ -686,6 +663,7 @@ describe('ParameterContext', () => {
|
|
|
686
663
|
hook.result.current.setSpan('date.range.1.week');
|
|
687
664
|
hook.result.current.addFilter('status:resolved');
|
|
688
665
|
});
|
|
666
|
+
hook.rerender();
|
|
689
667
|
await waitFor(() => {
|
|
690
668
|
expect(hook.result.current.query).toBe('multi query');
|
|
691
669
|
expect(hook.result.current.sort).toBe('multi.sort desc');
|
|
@@ -706,25 +684,308 @@ describe('ParameterContext', () => {
|
|
|
706
684
|
});
|
|
707
685
|
});
|
|
708
686
|
});
|
|
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
|
+
});
|
|
709
972
|
describe('views (multi-view support)', () => {
|
|
710
973
|
it('should initialize with empty array when no view params present', async () => {
|
|
711
974
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
|
|
712
975
|
expect(hook.result.current).toEqual([]);
|
|
713
976
|
});
|
|
714
977
|
it('should initialize with single view from URL', async () => {
|
|
715
|
-
mockSearchParams
|
|
978
|
+
mockSearchParams.set('view', 'view_1');
|
|
716
979
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
|
|
717
980
|
expect(hook.result.current).toEqual(['view_1']);
|
|
718
981
|
});
|
|
719
982
|
it('should initialize with multiple views from URL', async () => {
|
|
720
|
-
mockSearchParams = new URLSearchParams();
|
|
721
983
|
mockSearchParams.append('view', 'view_1');
|
|
722
984
|
mockSearchParams.append('view', 'view_2');
|
|
723
985
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
|
|
724
986
|
expect(hook.result.current).toEqual(['view_1', 'view_2']);
|
|
725
987
|
});
|
|
726
988
|
it('should preserve view order from URL', async () => {
|
|
727
|
-
mockSearchParams = new URLSearchParams();
|
|
728
989
|
mockSearchParams.append('view', 'view_c');
|
|
729
990
|
mockSearchParams.append('view', 'view_a');
|
|
730
991
|
mockSearchParams.append('view', 'view_b');
|
|
@@ -732,7 +993,6 @@ describe('ParameterContext', () => {
|
|
|
732
993
|
expect(hook.result.current).toEqual(['view_c', 'view_a', 'view_b']);
|
|
733
994
|
});
|
|
734
995
|
it('should deduplicate multiple empty view params to single empty string', async () => {
|
|
735
|
-
mockSearchParams = new URLSearchParams();
|
|
736
996
|
mockSearchParams.append('view', '');
|
|
737
997
|
mockSearchParams.append('view', '');
|
|
738
998
|
mockSearchParams.append('view', '');
|
|
@@ -753,7 +1013,7 @@ describe('ParameterContext', () => {
|
|
|
753
1013
|
});
|
|
754
1014
|
});
|
|
755
1015
|
it('should append view to existing views', async () => {
|
|
756
|
-
mockSearchParams
|
|
1016
|
+
mockSearchParams.set('view', 'existing_view');
|
|
757
1017
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
758
1018
|
views: ctx.views,
|
|
759
1019
|
addView: ctx.addView
|
|
@@ -766,7 +1026,7 @@ describe('ParameterContext', () => {
|
|
|
766
1026
|
});
|
|
767
1027
|
});
|
|
768
1028
|
it('should not add duplicate views', async () => {
|
|
769
|
-
mockSearchParams
|
|
1029
|
+
mockSearchParams.set('view', 'view_1');
|
|
770
1030
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
771
1031
|
views: ctx.views,
|
|
772
1032
|
addView: ctx.addView
|
|
@@ -782,7 +1042,6 @@ describe('ParameterContext', () => {
|
|
|
782
1042
|
});
|
|
783
1043
|
describe('removeView', () => {
|
|
784
1044
|
it('should remove first matching view', async () => {
|
|
785
|
-
mockSearchParams = new URLSearchParams();
|
|
786
1045
|
mockSearchParams.append('view', 'view_1');
|
|
787
1046
|
mockSearchParams.append('view', 'view_2');
|
|
788
1047
|
mockSearchParams.append('view', 'view_3');
|
|
@@ -798,7 +1057,6 @@ describe('ParameterContext', () => {
|
|
|
798
1057
|
});
|
|
799
1058
|
});
|
|
800
1059
|
it('should remove only first occurrence of duplicate views', async () => {
|
|
801
|
-
mockSearchParams = new URLSearchParams();
|
|
802
1060
|
mockSearchParams.append('view', 'dup');
|
|
803
1061
|
mockSearchParams.append('view', 'dup');
|
|
804
1062
|
mockSearchParams.append('view', 'other');
|
|
@@ -814,7 +1072,7 @@ describe('ParameterContext', () => {
|
|
|
814
1072
|
});
|
|
815
1073
|
});
|
|
816
1074
|
it('should do nothing when removing nonexistent view', async () => {
|
|
817
|
-
mockSearchParams
|
|
1075
|
+
mockSearchParams.set('view', 'existing');
|
|
818
1076
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
819
1077
|
views: ctx.views,
|
|
820
1078
|
removeView: ctx.removeView
|
|
@@ -839,17 +1097,16 @@ describe('ParameterContext', () => {
|
|
|
839
1097
|
});
|
|
840
1098
|
});
|
|
841
1099
|
});
|
|
842
|
-
describe('
|
|
1100
|
+
describe('resetViews', () => {
|
|
843
1101
|
it('should clear all views', async () => {
|
|
844
|
-
mockSearchParams = new URLSearchParams();
|
|
845
1102
|
mockSearchParams.append('view', 'view_1');
|
|
846
1103
|
mockSearchParams.append('view', 'view_2');
|
|
847
1104
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
848
1105
|
views: ctx.views,
|
|
849
|
-
|
|
1106
|
+
resetViews: ctx.resetViews
|
|
850
1107
|
})), { wrapper: Wrapper });
|
|
851
1108
|
await act(async () => {
|
|
852
|
-
hook.result.current.
|
|
1109
|
+
hook.result.current.resetViews();
|
|
853
1110
|
});
|
|
854
1111
|
await waitFor(() => {
|
|
855
1112
|
expect(hook.result.current.views).toEqual([]);
|
|
@@ -858,10 +1115,10 @@ describe('ParameterContext', () => {
|
|
|
858
1115
|
it('should be no-op when already empty', async () => {
|
|
859
1116
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
860
1117
|
views: ctx.views,
|
|
861
|
-
|
|
1118
|
+
resetViews: ctx.resetViews
|
|
862
1119
|
})), { wrapper: Wrapper });
|
|
863
1120
|
await act(async () => {
|
|
864
|
-
hook.result.current.
|
|
1121
|
+
hook.result.current.resetViews();
|
|
865
1122
|
});
|
|
866
1123
|
await waitFor(() => {
|
|
867
1124
|
expect(hook.result.current.views).toEqual([]);
|
|
@@ -870,7 +1127,6 @@ describe('ParameterContext', () => {
|
|
|
870
1127
|
});
|
|
871
1128
|
describe('setView', () => {
|
|
872
1129
|
it('should update view at specified index', async () => {
|
|
873
|
-
mockSearchParams = new URLSearchParams();
|
|
874
1130
|
mockSearchParams.append('view', 'view_1');
|
|
875
1131
|
mockSearchParams.append('view', 'view_2');
|
|
876
1132
|
mockSearchParams.append('view', 'view_3');
|
|
@@ -886,7 +1142,6 @@ describe('ParameterContext', () => {
|
|
|
886
1142
|
});
|
|
887
1143
|
});
|
|
888
1144
|
it('should update view at index 0', async () => {
|
|
889
|
-
mockSearchParams = new URLSearchParams();
|
|
890
1145
|
mockSearchParams.append('view', 'old_view');
|
|
891
1146
|
mockSearchParams.append('view', 'view_2');
|
|
892
1147
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
@@ -901,7 +1156,6 @@ describe('ParameterContext', () => {
|
|
|
901
1156
|
});
|
|
902
1157
|
});
|
|
903
1158
|
it('should update view at last index', async () => {
|
|
904
|
-
mockSearchParams = new URLSearchParams();
|
|
905
1159
|
mockSearchParams.append('view', 'view_1');
|
|
906
1160
|
mockSearchParams.append('view', 'old_last');
|
|
907
1161
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
@@ -916,7 +1170,7 @@ describe('ParameterContext', () => {
|
|
|
916
1170
|
});
|
|
917
1171
|
});
|
|
918
1172
|
it('should do nothing when index is out of bounds', async () => {
|
|
919
|
-
mockSearchParams
|
|
1173
|
+
mockSearchParams.set('view', 'existing');
|
|
920
1174
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
921
1175
|
views: ctx.views,
|
|
922
1176
|
setView: ctx.setView
|
|
@@ -929,7 +1183,7 @@ describe('ParameterContext', () => {
|
|
|
929
1183
|
});
|
|
930
1184
|
});
|
|
931
1185
|
it('should do nothing when index is negative', async () => {
|
|
932
|
-
mockSearchParams
|
|
1186
|
+
mockSearchParams.set('view', 'existing');
|
|
933
1187
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
934
1188
|
views: ctx.views,
|
|
935
1189
|
setView: ctx.setView
|
|
@@ -954,7 +1208,6 @@ describe('ParameterContext', () => {
|
|
|
954
1208
|
});
|
|
955
1209
|
});
|
|
956
1210
|
it('should sync updated view to URL', async () => {
|
|
957
|
-
mockSearchParams = new URLSearchParams();
|
|
958
1211
|
mockSearchParams.append('view', 'view_1');
|
|
959
1212
|
mockSearchParams.append('view', 'view_2');
|
|
960
1213
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
@@ -1004,14 +1257,13 @@ describe('ParameterContext', () => {
|
|
|
1004
1257
|
});
|
|
1005
1258
|
});
|
|
1006
1259
|
it('should remove all view params when views is empty', async () => {
|
|
1007
|
-
mockSearchParams = new URLSearchParams();
|
|
1008
1260
|
mockSearchParams.append('view', 'view_1');
|
|
1009
1261
|
mockSearchParams.append('view', 'view_2');
|
|
1010
1262
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
1011
|
-
|
|
1263
|
+
resetViews: ctx.resetViews
|
|
1012
1264
|
})), { wrapper: Wrapper });
|
|
1013
1265
|
await act(async () => {
|
|
1014
|
-
hook.result.current.
|
|
1266
|
+
hook.result.current.resetViews();
|
|
1015
1267
|
});
|
|
1016
1268
|
await waitFor(() => {
|
|
1017
1269
|
expect(mockSetParams).toHaveBeenCalled();
|