@cccsaurora/howler-ui 2.19.0-dev.1266 → 2.19.0-dev.1277

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.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render, waitFor } from '@testing-library/react';
2
+ import { act, render, screen, waitFor } from '@testing-library/react';
3
3
  import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
4
4
  import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
5
5
  import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -40,10 +40,10 @@ vi.mock('api', () => ({
40
40
  }
41
41
  }));
42
42
  vi.mock('./detail/CaseDetails', () => ({
43
- default: () => _jsx("div", { id: "case-details" })
43
+ default: ({ case: _case }) => _jsx("div", { id: "case-details", children: _case?.title })
44
44
  }));
45
45
  vi.mock('./detail/CaseSidebar', () => ({
46
- default: () => _jsx("div", { id: "case-sidebar" })
46
+ default: ({ case: _case }) => _jsx("div", { id: "case-sidebar", children: _case?.title })
47
47
  }));
48
48
  // ---------------------------------------------------------------------------
49
49
  // Import after mocks
@@ -130,4 +130,27 @@ describe('CaseViewer', () => {
130
130
  expect(mockFetchViewers).toHaveBeenCalledWith('case-1');
131
131
  });
132
132
  });
133
+ it('propagates socket case updates to the sidebar and details', async () => {
134
+ const initialCase = createMockCase({ case_id: 'case-1', title: 'Original title' });
135
+ const updatedCase = createMockCase({ case_id: 'case-1', title: 'Updated title' });
136
+ mockDispatchApi.mockResolvedValue(initialCase);
137
+ render(_jsx(CaseViewer, {}), { wrapper: createWrapper() });
138
+ await waitFor(() => {
139
+ expect(mockAddListener).toHaveBeenCalledOnce();
140
+ });
141
+ await waitFor(() => {
142
+ expect(screen.getByTestId('case-sidebar')).toHaveTextContent('Original title');
143
+ });
144
+ act(() => {
145
+ mockAddListener.mock.calls[0][1]({
146
+ type: 'cases',
147
+ case: updatedCase,
148
+ error: false,
149
+ message: '',
150
+ status: 200
151
+ });
152
+ });
153
+ expect(screen.getByTestId('case-sidebar')).toHaveTextContent('Updated title');
154
+ expect(screen.getByTestId('case-details')).toHaveTextContent('Updated title');
155
+ });
133
156
  });
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { act, render, screen, waitFor } from '@testing-library/react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { describe, expect, it, vi } from 'vitest';
5
5
  globalThis.IS_REACT_ACT_ENVIRONMENT = true;
@@ -70,9 +70,7 @@ describe('CreateRuleDialog', () => {
70
70
  it('disables submit when destination is empty', async () => {
71
71
  const user = userEvent.setup();
72
72
  render(_jsx(CreateRuleDialog, { ...defaultProps }));
73
- await act(async () => {
74
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
75
- });
73
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
76
74
  expect(screen.getByTestId('rule-submit-button')).toBeDisabled();
77
75
  });
78
76
  it('enables submit when query, destination are filled and search has been run', async () => {
@@ -80,15 +78,9 @@ describe('CreateRuleDialog', () => {
80
78
  mockDispatchApi.mockResolvedValueOnce({ items: [], total: 5, offset: 0, rows: 0 });
81
79
  api.search.hit.post.mockReturnValue('search-request');
82
80
  render(_jsx(CreateRuleDialog, { ...defaultProps }));
83
- await act(async () => {
84
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
85
- });
86
- await act(async () => {
87
- await user.click(screen.getByTestId('rule-search-button'));
88
- });
89
- await act(async () => {
90
- await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
91
- });
81
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
82
+ await user.click(screen.getByTestId('rule-search-button'));
83
+ await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
92
84
  await waitFor(() => {
93
85
  expect(screen.getByTestId('rule-submit-button')).not.toBeDisabled();
94
86
  });
@@ -99,18 +91,10 @@ describe('CreateRuleDialog', () => {
99
91
  mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
100
92
  api.search.hit.post.mockReturnValue('search-request');
101
93
  render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
102
- await act(async () => {
103
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
104
- });
105
- await act(async () => {
106
- await user.click(screen.getByTestId('rule-search-button'));
107
- });
108
- await act(async () => {
109
- await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
110
- });
111
- await act(async () => {
112
- await user.click(screen.getByTestId('rule-submit-button'));
113
- });
94
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
95
+ await user.click(screen.getByTestId('rule-search-button'));
96
+ await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
97
+ await user.click(screen.getByTestId('rule-submit-button'));
114
98
  await waitFor(() => {
115
99
  expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
116
100
  query: 'event.kind:alert',
@@ -123,9 +107,7 @@ describe('CreateRuleDialog', () => {
123
107
  const user = userEvent.setup();
124
108
  const onClose = vi.fn();
125
109
  render(_jsx(CreateRuleDialog, { ...defaultProps, onClose: onClose }));
126
- await act(async () => {
127
- await user.click(screen.getByText('cancel'));
128
- });
110
+ await user.click(screen.getByText('cancel'));
129
111
  expect(onClose).toHaveBeenCalled();
130
112
  });
131
113
  it('shows search results after triggering search', async () => {
@@ -133,12 +115,8 @@ describe('CreateRuleDialog', () => {
133
115
  mockDispatchApi.mockResolvedValueOnce({ items: [], total: 42, offset: 0, rows: 0 });
134
116
  api.search.hit.post.mockReturnValue('search-request');
135
117
  render(_jsx(CreateRuleDialog, { ...defaultProps }));
136
- await act(async () => {
137
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
138
- });
139
- await act(async () => {
140
- await user.click(screen.getByTestId('rule-search-button'));
141
- });
118
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
119
+ await user.click(screen.getByTestId('rule-search-button'));
142
120
  await waitFor(() => {
143
121
  expect(screen.getByTestId('query-result-text')).toBeInTheDocument();
144
122
  expect(screen.getByText(/42 results/)).toBeInTheDocument();
@@ -155,18 +133,10 @@ describe('CreateRuleDialog', () => {
155
133
  mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
156
134
  api.search.hit.post.mockReturnValue('search-request');
157
135
  render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
158
- await act(async () => {
159
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
160
- });
161
- await act(async () => {
162
- await user.click(screen.getByTestId('rule-search-button'));
163
- });
164
- await act(async () => {
165
- await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
166
- });
167
- await act(async () => {
168
- await user.click(screen.getByTestId('rule-submit-button'));
169
- });
136
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
137
+ await user.click(screen.getByTestId('rule-search-button'));
138
+ await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
139
+ await user.click(screen.getByTestId('rule-submit-button'));
170
140
  await waitFor(() => {
171
141
  expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
172
142
  timeframe: 14
@@ -179,21 +149,11 @@ describe('CreateRuleDialog', () => {
179
149
  mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
180
150
  api.search.hit.post.mockReturnValue('search-request');
181
151
  render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
182
- await act(async () => {
183
- await user.click(screen.getByTestId('rule-no-expiry-checkbox'));
184
- });
185
- await act(async () => {
186
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
187
- });
188
- await act(async () => {
189
- await user.click(screen.getByTestId('rule-search-button'));
190
- });
191
- await act(async () => {
192
- await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
193
- });
194
- await act(async () => {
195
- await user.click(screen.getByTestId('rule-submit-button'));
196
- });
152
+ await user.click(screen.getByTestId('rule-no-expiry-checkbox'));
153
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
154
+ await user.click(screen.getByTestId('rule-search-button'));
155
+ await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
156
+ await user.click(screen.getByTestId('rule-submit-button'));
197
157
  await waitFor(() => {
198
158
  expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
199
159
  query: 'event.kind:alert',
@@ -208,21 +168,11 @@ describe('CreateRuleDialog', () => {
208
168
  mockDispatchApi.mockResolvedValueOnce({ items: [], total: 1, offset: 0, rows: 0 });
209
169
  api.search.hit.post.mockReturnValue('search-request');
210
170
  render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
211
- await act(async () => {
212
- await user.click(screen.getByTestId('rule-expire-after-resolved'));
213
- });
214
- await act(async () => {
215
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
216
- });
217
- await act(async () => {
218
- await user.click(screen.getByTestId('rule-search-button'));
219
- });
220
- await act(async () => {
221
- await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
222
- });
223
- await act(async () => {
224
- await user.click(screen.getByTestId('rule-submit-button'));
225
- });
171
+ await user.click(screen.getByTestId('rule-expire-after-resolved'));
172
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
173
+ await user.click(screen.getByTestId('rule-search-button'));
174
+ await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
175
+ await user.click(screen.getByTestId('rule-submit-button'));
226
176
  await waitFor(() => {
227
177
  expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
228
178
  timeframe: 14,
@@ -244,24 +194,12 @@ describe('CreateRuleDialog', () => {
244
194
  api.search.hit.post.mockReturnValue('search-request');
245
195
  render(_jsx(CreateRuleDialog, { ...defaultProps, onSubmit: onSubmit }));
246
196
  // Open the index Autocomplete and select 'event'
247
- await act(async () => {
248
- await user.click(screen.getByRole('combobox'));
249
- });
250
- await act(async () => {
251
- await user.click(screen.getByRole('option', { name: 'hit.search.index.event' }));
252
- });
253
- await act(async () => {
254
- await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
255
- });
256
- await act(async () => {
257
- await user.click(screen.getByTestId('rule-search-button'));
258
- });
259
- await act(async () => {
260
- await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
261
- });
262
- await act(async () => {
263
- await user.click(screen.getByTestId('rule-submit-button'));
264
- });
197
+ await user.click(screen.getByRole('combobox'));
198
+ await user.click(screen.getByRole('option', { name: 'hit.search.index.event' }));
199
+ await user.type(screen.getByTestId('rule-query-editor'), 'event.kind:alert');
200
+ await user.click(screen.getByTestId('rule-search-button'));
201
+ await user.type(screen.getByTestId('rule-destination-input'), 'alerts/incoming');
202
+ await user.click(screen.getByTestId('rule-submit-button'));
265
203
  await waitFor(() => {
266
204
  expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
267
205
  indexes: expect.arrayContaining(['hit', 'event'])
@@ -272,12 +210,8 @@ describe('CreateRuleDialog', () => {
272
210
  const user = userEvent.setup();
273
211
  render(_jsx(CreateRuleDialog, { ...defaultProps }));
274
212
  // 'hit' is selected by default; open the dropdown and try to deselect it
275
- await act(async () => {
276
- await user.click(screen.getByRole('combobox'));
277
- });
278
- await act(async () => {
279
- await user.click(screen.getByRole('option', { name: 'hit.search.index.hit' }));
280
- });
213
+ await user.click(screen.getByRole('combobox'));
214
+ await user.click(screen.getByRole('option', { name: 'hit.search.index.hit' }));
281
215
  // 'hit' should still be selected because deselecting the last index is prevented
282
216
  expect(screen.getAllByText('hit.search.index.hit').length).toBeGreaterThan(0);
283
217
  });
@@ -1,11 +1,12 @@
1
1
  import api from '@cccsaurora/howler-ui/api';
2
2
  import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
3
3
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
4
- import { useCallback, useContext, useEffect, useState } from 'react';
4
+ import { useCallback, useContext, useEffect, useId, useState } from 'react';
5
5
  import { isCaseUpdate } from '@cccsaurora/howler-ui/utils/socketUtils';
6
6
  const useCase = ({ caseId, case: providedCase }) => {
7
7
  const { dispatchApi } = useMyApi();
8
8
  const { addListener, removeListener } = useContext(SocketContext);
9
+ const listenerId = useId();
9
10
  const [loading, setLoading] = useState(false);
10
11
  const [missing, setMissing] = useState(false);
11
12
  const [_case, setCase] = useState(providedCase);
@@ -27,7 +28,7 @@ const useCase = ({ caseId, case: providedCase }) => {
27
28
  if (!activeCaseId) {
28
29
  return;
29
30
  }
30
- const listenerKey = `case-update-${activeCaseId}`;
31
+ const listenerKey = `case-update-${activeCaseId}-${listenerId}`;
31
32
  addListener(listenerKey, data => {
32
33
  if (isCaseUpdate(data) && data.case.case_id === activeCaseId) {
33
34
  setCase(data.case);
@@ -36,7 +37,7 @@ const useCase = ({ caseId, case: providedCase }) => {
36
37
  return () => {
37
38
  removeListener(listenerKey);
38
39
  };
39
- }, [activeCaseId, addListener, removeListener]);
40
+ }, [activeCaseId, addListener, listenerId, removeListener]);
40
41
  const update = useCallback(async (_updatedCase, publish = true) => {
41
42
  if (!activeCaseId) {
42
43
  return;
@@ -1,4 +1,5 @@
1
- import { act, renderHook, waitFor } from '@testing-library/react';
1
+ import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
2
+ import { createElement } from 'react';
2
3
  import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
3
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
5
  // ---------------------------------------------------------------------------
@@ -47,6 +48,12 @@ import useCase from './useCase';
47
48
  const renderUseCaseHook = (args) => {
48
49
  return renderHook(() => useCase(args));
49
50
  };
51
+ const CaseConsumers = ({ case: providedCase }) => {
52
+ const sidebarCase = useCase({ case: providedCase });
53
+ const dashboardCase = useCase({ case: providedCase });
54
+ const detailsCase = useCase({ case: providedCase });
55
+ return createElement('div', null, createElement('span', { id: 'sidebar-case-title' }, sidebarCase.case.title), createElement('span', { id: 'dashboard-case-title' }, dashboardCase.case.title), createElement('span', { id: 'details-case-title' }, detailsCase.case.title));
56
+ };
50
57
  // ---------------------------------------------------------------------------
51
58
  // Setup
52
59
  // ---------------------------------------------------------------------------
@@ -78,10 +85,10 @@ describe('useCase', () => {
78
85
  });
79
86
  });
80
87
  describe('socket listener', () => {
81
- it('registers a listener keyed by case ID', () => {
88
+ it('registers a listener keyed by case ID and hook instance', () => {
82
89
  const mockCase = createMockCase({ case_id: 'c3' });
83
90
  renderUseCaseHook({ case: mockCase });
84
- expect(mockAddListener).toHaveBeenCalledWith('case-update-c3', expect.any(Function));
91
+ expect(mockAddListener).toHaveBeenCalledWith(expect.stringMatching(/^case-update-c3-/), expect.any(Function));
85
92
  });
86
93
  it('updates state when a matching case update is received', () => {
87
94
  const mockCase = createMockCase({ case_id: 'c4', title: 'Original' });
@@ -135,7 +142,29 @@ describe('useCase', () => {
135
142
  const mockCase = createMockCase({ case_id: 'c7' });
136
143
  const { unmount } = renderUseCaseHook({ case: mockCase });
137
144
  unmount();
138
- expect(mockRemoveListener).toHaveBeenCalledWith('case-update-c7');
145
+ expect(mockRemoveListener).toHaveBeenCalledWith(expect.stringMatching(/^case-update-c7-/));
146
+ });
147
+ it('updates every concurrent case consumer for the same case', () => {
148
+ const mockCase = createMockCase({ case_id: 'c8', title: 'Original' });
149
+ render(createElement(CaseConsumers, { case: mockCase }));
150
+ const listenerKeys = mockAddListener.mock.calls.map(([key]) => key);
151
+ expect(listenerKeys).toHaveLength(3);
152
+ expect([...new Set(listenerKeys)]).toHaveLength(3);
153
+ const updatedCase = createMockCase({ case_id: 'c8', title: 'Updated via socket' });
154
+ act(() => {
155
+ mockAddListener.mock.calls.forEach(([, listener]) => {
156
+ listener({
157
+ type: 'cases',
158
+ case: updatedCase,
159
+ error: false,
160
+ message: '',
161
+ status: 200
162
+ });
163
+ });
164
+ });
165
+ expect(screen.getByTestId('sidebar-case-title')).toHaveTextContent('Updated via socket');
166
+ expect(screen.getByTestId('dashboard-case-title')).toHaveTextContent('Updated via socket');
167
+ expect(screen.getByTestId('details-case-title')).toHaveTextContent('Updated via socket');
139
168
  });
140
169
  });
141
170
  });
@@ -254,11 +254,9 @@ describe('QuerySettings', () => {
254
254
  const chipButton = screen.getByRole('button');
255
255
  await user.click(chipButton);
256
256
  const addFilterButton = screen.getByLabelText(i18n.t('hit.search.filter.add'));
257
- await act(async () => {
258
- await user.click(addFilterButton);
259
- await user.click(addFilterButton);
260
- await user.click(addFilterButton);
261
- });
257
+ await user.click(addFilterButton);
258
+ await user.click(addFilterButton);
259
+ await user.click(addFilterButton);
262
260
  expect(mockAddFilter).toHaveBeenCalledTimes(3);
263
261
  expect(mockAddFilter).toHaveBeenCalledWith('howler.assessment:*');
264
262
  });
package/package.json CHANGED
@@ -93,7 +93,7 @@
93
93
  "url": "https://github.com/CybercentreCanada/howler"
94
94
  },
95
95
  "type": "module",
96
- "version": "2.19.0-dev.1266",
96
+ "version": "2.19.0-dev.1277",
97
97
  "exports": {
98
98
  "./i18n": "./i18n.js",
99
99
  "./index.css": "./index.css",