@cccsaurora/howler-ui 2.16.0-dev.378 → 2.16.0-dev.381

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/components/app/App.js +2 -0
  2. package/components/app/hooks/useMatchers.js +0 -4
  3. package/components/app/providers/FavouritesProvider.js +2 -1
  4. package/components/app/providers/FieldProvider.d.ts +2 -2
  5. package/components/app/providers/HitProvider.d.ts +3 -3
  6. package/components/app/providers/HitSearchProvider.d.ts +7 -8
  7. package/components/app/providers/HitSearchProvider.js +64 -39
  8. package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
  9. package/components/app/providers/HitSearchProvider.test.js +505 -0
  10. package/components/app/providers/ParameterProvider.d.ts +13 -5
  11. package/components/app/providers/ParameterProvider.js +240 -84
  12. package/components/app/providers/ParameterProvider.test.d.ts +1 -0
  13. package/components/app/providers/ParameterProvider.test.js +1041 -0
  14. package/components/app/providers/ViewProvider.d.ts +3 -2
  15. package/components/app/providers/ViewProvider.js +21 -14
  16. package/components/app/providers/ViewProvider.test.js +19 -29
  17. package/components/elements/display/ChipPopper.d.ts +21 -0
  18. package/components/elements/display/ChipPopper.js +36 -0
  19. package/components/elements/display/ChipPopper.test.d.ts +1 -0
  20. package/components/elements/display/ChipPopper.test.js +309 -0
  21. package/components/elements/hit/HitActions.js +3 -3
  22. package/components/elements/hit/HitSummary.d.ts +0 -1
  23. package/components/elements/hit/HitSummary.js +11 -21
  24. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
  25. package/components/elements/hit/aggregate/HitGraph.js +9 -15
  26. package/components/routes/dossiers/DossierCard.test.js +0 -2
  27. package/components/routes/dossiers/DossierEditor.test.js +27 -33
  28. package/components/routes/hits/search/HitBrowser.js +7 -48
  29. package/components/routes/hits/search/HitContextMenu.test.js +11 -29
  30. package/components/routes/hits/search/InformationPane.js +1 -1
  31. package/components/routes/hits/search/QuerySettings.js +30 -0
  32. package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
  33. package/components/routes/hits/search/QuerySettings.test.js +553 -0
  34. package/components/routes/hits/search/SearchPane.js +8 -10
  35. package/components/routes/hits/search/ViewLink.d.ts +4 -1
  36. package/components/routes/hits/search/ViewLink.js +37 -19
  37. package/components/routes/hits/search/ViewLink.test.js +349 -303
  38. package/components/routes/hits/search/grid/HitGrid.js +2 -6
  39. package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
  40. package/components/routes/hits/search/shared/HitFilter.js +31 -23
  41. package/components/routes/hits/search/shared/HitSort.js +16 -8
  42. package/components/routes/hits/search/shared/SearchSpan.js +19 -10
  43. package/components/routes/views/ViewComposer.js +7 -6
  44. package/components/routes/views/Views.js +2 -1
  45. package/locales/en/translation.json +6 -0
  46. package/locales/fr/translation.json +6 -0
  47. package/package.json +2 -2
  48. package/setupTests.js +4 -1
  49. package/tests/mocks.d.ts +18 -0
  50. package/tests/mocks.js +65 -0
  51. package/tests/server-handlers.js +10 -28
  52. package/utils/viewUtils.d.ts +2 -0
  53. package/utils/viewUtils.js +11 -0
  54. package/components/routes/hits/search/shared/QuerySettings.js +0 -22
  55. /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
  56. /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
  57. /package/components/routes/hits/search/{CustomSort.js → shared/CustomSort.js} +0 -0
@@ -1,29 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- /* eslint-disable react/jsx-no-literals */
3
- /* eslint-disable import/imports-first */
4
2
  /// <reference types="vitest" />
5
- import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { render, screen, waitFor } from '@testing-library/react';
6
4
  import userEvent, {} from '@testing-library/user-event';
7
- import React, { createContext, useContext } from 'react';
5
+ import {} from 'react';
6
+ import { setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
8
7
  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
- });
8
+ setupReactRouterMock();
27
9
  // Import component after mocks
28
10
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
29
11
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
@@ -33,22 +15,25 @@ import { I18nextProvider } from 'react-i18next';
33
15
  import { createMockView } from '@cccsaurora/howler-ui/tests/utils';
34
16
  import ViewLink from './ViewLink';
35
17
  // Mock contexts
36
- const mockSearch = vi.fn();
37
18
  let mockParameterContext = {
38
19
  query: 'howler.id:*',
39
20
  sort: 'event.created desc',
40
21
  span: 'date.range.1.month',
22
+ views: ['test-view-id'],
41
23
  setQuery: vi.fn(),
42
24
  setSort: vi.fn(),
43
- setSpan: vi.fn()
25
+ setSpan: vi.fn(),
26
+ removeView: vi.fn(),
27
+ setView: vi.fn()
44
28
  };
45
29
  let mockHitSearchContext = {
46
- viewId: 'test-view-id',
47
- search: mockSearch
30
+ search: vi.fn()
48
31
  };
49
32
  let mockViewContext = {
33
+ getCurrentViews: vi.fn(),
50
34
  views: {
51
- 'test-view-id': createMockView()
35
+ 'test-view-id': createMockView(),
36
+ 'another-view-id': createMockView({ view_id: 'another-view-id', title: 'Another View' })
52
37
  }
53
38
  };
54
39
  // Test wrapper
@@ -61,315 +46,376 @@ describe('ViewLink', () => {
61
46
  user = userEvent.setup();
62
47
  vi.clearAllMocks();
63
48
  // Reset mock contexts to defaults
64
- mockHitSearchContext.viewId = 'test-view-id';
65
49
  mockParameterContext.query = 'howler.id:*';
66
50
  mockParameterContext.sort = 'event.created desc';
67
51
  mockParameterContext.span = 'date.range.1.month';
52
+ mockParameterContext.views = ['test-view-id'];
53
+ mockParameterContext.removeView = vi.fn();
54
+ mockParameterContext.setView = vi.fn();
55
+ mockHitSearchContext.search = vi.fn();
56
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
68
57
  mockViewContext.views = {
69
- 'test-view-id': createMockView()
58
+ 'test-view-id': createMockView(),
59
+ 'another-view-id': createMockView({ view_id: 'another-view-id', title: 'Another View' })
70
60
  };
71
61
  });
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');
62
+ describe('Loading State', () => {
63
+ it('should show loading spinner while fetching view', () => {
64
+ mockViewContext.getCurrentViews = vi.fn(() => new Promise(() => { })); // Never resolves
65
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
66
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
67
+ });
68
+ it('should hide loading spinner after view is fetched', async () => {
69
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
70
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
71
+ await screen.findByText('Test View');
72
+ expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
73
+ });
74
+ });
75
+ describe('View Selection Mode (Empty String)', () => {
76
+ it('should render selection UI when viewId is empty string', async () => {
77
+ render(_jsx(ViewLink, { id: 0, viewId: "" }), { wrapper: Wrapper });
78
+ expect(await screen.findAllByText(i18n.t('hit.search.view.select'))).toHaveLength(1);
79
+ });
80
+ it('should show autocomplete for view selection', async () => {
81
+ render(_jsx(ViewLink, { id: 0, viewId: "" }), { wrapper: Wrapper });
82
+ await user.click(await screen.findByText(i18n.t('hit.search.view.select')));
83
+ const autocomplete = screen.getByRole('combobox');
84
+ expect(autocomplete).toBeInTheDocument();
85
+ });
86
+ it('should filter out views already in currentViews', async () => {
87
+ mockParameterContext.views = ['test-view-id']; // Already selected
88
+ render(_jsx(ViewLink, { id: 1, viewId: "" }), { wrapper: Wrapper });
89
+ await user.click(await screen.findByText(i18n.t('hit.search.view.select')));
90
+ // Open the autocomplete
91
+ const autocomplete = screen.getByRole('combobox');
92
+ await user.click(autocomplete);
93
+ // test-view-id should not be in options, but another-view-id should be available
94
+ expect(screen.queryByText('Test View')).not.toBeInTheDocument();
95
+ expect(screen.queryByText('Another View')).toBeInTheDocument();
96
+ });
97
+ it('should call setView when view is selected from autocomplete', async () => {
98
+ mockParameterContext.views = [];
99
+ render(_jsx(ViewLink, { id: 0, viewId: "" }), { wrapper: Wrapper });
100
+ await user.click(await screen.findByText(i18n.t('hit.search.view.select')));
101
+ const autocomplete = screen.getByRole('combobox');
102
+ await user.click(autocomplete);
103
+ expect(mockParameterContext.setView).not.toHaveBeenCalled(); // Not clicked yet
104
+ await screen.findByText('Test View');
105
+ await user.click(await screen.findByText('Test View'));
106
+ expect(mockParameterContext.setView).toHaveBeenCalledOnce();
107
+ expect(mockParameterContext.setView).toHaveBeenCalledWith(0, 'test-view-id'); // Clicked yet
108
+ });
109
+ it('should show delete button in selection mode', async () => {
110
+ render(_jsx(ViewLink, { id: 0, viewId: "" }), { wrapper: Wrapper });
111
+ await user.click(await screen.findByText(i18n.t('hit.search.view.select')));
112
+ const deleteButton = screen.getByLabelText(i18n.t('hit.search.view.remove'));
113
+ expect(deleteButton).toBeInTheDocument();
114
+ });
115
+ it('should call removeView when delete button is clicked in selection mode', async () => {
116
+ render(_jsx(ViewLink, { id: 0, viewId: "" }), { wrapper: Wrapper });
117
+ await user.click(await screen.findByText(i18n.t('hit.search.view.select')));
118
+ const deleteButton = screen.getByLabelText(i18n.t('hit.search.view.remove'));
119
+ await user.click(deleteButton);
120
+ expect(mockParameterContext.removeView).toHaveBeenCalledWith('');
100
121
  });
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();
122
+ });
123
+ describe('View Not Found', () => {
124
+ it('should show error chip when view is not found', async () => {
125
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([]);
126
+ render(_jsx(ViewLink, { id: 0, viewId: "non-existent-view" }), { wrapper: Wrapper });
127
+ expect(await screen.findByRole('alert')).toBeInTheDocument();
128
+ expect(screen.getByText(i18n.t('view.notfound'))).toBeInTheDocument();
129
+ });
130
+ it('should have proper accessibility attributes on error alert', async () => {
131
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([]);
132
+ render(_jsx(ViewLink, { id: 0, viewId: "non-existent-view" }), { wrapper: Wrapper });
133
+ const alert = await screen.findByRole('alert');
134
+ expect(alert).toHaveAttribute('aria-live', 'assertive');
135
+ expect(alert).toHaveAttribute('aria-atomic', 'true');
136
+ });
137
+ it('should call removeView when error chip is deleted', async () => {
138
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([]);
139
+ render(_jsx(ViewLink, { id: 0, viewId: "non-existent-view" }), { wrapper: Wrapper });
140
+ const errorChip = await screen.findByRole('alert');
141
+ const deleteButton = errorChip.querySelector('[data-testid="CancelIcon"]')?.closest('button');
142
+ if (deleteButton) {
143
+ await user.click(deleteButton);
144
+ expect(mockParameterContext.removeView).toHaveBeenCalledWith('non-existent-view');
145
+ }
106
146
  });
107
147
  });
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, {}) }));
148
+ describe('Valid View Display', () => {
149
+ it('should display view title as link', async () => {
150
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
151
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
152
+ const titleLink = await screen.findByText('Test View');
153
+ expect(titleLink.closest('a')).toHaveAttribute('href', '/views/test-view-id/edit');
154
+ });
155
+ it('should display tooltip with view query on title', async () => {
156
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
157
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
158
+ const tooltip = await screen.findByText('Test View');
159
+ expect(tooltip).toHaveAttribute('aria-label', expect.stringContaining('howler.status:open'));
160
+ });
161
+ it('should display translated view title', async () => {
162
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
163
+ createMockView({
164
+ title: 'view.assigned_to_me'
165
+ })
166
+ ]);
167
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
124
168
  // The i18n mock should translate this key
125
- expect(screen.getByText(/assigned/i)).toBeInTheDocument();
169
+ expect(await screen.findByText(/assigned/i)).toBeInTheDocument();
170
+ });
171
+ it('should display correct icon based on view type', async () => {
172
+ const viewTypes = ['personal', 'global', 'readonly'];
173
+ for (const type of viewTypes) {
174
+ vi.clearAllMocks();
175
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView({ type })]);
176
+ const { unmount } = render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
177
+ expect(await screen.findByLabelText(i18n.t(`route.views.manager.${type}`))).toBeInTheDocument();
178
+ unmount();
179
+ }
180
+ });
181
+ it('should display delete button to remove view', async () => {
182
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
183
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
184
+ await screen.findByText('Test View');
185
+ // The chip should have a delete button
186
+ const chip = screen.getByText('Test View').closest('[role="button"]');
187
+ expect(chip?.querySelector('[data-testid="CancelIcon"]')).toBeInTheDocument();
188
+ });
189
+ it('should call removeView when delete button is clicked', async () => {
190
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
191
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
192
+ await screen.findByText('Test View');
193
+ // Find and click the delete button on the chip
194
+ const chip = screen.getByText('Test View').closest('[role="button"]');
195
+ const deleteButton = chip?.querySelector('[data-testid="CancelIcon"]')?.closest('button');
196
+ if (deleteButton) {
197
+ await user.click(deleteButton);
198
+ expect(mockParameterContext.removeView).toHaveBeenCalledWith('test-view-id');
199
+ }
126
200
  });
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 });
201
+ });
202
+ describe('Action Buttons', () => {
203
+ beforeEach(async () => {
204
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
205
+ });
206
+ it('should display edit button', async () => {
207
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
208
+ await screen.findByText('Test View');
209
+ await user.click(screen.getByText('Test View').parentElement);
210
+ const editButton = await screen.findByLabelText(i18n.t('route.views.edit'));
131
211
  expect(editButton).toBeInTheDocument();
132
212
  });
133
- it('should display refresh button when viewId exists', () => {
134
- render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
135
- const refreshButton = screen.getByRole('button');
213
+ it('should navigate to edit page when edit button is clicked', async () => {
214
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
215
+ await screen.findByText('Test View');
216
+ await user.click(screen.getByText('Test View').parentElement);
217
+ const editButton = await screen.findByLabelText(i18n.t('route.views.edit'));
218
+ expect(editButton.closest('a')).toHaveAttribute('href', '/views/test-view-id/edit');
219
+ });
220
+ it('should display refresh button', async () => {
221
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
222
+ await screen.findByText('Test View');
223
+ await user.click(screen.getByText('Test View').parentElement);
224
+ const refreshButton = await screen.findByLabelText(i18n.t('view.refresh'));
136
225
  expect(refreshButton).toBeInTheDocument();
137
226
  });
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
227
  it('should call search function when refresh button is clicked', async () => {
157
- render(_jsx(Wrapper, { children: _jsx(ViewLink, {}) }));
158
- const refreshButton = screen.getByRole('button');
228
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
229
+ await screen.findByText('Test View');
230
+ await user.click(screen.getByText('Test View').parentElement);
231
+ const refreshButton = await screen.findByLabelText(i18n.t('view.refresh'));
159
232
  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'));
233
+ expect(mockHitSearchContext.search).toHaveBeenCalledWith('howler.id:*');
234
+ });
235
+ it('should display open button', async () => {
236
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
237
+ await screen.findByText('Test View');
238
+ await user.click(screen.getByText('Test View').parentElement);
239
+ const openButton = await screen.findByLabelText(i18n.t('view.open'));
240
+ expect(openButton).toBeInTheDocument();
241
+ });
242
+ it('should navigate to search with view query when open button is clicked', async () => {
243
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
244
+ await screen.findByText('Test View');
245
+ await user.click(screen.getByText('Test View').parentElement);
246
+ const openButton = await screen.findByLabelText(i18n.t('view.open'));
247
+ expect(openButton.closest('a')).toHaveAttribute('href', '/search?query=howler.status:open');
189
248
  });
190
249
  });
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'));
250
+ describe('URL Generation', () => {
251
+ it('should generate edit URL when view exists', async () => {
252
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
253
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
254
+ await screen.findByText('Test View');
255
+ await user.click(screen.getByText('Test View').parentElement);
256
+ const editButton = await screen.findByLabelText(i18n.t('route.views.edit'));
257
+ expect(editButton.closest('a')).toHaveAttribute('href', '/views/test-view-id/edit');
258
+ });
259
+ it('should generate create URL with query params when view does not exist', async () => {
260
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([]);
261
+ mockParameterContext.query = 'test-query';
262
+ mockParameterContext.sort = 'test-sort';
263
+ mockParameterContext.span = 'test-span';
264
+ render(_jsx(ViewLink, { id: 0, viewId: "new-view" }), { wrapper: Wrapper });
265
+ // Wait for loading to complete
266
+ await screen.findByRole('alert');
267
+ // The viewUrl memo should generate create URL with params
268
+ // This is tested indirectly through the edit button when view is null
269
+ });
270
+ it('should generate create URL without params when no query/sort/span', async () => {
271
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([]);
272
+ mockParameterContext.query = null;
273
+ mockParameterContext.sort = null;
274
+ mockParameterContext.span = null;
275
+ render(_jsx(ViewLink, { id: 0, viewId: "new-view" }), { wrapper: Wrapper });
276
+ await screen.findByRole('alert');
277
+ // Should generate /views/create without query params
201
278
  });
202
279
  });
203
280
  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();
281
+ it('should handle view with missing title', async () => {
282
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
283
+ createMockView({
284
+ title: undefined
285
+ })
286
+ ]);
287
+ const { container } = render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
210
288
  // 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', () => {
289
+ await waitFor(() => expect(container).toBeInTheDocument());
290
+ });
291
+ it('should handle view with missing query', async () => {
292
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
293
+ createMockView({
294
+ query: undefined
295
+ })
296
+ ]);
297
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
298
+ const titleLink = await screen.findByText('Test View');
299
+ expect(titleLink).toHaveAttribute('aria-label', expect.stringContaining(i18n.t('unknown')));
300
+ });
301
+ it('should handle undefined query in parameter context', async () => {
244
302
  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();
303
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
304
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
305
+ await screen.findByText('Test View');
306
+ await user.click(screen.getByText('Test View').parentElement);
307
+ const refreshButton = await screen.findByLabelText(i18n.t('view.refresh'));
308
+ await user.click(refreshButton);
309
+ expect(mockHitSearchContext.search).toHaveBeenCalledWith(undefined);
310
+ });
311
+ it('should handle custom span correctly', async () => {
312
+ mockParameterContext.span = 'date.range.custom';
313
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
314
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
315
+ await screen.findByText('Test View');
316
+ await user.click(screen.getByText('Test View').parentElement);
317
+ // Edit button should be disabled when span is custom
318
+ const editButton = await screen.findByLabelText(i18n.t('route.views.edit'));
319
+ expect(editButton).toHaveAttribute('aria-disabled', 'true');
320
+ });
321
+ it('should disable edit button when no query and no view', async () => {
322
+ mockParameterContext.query = null;
323
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([]);
324
+ render(_jsx(ViewLink, { id: 0, viewId: "new-view" }), { wrapper: Wrapper });
325
+ await screen.findByRole('alert');
326
+ // Can't test disabled state directly on error chip
327
+ });
328
+ it('should handle rapid viewId changes', async () => {
329
+ const { rerender } = render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
330
+ expect(await screen.findByText('Test View')).toBeInTheDocument();
331
+ // Change viewId
332
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
333
+ createMockView({
334
+ view_id: 'another-view-id',
335
+ title: 'Another View'
336
+ })
337
+ ]);
338
+ rerender(_jsx(ViewLink, { id: 0, viewId: "another-view-id" }));
339
+ expect(await screen.findByText('Another View')).toBeInTheDocument();
340
+ });
341
+ it('should call getCurrentViews when viewId changes', async () => {
342
+ const { rerender } = render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
343
+ await screen.findByText('Test View');
344
+ expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ viewId: 'test-view-id', ignoreParams: true });
345
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([
346
+ createMockView({
347
+ view_id: 'another-view-id',
348
+ title: 'Another View'
349
+ })
350
+ ]);
351
+ rerender(_jsx(ViewLink, { id: 0, viewId: "another-view-id" }));
352
+ await screen.findByText('Another View');
353
+ expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ viewId: 'another-view-id', ignoreParams: true });
314
354
  });
315
355
  });
316
356
  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 });
357
+ it('should have accessible link text for view title', async () => {
358
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
359
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
360
+ const titleLink = await screen.findByText('Test View');
361
+ expect(titleLink).toHaveAttribute('role', 'link');
362
+ expect(titleLink.textContent).toBe('Test View');
363
+ });
364
+ it('should have tooltips for all action buttons', async () => {
365
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
366
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
367
+ await screen.findByText('Test View');
368
+ await user.click(screen.getByText('Test View').parentElement);
369
+ const editButton = await screen.findByLabelText(i18n.t('route.views.edit'));
370
+ const refreshButton = await screen.findByLabelText(i18n.t('view.refresh'));
371
+ const openButton = await screen.findByLabelText(i18n.t('view.open'));
322
372
  expect(editButton).toHaveAttribute('aria-label');
323
373
  expect(refreshButton).toHaveAttribute('aria-label');
324
374
  expect(openButton).toHaveAttribute('aria-label');
325
375
  });
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');
376
+ it('should have proper ARIA attributes on view type icon', async () => {
377
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
378
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
379
+ const icon = await screen.findByLabelText(i18n.t('route.views.manager.personal'));
380
+ expect(icon).toHaveAttribute('aria-label');
341
381
  });
342
382
  });
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');
383
+ describe('Integration with Context', () => {
384
+ it('should use getCurrentViews from ViewContext', async () => {
385
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
386
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
387
+ await screen.findByText('Test View');
388
+ expect(mockViewContext.getCurrentViews).toHaveBeenCalledWith({ viewId: 'test-view-id', ignoreParams: true });
389
+ });
390
+ it('should use removeView from ParameterContext', async () => {
391
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
392
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
393
+ await screen.findByText('Test View');
394
+ const chip = screen.getByText('Test View').closest('[role="button"]');
395
+ const deleteButton = chip?.querySelector('[data-testid="CancelIcon"]')?.closest('button');
396
+ if (deleteButton) {
397
+ await user.click(deleteButton);
398
+ expect(mockParameterContext.removeView).toHaveBeenCalledWith('test-view-id');
399
+ }
400
+ });
401
+ it('should use search from HitSearchContext', async () => {
402
+ mockViewContext.getCurrentViews = vi.fn().mockResolvedValue([createMockView()]);
403
+ render(_jsx(ViewLink, { id: 0, viewId: "test-view-id" }), { wrapper: Wrapper });
404
+ await screen.findByText('Test View');
405
+ await user.click(screen.getByText('Test View').parentElement);
406
+ const refreshButton = await screen.findByLabelText(i18n.t('view.refresh'));
407
+ await user.click(refreshButton);
408
+ expect(mockHitSearchContext.search).toHaveBeenCalledWith('howler.id:*');
409
+ });
410
+ it('should filter available views using currentViews from ParameterContext', async () => {
411
+ mockParameterContext.views = ['test-view-id'];
412
+ render(_jsx(ViewLink, { id: 1, viewId: "" }), { wrapper: Wrapper });
413
+ await user.click(await screen.findByText(i18n.t('hit.search.view.select')));
414
+ // Open the autocomplete
415
+ const autocomplete = screen.getByRole('combobox');
416
+ await user.click(autocomplete);
417
+ expect(screen.queryByText('Test View')).not.toBeInTheDocument();
418
+ expect(screen.queryByText('Another View')).toBeInTheDocument();
373
419
  });
374
420
  });
375
421
  });