@cccsaurora/howler-ui 2.16.0-dev.376 → 2.16.0-dev.380
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/commons/components/app/hooks/useAppConfigs.d.ts +1 -1
- package/components/app/App.js +2 -0
- package/components/app/hooks/useMatchers.js +0 -4
- package/components/app/providers/FavouritesProvider.js +2 -1
- package/components/app/providers/FieldProvider.d.ts +2 -2
- package/components/app/providers/HitProvider.d.ts +3 -3
- package/components/app/providers/HitSearchProvider.d.ts +7 -8
- package/components/app/providers/HitSearchProvider.js +64 -39
- package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
- package/components/app/providers/HitSearchProvider.test.js +505 -0
- package/components/app/providers/ParameterProvider.d.ts +13 -5
- package/components/app/providers/ParameterProvider.js +240 -84
- package/components/app/providers/ParameterProvider.test.d.ts +1 -0
- package/components/app/providers/ParameterProvider.test.js +1041 -0
- package/components/app/providers/ViewProvider.d.ts +3 -2
- package/components/app/providers/ViewProvider.js +21 -14
- package/components/app/providers/ViewProvider.test.js +19 -29
- package/components/elements/display/ChipPopper.d.ts +21 -0
- package/components/elements/display/ChipPopper.js +36 -0
- package/components/elements/display/ChipPopper.test.d.ts +1 -0
- package/components/elements/display/ChipPopper.test.js +309 -0
- package/components/elements/hit/HitActions.js +3 -3
- package/components/elements/hit/HitSummary.d.ts +0 -1
- package/components/elements/hit/HitSummary.js +11 -21
- package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
- package/components/elements/hit/aggregate/HitGraph.js +9 -15
- package/components/routes/dossiers/DossierCard.test.js +0 -2
- package/components/routes/dossiers/DossierEditor.test.js +27 -33
- package/components/routes/hits/search/HitBrowser.js +7 -48
- package/components/routes/hits/search/HitContextMenu.test.js +11 -29
- package/components/routes/hits/search/InformationPane.js +1 -1
- package/components/routes/hits/search/QuerySettings.js +30 -0
- package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
- package/components/routes/hits/search/QuerySettings.test.js +553 -0
- package/components/routes/hits/search/SearchPane.js +8 -10
- package/components/routes/hits/search/ViewLink.d.ts +4 -1
- package/components/routes/hits/search/ViewLink.js +37 -19
- package/components/routes/hits/search/ViewLink.test.js +349 -303
- package/components/routes/hits/search/grid/HitGrid.js +2 -6
- package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
- package/components/routes/hits/search/shared/HitFilter.js +31 -23
- package/components/routes/hits/search/shared/HitSort.js +16 -8
- package/components/routes/hits/search/shared/SearchSpan.js +19 -10
- package/components/routes/views/ViewComposer.js +7 -6
- package/components/routes/views/Views.js +2 -1
- package/locales/en/translation.json +6 -0
- package/locales/fr/translation.json +6 -0
- package/package.json +2 -2
- package/setupTests.js +4 -1
- package/tests/mocks.d.ts +18 -0
- package/tests/mocks.js +65 -0
- package/tests/server-handlers.js +10 -28
- package/utils/viewUtils.d.ts +2 -0
- package/utils/viewUtils.js +11 -0
- package/components/routes/hits/search/shared/QuerySettings.js +0 -22
- /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
- /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
- /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 {
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
6
4
|
import userEvent, {} from '@testing-library/user-event';
|
|
7
|
-
import
|
|
5
|
+
import {} from 'react';
|
|
6
|
+
import { setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
|
|
8
7
|
import { vi } from 'vitest';
|
|
9
|
-
|
|
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
|
-
|
|
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('
|
|
73
|
-
it('should
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
expect(
|
|
77
|
-
});
|
|
78
|
-
it('should
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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('
|
|
109
|
-
it('should display view title as link
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
134
|
-
render(_jsx(
|
|
135
|
-
|
|
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(
|
|
158
|
-
|
|
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(
|
|
161
|
-
});
|
|
162
|
-
it('should
|
|
163
|
-
render(_jsx(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
192
|
-
it('should generate edit URL when
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
205
|
-
mockViewContext.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
expect(screen.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
expect(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
expect(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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('
|
|
344
|
-
it('should
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const refreshButton = screen.
|
|
367
|
-
|
|
368
|
-
expect(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
});
|