@cccsaurora/howler-ui 2.15.0-dev.327 → 2.15.0-dev.335
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/elements/display/HowlerAvatar.js +4 -2
- package/components/routes/dossiers/DossierCard.test.d.ts +1 -0
- package/components/routes/dossiers/DossierCard.test.js +410 -0
- package/components/routes/hits/search/ViewLink.js +8 -3
- package/components/routes/hits/search/ViewLink.test.d.ts +1 -0
- package/components/routes/hits/search/ViewLink.test.js +375 -0
- package/locales/en/translation.json +3 -1
- package/locales/fr/translation.json +2 -0
- package/package.json +1 -1
- package/tests/utils.d.ts +4 -0
- package/tests/utils.js +22 -0
|
@@ -2,9 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Avatar, Tooltip, useTheme } from '@mui/material';
|
|
3
3
|
import { AvatarContext } from '@cccsaurora/howler-ui/components/app/providers/AvatarProvider';
|
|
4
4
|
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
5
6
|
import { nameToInitials } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
6
7
|
import { stringToColor } from '@cccsaurora/howler-ui/utils/utils';
|
|
7
8
|
const HowlerAvatar = ({ userId, ...avatarProps }) => {
|
|
9
|
+
const { t } = useTranslation();
|
|
8
10
|
const { getAvatar } = useContext(AvatarContext);
|
|
9
11
|
const theme = useTheme();
|
|
10
12
|
const [props, setProps] = useState();
|
|
@@ -35,10 +37,10 @@ const HowlerAvatar = ({ userId, ...avatarProps }) => {
|
|
|
35
37
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
38
|
}, [userId]);
|
|
37
39
|
if (userId) {
|
|
38
|
-
return (_jsx(Tooltip, { title: userId, children: _jsx(Avatar, { ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }) }));
|
|
40
|
+
return (_jsx(Tooltip, { title: userId, children: _jsx(Avatar, { "aria-label": userId, ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }) }));
|
|
39
41
|
}
|
|
40
42
|
else {
|
|
41
|
-
return (_jsx(Avatar, { ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }));
|
|
43
|
+
return (_jsx(Avatar, { "aria-label": t('unknown'), ...avatarProps, ...props, sx: { ...(avatarProps?.sx || {}), ...(props?.sx || {}) } }));
|
|
42
44
|
}
|
|
43
45
|
};
|
|
44
46
|
export default memo(HowlerAvatar);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable react/jsx-no-literals */
|
|
3
|
+
/* eslint-disable import/imports-first */
|
|
4
|
+
/// <reference types="vitest" />
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
7
|
+
import { AvatarContext } from '@cccsaurora/howler-ui/components/app/providers/AvatarProvider';
|
|
8
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
9
|
+
import { act } from 'react';
|
|
10
|
+
import { I18nextProvider } from 'react-i18next';
|
|
11
|
+
import { createMockDossier } from '@cccsaurora/howler-ui/tests/utils';
|
|
12
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import DossierCard from './DossierCard';
|
|
14
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
const mockAvatarContext = {
|
|
16
|
+
getAvatar: vi.fn(userId => Promise.resolve('https://images.example.com/' + userId))
|
|
17
|
+
};
|
|
18
|
+
// Test wrapper
|
|
19
|
+
const Wrapper = ({ children }) => {
|
|
20
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(AvatarContext.Provider, { value: mockAvatarContext, children: children }) }));
|
|
21
|
+
};
|
|
22
|
+
describe('DossierCard', () => {
|
|
23
|
+
let user;
|
|
24
|
+
let mockOnDelete;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
user = userEvent.setup();
|
|
27
|
+
mockOnDelete = vi.fn();
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
describe('Rendering Conditions', () => {
|
|
31
|
+
it('should render with required props only', async () => {
|
|
32
|
+
const dossier = createMockDossier();
|
|
33
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
34
|
+
await waitFor(() => {
|
|
35
|
+
expect(screen.getByText('Test Dossier')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('should render with all props', async () => {
|
|
39
|
+
const dossier = createMockDossier();
|
|
40
|
+
render(_jsx(DossierCard, { dossier: dossier, className: "custom-class", onDelete: mockOnDelete }), {
|
|
41
|
+
wrapper: Wrapper
|
|
42
|
+
});
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByText('Test Dossier')).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('should apply custom className when provided', async () => {
|
|
48
|
+
const dossier = createMockDossier();
|
|
49
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier, className: "custom-class" }), { wrapper: Wrapper });
|
|
50
|
+
await waitFor(() => {
|
|
51
|
+
const card = container.querySelector('.custom-class');
|
|
52
|
+
expect(card).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('should render without className when not provided', async () => {
|
|
56
|
+
const dossier = createMockDossier();
|
|
57
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
58
|
+
await waitFor(() => {
|
|
59
|
+
expect(container.querySelector('.MuiCard-root')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('UI Element Display', () => {
|
|
64
|
+
it('should display dossier title', async () => {
|
|
65
|
+
const dossier = createMockDossier({ title: 'My Custom Dossier' });
|
|
66
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
expect(screen.getByText('My Custom Dossier')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('should display dossier query in code block', async () => {
|
|
72
|
+
const dossier = createMockDossier({ query: 'howler.id:*' });
|
|
73
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
const codeElement = screen.getByText('howler.id:*');
|
|
76
|
+
expect(codeElement).toBeInTheDocument();
|
|
77
|
+
expect(codeElement.tagName).toBe('CODE');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('should display Person icon for personal dossier type', async () => {
|
|
81
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
82
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
// Check for Person icon via MUI's data-testid or by checking the tooltip
|
|
85
|
+
const tooltip = screen.getByLabelText(/personal/i);
|
|
86
|
+
expect(tooltip).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should display Language icon for global dossier type', async () => {
|
|
90
|
+
const dossier = createMockDossier({ type: 'global' });
|
|
91
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
// Check for Language icon via tooltip
|
|
94
|
+
const tooltip = screen.getByLabelText(/global/i);
|
|
95
|
+
expect(tooltip).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it('should display HowlerAvatar with correct userId', async () => {
|
|
99
|
+
const dossier = createMockDossier({ owner: 'john.doe' });
|
|
100
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
expect(screen.getByLabelText('john.doe')).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('should display delete button when onDelete is provided', async () => {
|
|
106
|
+
const dossier = createMockDossier();
|
|
107
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
const deleteButton = screen.getByRole('button');
|
|
110
|
+
expect(deleteButton).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
it('should not display delete button when onDelete is not provided', async () => {
|
|
114
|
+
const dossier = createMockDossier();
|
|
115
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it('should have tooltip on type icon', async () => {
|
|
121
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
122
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
it('should have tooltip on delete button', async () => {
|
|
128
|
+
const dossier = createMockDossier();
|
|
129
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(screen.getByLabelText(/delete/i)).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('Button States & Interactions', () => {
|
|
136
|
+
it('should call onDelete with correct parameters when delete button is clicked', async () => {
|
|
137
|
+
const dossier = createMockDossier({ dossier_id: 'my-dossier-123' });
|
|
138
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
const deleteButton = screen.getByRole('button');
|
|
143
|
+
await user.click(deleteButton);
|
|
144
|
+
await waitFor(() => {
|
|
145
|
+
expect(mockOnDelete).toHaveBeenCalledTimes(1);
|
|
146
|
+
expect(mockOnDelete).toHaveBeenCalledWith(expect.any(Object), 'my-dossier-123');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
it('should call onDelete with event object', async () => {
|
|
150
|
+
const dossier = createMockDossier();
|
|
151
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
const deleteButton = screen.getByRole('button');
|
|
156
|
+
await user.click(deleteButton);
|
|
157
|
+
await waitFor(() => {
|
|
158
|
+
const eventArg = mockOnDelete.mock.calls[0][0];
|
|
159
|
+
expect(eventArg).toBeDefined();
|
|
160
|
+
expect(eventArg.type).toBe('click');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
it('should not crash when delete button is clicked multiple times', async () => {
|
|
164
|
+
const dossier = createMockDossier();
|
|
165
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
const deleteButton = screen.getByRole('button');
|
|
170
|
+
await user.click(deleteButton);
|
|
171
|
+
await user.click(deleteButton);
|
|
172
|
+
await user.click(deleteButton);
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(mockOnDelete).toHaveBeenCalledTimes(3);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
describe('Edge Cases', () => {
|
|
179
|
+
it('should handle empty title', async () => {
|
|
180
|
+
const dossier = createMockDossier({ title: '' });
|
|
181
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(container).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
it('should handle empty query', async () => {
|
|
187
|
+
const dossier = createMockDossier({ query: '' });
|
|
188
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
const codeElement = container.querySelector('code');
|
|
191
|
+
expect(codeElement).toBeEmptyDOMElement();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
it('should handle very long title', async () => {
|
|
195
|
+
const longTitle = 'A'.repeat(200);
|
|
196
|
+
const dossier = createMockDossier({ title: longTitle });
|
|
197
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(screen.getByText(longTitle)).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('should handle very long query', async () => {
|
|
203
|
+
const longQuery = 'howler.id:*' + ' AND howler.status:open'.repeat(50);
|
|
204
|
+
const dossier = createMockDossier({ query: longQuery });
|
|
205
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(screen.getByText(longQuery)).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
it('should handle special characters in title', async () => {
|
|
211
|
+
const dossier = createMockDossier({ title: '<script>alert("xss")</script>' });
|
|
212
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(screen.getByText('<script>alert("xss")</script>')).toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
it('should handle special characters in query', async () => {
|
|
218
|
+
const dossier = createMockDossier({ query: 'howler.id:*&query=<test>' });
|
|
219
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
220
|
+
await waitFor(() => {
|
|
221
|
+
expect(screen.getByText('howler.id:*&query=<test>')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
it('should handle undefined owner', async () => {
|
|
225
|
+
const dossier = createMockDossier({ owner: undefined });
|
|
226
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(screen.getByLabelText('Unknown')).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
it('should handle null owner', async () => {
|
|
232
|
+
const dossier = createMockDossier({ owner: null });
|
|
233
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(screen.getByLabelText('Unknown')).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe('Dossier Types', () => {
|
|
240
|
+
it('should render correctly for personal type', async () => {
|
|
241
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
242
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
243
|
+
await waitFor(() => {
|
|
244
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
it('should render correctly for global type', async () => {
|
|
248
|
+
const dossier = createMockDossier({ type: 'global' });
|
|
249
|
+
render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(screen.getByLabelText(/global/i)).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
it('should handle switching between types', async () => {
|
|
255
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier({ type: 'personal' }) }), {
|
|
256
|
+
wrapper: Wrapper
|
|
257
|
+
});
|
|
258
|
+
await waitFor(() => {
|
|
259
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
260
|
+
});
|
|
261
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier({ type: 'global' }) }));
|
|
262
|
+
await waitFor(() => {
|
|
263
|
+
expect(screen.getByLabelText(/global/i)).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
describe('Integration Tests', () => {
|
|
268
|
+
it('should render complete card with all elements', async () => {
|
|
269
|
+
const dossier = createMockDossier({
|
|
270
|
+
dossier_id: 'full-test',
|
|
271
|
+
title: 'Complete Dossier',
|
|
272
|
+
query: 'howler.status:open AND howler.assigned:me',
|
|
273
|
+
type: 'personal',
|
|
274
|
+
owner: 'admin'
|
|
275
|
+
});
|
|
276
|
+
render(_jsx(DossierCard, { dossier: dossier, className: "test-class", onDelete: mockOnDelete }), {
|
|
277
|
+
wrapper: Wrapper
|
|
278
|
+
});
|
|
279
|
+
await waitFor(() => {
|
|
280
|
+
// Check all elements are present
|
|
281
|
+
expect(screen.getByText('Complete Dossier')).toBeInTheDocument();
|
|
282
|
+
expect(screen.getByText('howler.status:open AND howler.assigned:me')).toBeInTheDocument();
|
|
283
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
284
|
+
expect(screen.getByLabelText('admin')).toBeInTheDocument();
|
|
285
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
it('should handle multiple dossier cards with different data', async () => {
|
|
289
|
+
const dossiers = [
|
|
290
|
+
createMockDossier({ dossier_id: 'dossier-1', title: 'Dossier 1', type: 'personal' }),
|
|
291
|
+
createMockDossier({ dossier_id: 'dossier-2', title: 'Dossier 2', type: 'global' }),
|
|
292
|
+
createMockDossier({ dossier_id: 'dossier-3', title: 'Dossier 3', type: 'personal' })
|
|
293
|
+
];
|
|
294
|
+
render(_jsx(Wrapper, { children: dossiers.map(dossier => (_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }, dossier.dossier_id))) }));
|
|
295
|
+
await waitFor(() => {
|
|
296
|
+
expect(screen.getByText('Dossier 1')).toBeInTheDocument();
|
|
297
|
+
expect(screen.getByText('Dossier 2')).toBeInTheDocument();
|
|
298
|
+
expect(screen.getByText('Dossier 3')).toBeInTheDocument();
|
|
299
|
+
expect(screen.getAllByRole('button')).toHaveLength(3);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
it('should work with different owners', async () => {
|
|
303
|
+
const owners = ['user1', 'user2', 'admin'];
|
|
304
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier({ owner: owners[0] }) }), {
|
|
305
|
+
wrapper: Wrapper
|
|
306
|
+
});
|
|
307
|
+
for (const owner of owners) {
|
|
308
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier({ owner }) }));
|
|
309
|
+
await waitFor(() => {
|
|
310
|
+
expect(screen.getByLabelText(owner)).toBeInTheDocument();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
describe('Accessibility', () => {
|
|
316
|
+
it('should have accessible delete button', async () => {
|
|
317
|
+
const dossier = createMockDossier();
|
|
318
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
319
|
+
await waitFor(() => {
|
|
320
|
+
const deleteButton = screen.getByRole('button');
|
|
321
|
+
expect(deleteButton).toBeInTheDocument();
|
|
322
|
+
expect(deleteButton).toHaveAccessibleName();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
it('should have tooltips for icons', async () => {
|
|
326
|
+
const dossier = createMockDossier({ type: 'personal' });
|
|
327
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
328
|
+
await waitFor(() => {
|
|
329
|
+
// Type icon tooltip
|
|
330
|
+
expect(screen.getByLabelText(/personal/i)).toBeInTheDocument();
|
|
331
|
+
// Delete button tooltip
|
|
332
|
+
expect(screen.getByLabelText(/delete/i)).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
it('should have proper semantic HTML structure', async () => {
|
|
336
|
+
const dossier = createMockDossier();
|
|
337
|
+
const { container } = render(_jsx(DossierCard, { dossier: dossier }), { wrapper: Wrapper });
|
|
338
|
+
await waitFor(() => {
|
|
339
|
+
// Should have a card
|
|
340
|
+
expect(container.querySelector('.MuiCard-root')).toBeInTheDocument();
|
|
341
|
+
// Should have code element for query
|
|
342
|
+
expect(container.querySelector('code')).toBeInTheDocument();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
it('should maintain focus on delete button', async () => {
|
|
346
|
+
const dossier = createMockDossier();
|
|
347
|
+
render(_jsx(DossierCard, { dossier: dossier, onDelete: mockOnDelete }), { wrapper: Wrapper });
|
|
348
|
+
await waitFor(() => {
|
|
349
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
350
|
+
});
|
|
351
|
+
const deleteButton = screen.getByRole('button');
|
|
352
|
+
act(() => {
|
|
353
|
+
deleteButton.focus();
|
|
354
|
+
});
|
|
355
|
+
await waitFor(() => {
|
|
356
|
+
expect(deleteButton).toHaveFocus();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
describe('Prop Changes', () => {
|
|
361
|
+
it('should update when dossier prop changes', async () => {
|
|
362
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier({ title: 'Original' }) }), {
|
|
363
|
+
wrapper: Wrapper
|
|
364
|
+
});
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
expect(screen.getByText('Original')).toBeInTheDocument();
|
|
367
|
+
});
|
|
368
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier({ title: 'Updated' }) }));
|
|
369
|
+
await waitFor(() => {
|
|
370
|
+
expect(screen.getByText('Updated')).toBeInTheDocument();
|
|
371
|
+
expect(screen.queryByText('Original')).not.toBeInTheDocument();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
it('should update when onDelete prop is added', async () => {
|
|
375
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier() }), { wrapper: Wrapper });
|
|
376
|
+
await waitFor(() => {
|
|
377
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
378
|
+
});
|
|
379
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier(), onDelete: mockOnDelete }));
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
it('should update when onDelete prop is removed', async () => {
|
|
385
|
+
const { rerender } = render(_jsx(DossierCard, { dossier: createMockDossier(), onDelete: mockOnDelete }), {
|
|
386
|
+
wrapper: Wrapper
|
|
387
|
+
});
|
|
388
|
+
await waitFor(() => {
|
|
389
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
390
|
+
});
|
|
391
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier() }));
|
|
392
|
+
await waitFor(() => {
|
|
393
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
it('should update when className changes', async () => {
|
|
397
|
+
const { container, rerender } = render(_jsx(DossierCard, { dossier: createMockDossier(), className: "class-1" }), {
|
|
398
|
+
wrapper: Wrapper
|
|
399
|
+
});
|
|
400
|
+
await waitFor(() => {
|
|
401
|
+
expect(container.querySelector('.class-1')).toBeInTheDocument();
|
|
402
|
+
});
|
|
403
|
+
rerender(_jsx(DossierCard, { dossier: createMockDossier(), className: "class-2" }));
|
|
404
|
+
await waitFor(() => {
|
|
405
|
+
expect(container.querySelector('.class-2')).toBeInTheDocument();
|
|
406
|
+
expect(container.querySelector('.class-1')).not.toBeInTheDocument();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Close, Edit, SavedSearch } from '@mui/icons-material';
|
|
2
|
+
import { Close, Edit, Language, Lock, OpenInNew, Person, Refresh, SavedSearch } from '@mui/icons-material';
|
|
3
3
|
import { Alert, IconButton, Stack, Tooltip, Typography } from '@mui/material';
|
|
4
4
|
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
5
5
|
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
@@ -15,6 +15,7 @@ const ViewLink = () => {
|
|
|
15
15
|
const sort = useContextSelector(ParameterContext, ctx => ctx.sort);
|
|
16
16
|
const span = useContextSelector(ParameterContext, ctx => ctx.span);
|
|
17
17
|
const viewId = useContextSelector(HitSearchContext, ctx => ctx.viewId);
|
|
18
|
+
const search = useContextSelector(HitSearchContext, ctx => ctx.search);
|
|
18
19
|
const viewsReady = useContextSelector(ViewContext, ctx => has(ctx.views, viewId));
|
|
19
20
|
const selectedView = useContextSelector(ViewContext, ctx => ctx.views[viewId]);
|
|
20
21
|
const viewUrl = useMemo(() => {
|
|
@@ -33,10 +34,14 @@ const ViewLink = () => {
|
|
|
33
34
|
}
|
|
34
35
|
return keys.length > 0 ? `/views/create?${keys.join('&')}` : '/views/create';
|
|
35
36
|
}, [query, sort, span, viewId]);
|
|
36
|
-
const viewButton = useMemo(() => (_jsx(Tooltip, { title: viewId ? t('route.views.edit') : t('route.views.create'), children: _jsx(IconButton, { size: "small", component: Link, disabled: (!viewId && !query) || span?.endsWith('custom'), to: viewUrl, children: viewId ? _jsx(Edit, { fontSize: "small" }) : _jsx(SavedSearch, {}) }) })), [query, span, t, viewId, viewUrl]);
|
|
37
|
+
const viewButton = useMemo(() => (_jsx(Tooltip, { title: viewId ? t('route.views.edit') : t('route.views.create'), children: _jsx(IconButton, { "aria-label": viewId ? t('route.views.edit') : t('route.views.create'), size: "small", component: Link, disabled: (!viewId && !query) || span?.endsWith('custom'), to: viewUrl, role: "link", children: viewId ? _jsx(Edit, { fontSize: "small" }) : _jsx(SavedSearch, {}) }) })), [query, span, t, viewId, viewUrl]);
|
|
37
38
|
if (!viewId) {
|
|
38
39
|
return null;
|
|
39
40
|
}
|
|
40
|
-
return selectedView ? (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Tooltip, { title:
|
|
41
|
+
return selectedView ? (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Tooltip, { title: t(`route.views.manager.${selectedView.type}`), children: {
|
|
42
|
+
readonly: _jsx(Lock, { fontSize: "small", "aria-label": t(`route.views.manager.${selectedView.type}`) }),
|
|
43
|
+
global: _jsx(Language, { fontSize: "small", "aria-label": t(`route.views.manager.${selectedView.type}`) }),
|
|
44
|
+
personal: _jsx(Person, { fontSize: "small", "aria-label": t(`route.views.manager.${selectedView.type}`) })
|
|
45
|
+
}[selectedView.type] }), _jsx(Tooltip, { title: selectedView.query, children: _jsx(Typography, { role: "link", sx: { color: 'text.primary' }, variant: "body1", component: Link, to: `/views/${selectedView.view_id}/edit`, "aria-label": `${t(selectedView.title)} - ${selectedView.query ?? t('unknown')}`, children: t(selectedView.title) }) }), viewButton, _jsx(Tooltip, { title: t('view.refresh'), children: _jsx(IconButton, { size: "small", onClick: () => search(query), "aria-label": t('view.refresh'), children: _jsx(Refresh, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('view.open'), children: _jsx(IconButton, { size: "small", component: Link, to: `/search?query=${selectedView.query}`, "aria-label": t('view.open'), role: "link", children: _jsx(OpenInNew, { fontSize: "small" }) }) })] })) : (viewsReady && (_jsx(Alert, { role: "alert", variant: "outlined", severity: "error", "aria-live": "assertive", "aria-atomic": "true", action: _jsx(IconButton, { size: "small", component: Link, to: "/search", children: _jsx(Close, { fontSize: "small" }) }), children: t('view.notfound') })));
|
|
41
46
|
};
|
|
42
47
|
export default memo(ViewLink);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable react/jsx-no-literals */
|
|
3
|
+
/* eslint-disable import/imports-first */
|
|
4
|
+
/// <reference types="vitest" />
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
7
|
+
import React, { createContext, useContext } from 'react';
|
|
8
|
+
import { vi } from 'vitest';
|
|
9
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
// Mock use-context-selector
|
|
11
|
+
vi.mock('use-context-selector', async () => {
|
|
12
|
+
return {
|
|
13
|
+
createContext,
|
|
14
|
+
useContextSelector: (context, selector) => {
|
|
15
|
+
return selector(useContext(context));
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
// Mock react-router-dom
|
|
20
|
+
vi.mock('react-router-dom', async () => {
|
|
21
|
+
const actual = await vi.importActual('react-router-dom');
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
Link: React.forwardRef(({ to, children, ...props }, ref) => (_jsx("a", { ref: ref, href: to, ...props, children: children })))
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
// Import component after mocks
|
|
28
|
+
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
29
|
+
import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
|
|
30
|
+
import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
|
|
31
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
32
|
+
import { I18nextProvider } from 'react-i18next';
|
|
33
|
+
import { createMockView } from '@cccsaurora/howler-ui/tests/utils';
|
|
34
|
+
import ViewLink from './ViewLink';
|
|
35
|
+
// Mock contexts
|
|
36
|
+
const mockSearch = vi.fn();
|
|
37
|
+
let mockParameterContext = {
|
|
38
|
+
query: 'howler.id:*',
|
|
39
|
+
sort: 'event.created desc',
|
|
40
|
+
span: 'date.range.1.month',
|
|
41
|
+
setQuery: vi.fn(),
|
|
42
|
+
setSort: vi.fn(),
|
|
43
|
+
setSpan: vi.fn()
|
|
44
|
+
};
|
|
45
|
+
let mockHitSearchContext = {
|
|
46
|
+
viewId: 'test-view-id',
|
|
47
|
+
search: mockSearch
|
|
48
|
+
};
|
|
49
|
+
let mockViewContext = {
|
|
50
|
+
views: {
|
|
51
|
+
'test-view-id': createMockView()
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
// Test wrapper
|
|
55
|
+
const Wrapper = ({ children }) => {
|
|
56
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: _jsx(HitSearchContext.Provider, { value: mockHitSearchContext, children: _jsx(ViewContext.Provider, { value: mockViewContext, children: children }) }) }) }));
|
|
57
|
+
};
|
|
58
|
+
describe('ViewLink', () => {
|
|
59
|
+
let user;
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
user = userEvent.setup();
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
// Reset mock contexts to defaults
|
|
64
|
+
mockHitSearchContext.viewId = 'test-view-id';
|
|
65
|
+
mockParameterContext.query = 'howler.id:*';
|
|
66
|
+
mockParameterContext.sort = 'event.created desc';
|
|
67
|
+
mockParameterContext.span = 'date.range.1.month';
|
|
68
|
+
mockViewContext.views = {
|
|
69
|
+
'test-view-id': createMockView()
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
describe('Rendering Conditions', () => {
|
|
73
|
+
it('should return null when viewId is not set', () => {
|
|
74
|
+
mockHitSearchContext.viewId = null;
|
|
75
|
+
const { container } = render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
76
|
+
expect(container.firstChild).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
it('should return null when viewId is undefined', () => {
|
|
79
|
+
mockHitSearchContext.viewId = undefined;
|
|
80
|
+
const { container } = render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
81
|
+
expect(container.firstChild).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
it('should render selected view UI when viewId exists and view is found', () => {
|
|
84
|
+
render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
85
|
+
expect(screen.getByText('Test View')).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
it('should render error alert when viewId exists but view is not found', () => {
|
|
88
|
+
mockHitSearchContext = { ...mockHitSearchContext, viewId: 'non-existent-view' };
|
|
89
|
+
mockViewContext = {
|
|
90
|
+
...mockViewContext,
|
|
91
|
+
views: {
|
|
92
|
+
...mockViewContext.views,
|
|
93
|
+
'non-existent-view': null
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
97
|
+
const alert = screen.getByRole('alert');
|
|
98
|
+
expect(alert).toBeInTheDocument();
|
|
99
|
+
expect(alert).toHaveAttribute('aria-live', 'assertive');
|
|
100
|
+
});
|
|
101
|
+
it('should not render error alert when views object is empty (not ready)', () => {
|
|
102
|
+
mockHitSearchContext.viewId = 'non-existent-view';
|
|
103
|
+
mockViewContext.views = {};
|
|
104
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
105
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('UI Element Display', () => {
|
|
109
|
+
it('should display view title as link with correct href', () => {
|
|
110
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
111
|
+
const titleLink = screen.getByText('Test View').closest('a');
|
|
112
|
+
expect(titleLink).toHaveAttribute('href', '/views/test-view-id/edit');
|
|
113
|
+
});
|
|
114
|
+
it('should display tooltip with view query on title', () => {
|
|
115
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
116
|
+
const tooltip = screen.getByText('Test View');
|
|
117
|
+
expect(tooltip).toHaveAttribute('aria-label', 'Test View - howler.status:open');
|
|
118
|
+
});
|
|
119
|
+
it('should display translated view title', () => {
|
|
120
|
+
mockViewContext.views['test-view-id'] = createMockView({
|
|
121
|
+
title: 'view.assigned_to_me'
|
|
122
|
+
});
|
|
123
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
124
|
+
// The i18n mock should translate this key
|
|
125
|
+
expect(screen.getByText(/assigned/i)).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
it('should display edit icon when viewId exists', () => {
|
|
128
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
129
|
+
// Edit icon should be present (MUI Edit icon)
|
|
130
|
+
const editButton = screen.getByRole('link', { name: /edit /i });
|
|
131
|
+
expect(editButton).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
it('should display refresh button when viewId exists', () => {
|
|
134
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
135
|
+
const refreshButton = screen.getByRole('button');
|
|
136
|
+
expect(refreshButton).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
it('should display open button when viewId exists', () => {
|
|
139
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
140
|
+
const openLink = screen.getByRole('link', { name: /open /i });
|
|
141
|
+
expect(openLink).toBeInTheDocument();
|
|
142
|
+
expect(openLink).toHaveAttribute('href', '/search?query=howler.status:open');
|
|
143
|
+
});
|
|
144
|
+
it('should not display refresh button when viewId is null', () => {
|
|
145
|
+
mockHitSearchContext.viewId = null;
|
|
146
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
147
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
it('should not display open button when viewId is null', () => {
|
|
150
|
+
mockHitSearchContext.viewId = null;
|
|
151
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
152
|
+
expect(screen.queryByRole('link', { name: /open /i })).not.toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('Button States & Interactions', () => {
|
|
156
|
+
it('should call search function when refresh button is clicked', async () => {
|
|
157
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
158
|
+
const refreshButton = screen.getByRole('button');
|
|
159
|
+
await user.click(refreshButton);
|
|
160
|
+
expect(mockSearch).toHaveBeenCalledWith('howler.id:*');
|
|
161
|
+
});
|
|
162
|
+
it('should navigate to search when open button is clicked', () => {
|
|
163
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
164
|
+
const openLink = screen.getByRole('link', { name: /open /i });
|
|
165
|
+
expect(openLink).toHaveAttribute('href', '/search?query=howler.status:open');
|
|
166
|
+
});
|
|
167
|
+
it('should navigate to /search when error alert close button is clicked', () => {
|
|
168
|
+
mockHitSearchContext = { ...mockHitSearchContext, viewId: 'non-existent-view' };
|
|
169
|
+
mockViewContext = {
|
|
170
|
+
...mockViewContext,
|
|
171
|
+
views: {
|
|
172
|
+
...mockViewContext.views,
|
|
173
|
+
'non-existent-view': null
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
177
|
+
const closeButton = screen.getByRole('alert').querySelector('a');
|
|
178
|
+
expect(closeButton).toHaveAttribute('href', '/search');
|
|
179
|
+
});
|
|
180
|
+
it('should have correct tooltip on refresh button', () => {
|
|
181
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
182
|
+
const tooltip = screen.getByRole('button');
|
|
183
|
+
expect(tooltip).toHaveAttribute('aria-label', expect.stringContaining('Refresh'));
|
|
184
|
+
});
|
|
185
|
+
it('should have correct tooltip on open button', () => {
|
|
186
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
187
|
+
const openLink = screen.getByRole('link', { name: /open /i });
|
|
188
|
+
expect(openLink).toHaveAttribute('aria-label', expect.stringContaining('Open'));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('URL Generation (viewUrl)', () => {
|
|
192
|
+
it('should generate edit URL when viewId exists', () => {
|
|
193
|
+
render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
194
|
+
const editButton = screen.getByRole('link', { name: /edit/i });
|
|
195
|
+
expect(editButton).toHaveAttribute('href', '/views/test-view-id/edit');
|
|
196
|
+
});
|
|
197
|
+
it('should have edit tooltip when viewId exists', () => {
|
|
198
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
199
|
+
const editButton = screen.getByRole('link', { name: /edit/i });
|
|
200
|
+
expect(editButton).toHaveAttribute('aria-label', expect.stringContaining('Edit'));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('Edge Cases', () => {
|
|
204
|
+
it('should handle selectedView with missing title', () => {
|
|
205
|
+
mockViewContext.views['test-view-id'] = createMockView({
|
|
206
|
+
title: undefined
|
|
207
|
+
});
|
|
208
|
+
const { container } = render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
209
|
+
expect(container).toBeInTheDocument();
|
|
210
|
+
// Component should still render without crashing
|
|
211
|
+
});
|
|
212
|
+
it('should handle selectedView with missing query', () => {
|
|
213
|
+
mockViewContext.views['test-view-id'] = createMockView({
|
|
214
|
+
query: undefined
|
|
215
|
+
});
|
|
216
|
+
const { container } = render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
217
|
+
expect(container).toBeInTheDocument();
|
|
218
|
+
// Query tooltip should be empty or undefined
|
|
219
|
+
const tooltip = screen.getByText('Test View');
|
|
220
|
+
expect(tooltip).toHaveAttribute('aria-label', 'Test View - Unknown');
|
|
221
|
+
});
|
|
222
|
+
it('should handle rapid context changes', () => {
|
|
223
|
+
const { rerender } = render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
224
|
+
// Change viewId
|
|
225
|
+
mockHitSearchContext = { ...mockHitSearchContext, viewId: 'another-view-id' };
|
|
226
|
+
mockViewContext = {
|
|
227
|
+
...mockViewContext,
|
|
228
|
+
views: {
|
|
229
|
+
...mockViewContext.views,
|
|
230
|
+
'another-view-id': createMockView({
|
|
231
|
+
view_id: 'another-view-id',
|
|
232
|
+
title: 'Another View'
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
rerender(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
237
|
+
expect(screen.getByText('Another View')).toBeInTheDocument();
|
|
238
|
+
// Change back
|
|
239
|
+
mockHitSearchContext = { ...mockHitSearchContext, viewId: 'test-view-id' };
|
|
240
|
+
rerender(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
241
|
+
expect(screen.getByText('Test View')).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
it('should handle undefined query in parameter context', () => {
|
|
244
|
+
mockParameterContext.query = undefined;
|
|
245
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
246
|
+
const refreshButton = screen.getByRole('button');
|
|
247
|
+
fireEvent.click(refreshButton);
|
|
248
|
+
expect(mockSearch).toHaveBeenCalledWith(undefined);
|
|
249
|
+
});
|
|
250
|
+
it('should handle null sort in parameter context', () => {
|
|
251
|
+
mockParameterContext.sort = null;
|
|
252
|
+
const { container } = render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
253
|
+
expect(container).toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
it('should handle null span in parameter context', () => {
|
|
256
|
+
mockParameterContext.span = null;
|
|
257
|
+
const { container } = render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
258
|
+
expect(container).toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
it('should handle view with all optional fields missing', () => {
|
|
261
|
+
mockViewContext.views['test-view-id'] = {
|
|
262
|
+
view_id: 'test-view-id'
|
|
263
|
+
};
|
|
264
|
+
const { container } = render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
265
|
+
expect(container).toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
it('should handle empty views object', () => {
|
|
268
|
+
mockViewContext.views = {};
|
|
269
|
+
mockHitSearchContext.viewId = 'test-view-id';
|
|
270
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
271
|
+
// Should not render error alert because viewsReady is false
|
|
272
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
describe('Integration Tests', () => {
|
|
276
|
+
it('should work with all three contexts simultaneously', () => {
|
|
277
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
278
|
+
// Should use values from ParameterContext
|
|
279
|
+
const refreshButton = screen.getByRole('button');
|
|
280
|
+
fireEvent.click(refreshButton);
|
|
281
|
+
expect(mockSearch).toHaveBeenCalledWith('howler.id:*');
|
|
282
|
+
// Should use values from ViewContext
|
|
283
|
+
expect(screen.getByText('Test View')).toBeInTheDocument();
|
|
284
|
+
// Should use values from HitSearchContext
|
|
285
|
+
const editButton = screen.getByRole('link', { name: /edit/i });
|
|
286
|
+
expect(editButton).toHaveAttribute('href', '/views/test-view-id/edit');
|
|
287
|
+
});
|
|
288
|
+
it('should render correctly with different view types', () => {
|
|
289
|
+
const viewTypes = ['personal', 'global', 'readonly'];
|
|
290
|
+
const { rerender } = render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
291
|
+
viewTypes.forEach(type => {
|
|
292
|
+
mockViewContext = {
|
|
293
|
+
...mockViewContext,
|
|
294
|
+
views: {
|
|
295
|
+
...mockViewContext.views,
|
|
296
|
+
'test-view-id': createMockView({ type })
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
rerender(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
300
|
+
expect(screen.getByLabelText(i18n.t(`route.views.manager.${type}`))).toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
it('should handle multiple view IDs correctly', () => {
|
|
304
|
+
mockViewContext.views = {
|
|
305
|
+
'view-1': createMockView({ view_id: 'view-1', title: 'View 1' }),
|
|
306
|
+
'view-2': createMockView({ view_id: 'view-2', title: 'View 2' }),
|
|
307
|
+
'view-3': createMockView({ view_id: 'view-3', title: 'View 3' })
|
|
308
|
+
};
|
|
309
|
+
mockHitSearchContext.viewId = 'view-2';
|
|
310
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
311
|
+
expect(screen.getByText('View 2')).toBeInTheDocument();
|
|
312
|
+
expect(screen.queryByText('View 1')).not.toBeInTheDocument();
|
|
313
|
+
expect(screen.queryByText('View 3')).not.toBeInTheDocument();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
describe('Accessibility', () => {
|
|
317
|
+
it('should have tooltips for all icon buttons', () => {
|
|
318
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
319
|
+
const editButton = screen.getByRole('link', { name: /edit /i });
|
|
320
|
+
const refreshButton = screen.getByRole('button');
|
|
321
|
+
const openButton = screen.getByRole('link', { name: /open /i });
|
|
322
|
+
expect(editButton).toHaveAttribute('aria-label');
|
|
323
|
+
expect(refreshButton).toHaveAttribute('aria-label');
|
|
324
|
+
expect(openButton).toHaveAttribute('aria-label');
|
|
325
|
+
});
|
|
326
|
+
it('should have proper role for error alert', () => {
|
|
327
|
+
mockHitSearchContext.viewId = 'non-existent-view';
|
|
328
|
+
mockViewContext.views = {
|
|
329
|
+
'non-existent-view': null,
|
|
330
|
+
'test-view-id': createMockView()
|
|
331
|
+
};
|
|
332
|
+
render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
333
|
+
const alert = screen.getByRole('alert');
|
|
334
|
+
expect(alert).toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
it('should have accessible link text for view title', () => {
|
|
337
|
+
render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
338
|
+
const titleLink = screen.getByText('Test View').closest('a');
|
|
339
|
+
expect(titleLink).toHaveAttribute('href', '/views/test-view-id/edit');
|
|
340
|
+
expect(titleLink.textContent).toBe('Test View');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
describe('Memoization', () => {
|
|
344
|
+
it('should not re-render unnecessarily when unrelated context values change', () => {
|
|
345
|
+
const { rerender } = render(_jsx(ViewLink, {}), { wrapper: Wrapper });
|
|
346
|
+
// Change an unrelated value
|
|
347
|
+
mockParameterContext.setQuery = vi.fn();
|
|
348
|
+
rerender(_jsx(ViewLink, {}));
|
|
349
|
+
const secondRenderButton = screen.getByRole('button');
|
|
350
|
+
// Components should still be present
|
|
351
|
+
expect(secondRenderButton).toBeInTheDocument();
|
|
352
|
+
});
|
|
353
|
+
it('should update when viewId changes', () => {
|
|
354
|
+
const { rerender } = render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
355
|
+
expect(screen.getByText('Test View')).toBeInTheDocument();
|
|
356
|
+
mockHitSearchContext = { ...mockHitSearchContext, viewId: 'another-view' };
|
|
357
|
+
mockViewContext.views['another-view'] = createMockView({
|
|
358
|
+
view_id: 'another-view',
|
|
359
|
+
title: 'Another View'
|
|
360
|
+
});
|
|
361
|
+
rerender(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
362
|
+
expect(screen.getByText('Another View')).toBeInTheDocument();
|
|
363
|
+
});
|
|
364
|
+
it('should update when query changes', () => {
|
|
365
|
+
const { rerender } = render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
366
|
+
const refreshButton = screen.getByRole('button');
|
|
367
|
+
fireEvent.click(refreshButton);
|
|
368
|
+
expect(mockSearch).toHaveBeenCalledWith('howler.id:*');
|
|
369
|
+
mockParameterContext = { ...mockParameterContext, query: 'howler.status:closed' };
|
|
370
|
+
rerender(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
|
|
371
|
+
fireEvent.click(refreshButton);
|
|
372
|
+
expect(mockSearch).toHaveBeenCalledWith('howler.status:closed');
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -700,7 +700,9 @@
|
|
|
700
700
|
"usermenu.logout": "Logout",
|
|
701
701
|
"usermenu.settings": "Settings",
|
|
702
702
|
"view.assigned_to_me": "Assigned to Me",
|
|
703
|
-
"view.notfound": "This view is not
|
|
703
|
+
"view.notfound": "This view is not available to you, or does not exist.",
|
|
704
|
+
"view.open": "Open View Query",
|
|
705
|
+
"view.refresh": "Refresh View",
|
|
704
706
|
"view.settings.advance_on_triage": "Advance to next hit on triage",
|
|
705
707
|
"view.settings.advance_on_triage.description": "If this is enabled, triaging a hit on this view will automatically move to the next hit in line, making rapid triage of hits easier.",
|
|
706
708
|
"tui.query.save.alert": "Load a saved view or save the current query as a view",
|
|
@@ -700,6 +700,8 @@
|
|
|
700
700
|
"usermenu.settings": "Paramètres",
|
|
701
701
|
"view.assigned_to_me": "Assigné à moi",
|
|
702
702
|
"view.notfound": "Cette vue n'est pas disponible pour vous, ou n'existe pas.",
|
|
703
|
+
"view.open": "Ouvrir requête de la vue",
|
|
704
|
+
"view.refresh": "Rafraîchir la vue",
|
|
703
705
|
"view.settings.advance_on_triage": "Passer à la hit suivant lors du triage",
|
|
704
706
|
"view.settings.advance_on_triage.description": "Si cette option est activée, le triage d'une hit sur cette vue passera automatiquement à la hit suivant, ce qui facilitera le triage rapide des résultats.",
|
|
705
707
|
"Unclassified//Official Use Only": "Non classé//Réservé à des fins officielles",
|
package/package.json
CHANGED
package/tests/utils.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Action } from '@cccsaurora/howler-ui/models/entities/generated/Action';
|
|
2
2
|
import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
|
|
3
|
+
import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
|
|
3
4
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
4
5
|
import type { Template } from '@cccsaurora/howler-ui/models/entities/generated/Template';
|
|
6
|
+
import type { View } from '@cccsaurora/howler-ui/models/entities/generated/View';
|
|
5
7
|
type RecursivePartial<T> = {
|
|
6
8
|
[P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial<U>[] : T[P] extends object | undefined ? RecursivePartial<T[P]> : T[P];
|
|
7
9
|
};
|
|
@@ -9,4 +11,6 @@ export declare const createMockHit: (overrides?: RecursivePartial<Hit>) => Hit;
|
|
|
9
11
|
export declare const createMockAnalytic: (overrides?: Partial<Analytic>) => Analytic;
|
|
10
12
|
export declare const createMockTemplate: (overrides?: Partial<Template>) => Template;
|
|
11
13
|
export declare const createMockAction: (overrides?: Partial<Action>) => Action;
|
|
14
|
+
export declare const createMockView: (overrides?: Partial<View>) => View;
|
|
15
|
+
export declare const createMockDossier: (overrides?: Partial<Dossier>) => Dossier;
|
|
12
16
|
export {};
|
package/tests/utils.js
CHANGED
|
@@ -39,3 +39,25 @@ export const createMockAction = (overrides) => ({
|
|
|
39
39
|
],
|
|
40
40
|
...overrides
|
|
41
41
|
});
|
|
42
|
+
export const createMockView = (overrides) => ({
|
|
43
|
+
view_id: 'test-view-id',
|
|
44
|
+
title: 'Test View',
|
|
45
|
+
query: 'howler.status:open',
|
|
46
|
+
sort: 'event.created desc',
|
|
47
|
+
span: 'date.range.1.month',
|
|
48
|
+
type: 'personal',
|
|
49
|
+
owner: 'testuser',
|
|
50
|
+
settings: {
|
|
51
|
+
advance_on_triage: false
|
|
52
|
+
},
|
|
53
|
+
...overrides
|
|
54
|
+
});
|
|
55
|
+
// Helper function to create mock dossiers
|
|
56
|
+
export const createMockDossier = (overrides) => ({
|
|
57
|
+
dossier_id: 'test-dossier-id',
|
|
58
|
+
title: 'Test Dossier',
|
|
59
|
+
query: 'howler.status:open',
|
|
60
|
+
type: 'personal',
|
|
61
|
+
owner: 'test-user',
|
|
62
|
+
...overrides
|
|
63
|
+
});
|