@cccsaurora/howler-ui 2.19.0-cases.1095 → 2.19.0-cases.1097

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.
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
5
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
+ // ---------------------------------------------------------------------------
7
+ // Hoisted mocks
8
+ // ---------------------------------------------------------------------------
9
+ const mockHitCardProps = vi.hoisted(() => ({ current: [] }));
10
+ // ---------------------------------------------------------------------------
11
+ // Module mocks
12
+ // ---------------------------------------------------------------------------
13
+ vi.mock('components/elements/hit/HitCard', () => ({
14
+ default: (props) => {
15
+ mockHitCardProps.current.push(props);
16
+ return _jsx("div", { id: `hit-card-${String(props.id)}` });
17
+ }
18
+ }));
19
+ vi.mock('react-i18next', () => ({
20
+ useTranslation: () => ({
21
+ t: (key) => key
22
+ })
23
+ }));
24
+ vi.mock('react-router-dom', async () => {
25
+ const actual = await vi.importActual('react-router-dom');
26
+ return {
27
+ ...actual,
28
+ Link: ({ to, children, ...props }) => (_jsx("a", { href: to, ...props, children: children }))
29
+ };
30
+ });
31
+ // ---------------------------------------------------------------------------
32
+ // Imports after mocks
33
+ // ---------------------------------------------------------------------------
34
+ import AlertPanel from './AlertPanel';
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+ const renderPanel = (caseValue) => {
39
+ return render(_jsx(MemoryRouter, { children: _jsx(AlertPanel, { case: caseValue }) }));
40
+ };
41
+ const makeHitItem = (id, path = `/cases/test/${id}`) => ({
42
+ type: 'hit',
43
+ value: id,
44
+ path
45
+ });
46
+ // ---------------------------------------------------------------------------
47
+ // Setup
48
+ // ---------------------------------------------------------------------------
49
+ beforeEach(() => {
50
+ mockHitCardProps.current = [];
51
+ });
52
+ // ---------------------------------------------------------------------------
53
+ // Tests
54
+ // ---------------------------------------------------------------------------
55
+ describe('AlertPanel', () => {
56
+ it('renders a skeleton when the case is null', () => {
57
+ const { container } = renderPanel(null);
58
+ expect(container.querySelector('.MuiSkeleton-root')).toBeTruthy();
59
+ });
60
+ it('renders the translated heading key', () => {
61
+ const _case = createMockCase({ case_id: 'case-1', items: [] });
62
+ renderPanel(_case);
63
+ expect(screen.getByText('page.cases.dashboard.alerts')).toBeInTheDocument();
64
+ });
65
+ it('renders HitCard only for unique hit items on the current page', () => {
66
+ const duplicate = makeHitItem('hit-1', '/cases/test/path-a');
67
+ const _case = createMockCase({
68
+ case_id: 'case-2',
69
+ items: [
70
+ duplicate,
71
+ duplicate,
72
+ makeHitItem('hit-2', '/cases/test/path-b'),
73
+ { type: 'event', value: 'event-1', path: '/cases/test/event-1' }
74
+ ]
75
+ });
76
+ renderPanel(_case);
77
+ expect(screen.getByTestId('hit-card-hit-1')).toBeInTheDocument();
78
+ expect(screen.getByTestId('hit-card-hit-2')).toBeInTheDocument();
79
+ expect(screen.queryByTestId('hit-card-event-1')).not.toBeInTheDocument();
80
+ expect(mockHitCardProps.current).toHaveLength(2);
81
+ expect(mockHitCardProps.current[0]).toEqual(expect.objectContaining({
82
+ id: 'hit-1',
83
+ lazy: true,
84
+ layout: 'dense'
85
+ }));
86
+ });
87
+ it('renders overlay links that target each hit path', () => {
88
+ const _case = createMockCase({
89
+ case_id: 'case-3',
90
+ items: [makeHitItem('hit-1', '/cases/case-3/path-one'), makeHitItem('hit-2', '/cases/case-3/path-two')]
91
+ });
92
+ const { container } = renderPanel(_case);
93
+ const links = Array.from(container.querySelectorAll('a'));
94
+ expect(links).toHaveLength(2);
95
+ expect(links[0]).toHaveAttribute('href', '/cases/case-3/path-one');
96
+ expect(links[1]).toHaveAttribute('href', '/cases/case-3/path-two');
97
+ });
98
+ it('shows pagination with multiple pages and switches to page 2 items', () => {
99
+ const items = Array.from({ length: 6 }, (_, idx) => makeHitItem(`hit-${idx + 1}`, `/cases/test/hit-${idx + 1}`));
100
+ const _case = createMockCase({ case_id: 'case-4', items });
101
+ renderPanel(_case);
102
+ expect(screen.getByTestId('hit-card-hit-1')).toBeInTheDocument();
103
+ expect(screen.getByTestId('hit-card-hit-5')).toBeInTheDocument();
104
+ expect(screen.queryByTestId('hit-card-hit-6')).not.toBeInTheDocument();
105
+ const pageTwoButton = screen.getByRole('button', { name: 'Go to page 2' });
106
+ fireEvent.click(pageTwoButton);
107
+ expect(screen.getByTestId('hit-card-hit-6')).toBeInTheDocument();
108
+ expect(screen.queryByTestId('hit-card-hit-1')).not.toBeInTheDocument();
109
+ });
110
+ });
@@ -0,0 +1,218 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, waitFor } from '@testing-library/react';
3
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
4
+ import { useState } from 'react';
5
+ import { createMockCase, createMockEvent, createMockHit } from '@cccsaurora/howler-ui/tests/utils';
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
+ // ---------------------------------------------------------------------------
8
+ // Hoisted mocks
9
+ // ---------------------------------------------------------------------------
10
+ const mockDispatchApi = vi.hoisted(() => vi.fn());
11
+ const mockSearchPost = vi.hoisted(() => vi.fn());
12
+ const mockUpdateCase = vi.hoisted(() => vi.fn());
13
+ const mockUseCaseState = vi.hoisted(() => ({ case: null }));
14
+ const mockAggregateProps = vi.hoisted(() => ({
15
+ current: []
16
+ }));
17
+ const mockCaseOverviewProps = vi.hoisted(() => ({ current: [] }));
18
+ const mockTaskPanelProps = vi.hoisted(() => ({ current: [] }));
19
+ const mockAlertPanelProps = vi.hoisted(() => ({ current: [] }));
20
+ const mockRelatedCasePanelProps = vi.hoisted(() => ({ current: [] }));
21
+ // ---------------------------------------------------------------------------
22
+ // Module mocks
23
+ // ---------------------------------------------------------------------------
24
+ vi.mock('use-context-selector', async () => {
25
+ const react = (await vi.importActual('react'));
26
+ return {
27
+ createContext: react.createContext,
28
+ useContextSelector: (context, selector) => {
29
+ return selector(react.useContext(context));
30
+ }
31
+ };
32
+ });
33
+ vi.mock('components/hooks/useMyApi', () => ({
34
+ default: () => ({ dispatchApi: mockDispatchApi })
35
+ }));
36
+ vi.mock('api', () => ({
37
+ default: {
38
+ v2: {
39
+ search: {
40
+ post: (...args) => mockSearchPost(...args)
41
+ }
42
+ }
43
+ }
44
+ }));
45
+ vi.mock('../hooks/useCase', () => ({
46
+ default: () => ({
47
+ case: mockUseCaseState.case,
48
+ update: mockUpdateCase
49
+ })
50
+ }));
51
+ vi.mock('react-i18next', () => ({
52
+ useTranslation: () => ({
53
+ t: (key) => key
54
+ })
55
+ }));
56
+ vi.mock('react-router-dom', async () => {
57
+ const actual = await vi.importActual('react-router-dom');
58
+ return {
59
+ ...actual,
60
+ useOutletContext: () => createMockCase({ case_id: 'route-case-id', items: [] })
61
+ };
62
+ });
63
+ vi.mock('./aggregates/CaseAggregate', () => ({
64
+ default: (props) => {
65
+ mockAggregateProps.current.push(props);
66
+ return _jsx("div", { id: `aggregate-${String(mockAggregateProps.current.length)}` });
67
+ }
68
+ }));
69
+ vi.mock('./CaseOverview', () => ({
70
+ default: (props) => {
71
+ mockCaseOverviewProps.current.push(props);
72
+ return _jsx("div", { id: "case-overview" });
73
+ }
74
+ }));
75
+ vi.mock('./TaskPanel', () => ({
76
+ default: (props) => {
77
+ mockTaskPanelProps.current.push(props);
78
+ return _jsx("div", { id: "task-panel" });
79
+ }
80
+ }));
81
+ vi.mock('./AlertPanel', () => ({
82
+ default: (props) => {
83
+ mockAlertPanelProps.current.push(props);
84
+ return _jsx("div", { id: "alert-panel" });
85
+ }
86
+ }));
87
+ vi.mock('./RelatedCasePanel', () => ({
88
+ default: (props) => {
89
+ mockRelatedCasePanelProps.current.push(props);
90
+ return _jsx("div", { id: "related-case-panel" });
91
+ }
92
+ }));
93
+ // ---------------------------------------------------------------------------
94
+ // Imports after mocks
95
+ // ---------------------------------------------------------------------------
96
+ import CaseDashboard from './CaseDashboard';
97
+ // ---------------------------------------------------------------------------
98
+ // Helpers
99
+ // ---------------------------------------------------------------------------
100
+ const makeRecordContextValue = (records, loadRecords) => ({
101
+ records,
102
+ selectedRecords: [],
103
+ addRecordToSelection: vi.fn(),
104
+ removeRecordFromSelection: vi.fn(),
105
+ clearSelectedRecords: vi.fn(),
106
+ loadRecords,
107
+ updateRecord: vi.fn(),
108
+ getRecord: vi.fn()
109
+ });
110
+ const renderDashboard = ({ dashboardCase, initialRecords = {}, onLoadRecords }) => {
111
+ const Wrapper = ({ children }) => {
112
+ const [records, setRecords] = useState(initialRecords);
113
+ const loadRecords = items => {
114
+ onLoadRecords?.(items);
115
+ const mapped = Object.fromEntries(items.map(item => [item.howler.id, item]));
116
+ setRecords(prev => ({ ...prev, ...mapped }));
117
+ };
118
+ return (_jsx(RecordContext.Provider, { value: makeRecordContextValue(records, loadRecords), children: children }));
119
+ };
120
+ return render(_jsx(CaseDashboard, { case: dashboardCase }), { wrapper: Wrapper });
121
+ };
122
+ // ---------------------------------------------------------------------------
123
+ // Setup
124
+ // ---------------------------------------------------------------------------
125
+ beforeEach(() => {
126
+ mockDispatchApi.mockReset();
127
+ mockSearchPost.mockReset();
128
+ mockUpdateCase.mockReset();
129
+ mockAggregateProps.current = [];
130
+ mockCaseOverviewProps.current = [];
131
+ mockTaskPanelProps.current = [];
132
+ mockAlertPanelProps.current = [];
133
+ mockRelatedCasePanelProps.current = [];
134
+ mockUseCaseState.case = null;
135
+ });
136
+ // ---------------------------------------------------------------------------
137
+ // Tests
138
+ // ---------------------------------------------------------------------------
139
+ describe('CaseDashboard', () => {
140
+ it('renders nothing when useCase does not return a case', () => {
141
+ const baseCase = createMockCase({ case_id: 'case-1', items: [] });
142
+ mockUseCaseState.case = null;
143
+ const { container } = renderDashboard({ dashboardCase: baseCase });
144
+ expect(container).toBeEmptyDOMElement();
145
+ });
146
+ it('renders overview, panels and aggregate cards with translated subtitles', () => {
147
+ const dashboardCase = createMockCase({
148
+ case_id: 'case-2',
149
+ items: [
150
+ { type: 'hit', value: 'hit-1', path: 'root/hit-1' },
151
+ { type: 'event', value: 'event-1', path: 'root/event-1' }
152
+ ]
153
+ });
154
+ mockUseCaseState.case = dashboardCase;
155
+ renderDashboard({
156
+ dashboardCase,
157
+ initialRecords: {
158
+ 'hit-1': createMockHit({ howler: { id: 'hit-1', outline: { threat: 'threat-a', target: 'target-a' } } }),
159
+ 'event-1': createMockEvent({ howler: { id: 'event-1', outline: { indicators: ['ioc-a'] } } })
160
+ }
161
+ });
162
+ expect(mockCaseOverviewProps.current).toHaveLength(1);
163
+ expect(mockTaskPanelProps.current).toHaveLength(1);
164
+ expect(mockAlertPanelProps.current).toHaveLength(1);
165
+ expect(mockRelatedCasePanelProps.current).toHaveLength(1);
166
+ expect(mockAggregateProps.current).toHaveLength(4);
167
+ expect(mockAggregateProps.current[0]).toEqual(expect.objectContaining({
168
+ field: 'howler.outline.threat',
169
+ subtitle: 'page.cases.dashboard.threat'
170
+ }));
171
+ expect(mockAggregateProps.current[1]).toEqual(expect.objectContaining({
172
+ field: 'howler.outline.target',
173
+ subtitle: 'page.cases.dashboard.target'
174
+ }));
175
+ expect(mockAggregateProps.current[2]).toEqual(expect.objectContaining({
176
+ field: 'howler.outline.indicators',
177
+ subtitle: 'page.cases.dashboard.indicators'
178
+ }));
179
+ expect(mockAggregateProps.current[3]).toEqual(expect.objectContaining({
180
+ subtitle: 'page.cases.dashboard.duration',
181
+ title: '--'
182
+ }));
183
+ });
184
+ it('loads missing hit and event records and sends expected search query', async () => {
185
+ const dashboardCase = createMockCase({
186
+ case_id: 'case-3',
187
+ items: [
188
+ { type: 'hit', value: 'hit-1', path: 'root/hit-1' },
189
+ { type: 'event', value: 'event-1', path: 'root/event-1' },
190
+ { type: 'hit', value: 'hit-2', path: 'root/hit-2' },
191
+ { type: 'case', value: 'case-child', path: 'root/case-child' }
192
+ ]
193
+ });
194
+ mockUseCaseState.case = dashboardCase;
195
+ const loadedEvent = createMockEvent({ howler: { id: 'event-1' } });
196
+ const loadedHit = createMockHit({ howler: { id: 'hit-2' } });
197
+ mockSearchPost.mockReturnValue({ endpoint: 'search' });
198
+ mockDispatchApi.mockResolvedValue({
199
+ items: [loadedEvent, loadedHit]
200
+ });
201
+ const onLoadRecords = vi.fn();
202
+ renderDashboard({
203
+ dashboardCase,
204
+ initialRecords: {
205
+ 'hit-1': createMockHit({ howler: { id: 'hit-1' } })
206
+ },
207
+ onLoadRecords
208
+ });
209
+ await waitFor(() => {
210
+ expect(mockSearchPost).toHaveBeenCalledWith(['hit', 'event'], {
211
+ query: 'howler.id:(event-1 OR hit-2)',
212
+ metadata: ['template', 'analytic']
213
+ });
214
+ expect(mockDispatchApi).toHaveBeenCalledTimes(1);
215
+ expect(onLoadRecords).toHaveBeenCalledWith([loadedEvent, loadedHit]);
216
+ });
217
+ });
218
+ });
package/package.json CHANGED
@@ -96,7 +96,7 @@
96
96
  "internal-slot": "1.0.7"
97
97
  },
98
98
  "type": "module",
99
- "version": "2.19.0-cases.1095",
99
+ "version": "2.19.0-cases.1097",
100
100
  "exports": {
101
101
  "./i18n": "./i18n.js",
102
102
  "./index.css": "./index.css",