@cccsaurora/howler-ui 3.0.4 → 3.0.5-dev.1465
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/routes/hits/search/InformationPane.js +9 -4
- package/components/routes/hits/search/SearchPane.js +2 -1
- package/components/routes/hits/view/HitViewer.js +32 -2
- package/components/routes/hits/view/HitViewer.test.d.ts +1 -0
- package/components/routes/hits/view/HitViewer.test.js +261 -0
- package/package.json +1 -1
|
@@ -46,7 +46,7 @@ const InformationPane = ({ onClose, selected: _selected }) => {
|
|
|
46
46
|
const { emit, open } = useContext(SocketContext);
|
|
47
47
|
const { getMatchingOverview, getMatchingDossiers, getMatchingAnalytic } = useMatchers();
|
|
48
48
|
const selected = useContextSelector(ParameterContext, ctx => ctx?.selected) ?? _selected;
|
|
49
|
-
const
|
|
49
|
+
const { executeFunction } = usePluginStore();
|
|
50
50
|
const getRecord = useContextSelector(RecordContext, ctx => ctx.getRecord);
|
|
51
51
|
const [userIds, setUserIds] = useState(new Set());
|
|
52
52
|
const [analytic, setAnalytic] = useState();
|
|
@@ -59,9 +59,14 @@ const InformationPane = ({ onClose, selected: _selected }) => {
|
|
|
59
59
|
const dossiers = useMemo(() => _dossiers ?? [], [_dossiers]);
|
|
60
60
|
const users = useMyUserList(userIds);
|
|
61
61
|
const record = useContextSelector(RecordContext, ctx => ctx.records[selected]);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!selected) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
howlerPluginStore.plugins.forEach(plugin => {
|
|
67
|
+
executeFunction(`${plugin}.on`, 'viewing');
|
|
68
|
+
});
|
|
69
|
+
}, [executeFunction, selected]);
|
|
65
70
|
useEffect(() => {
|
|
66
71
|
if (!selected) {
|
|
67
72
|
return;
|
|
@@ -86,6 +86,7 @@ const SearchPane = () => {
|
|
|
86
86
|
const error = useContextSelector(RecordSearchContext, ctx => ctx.error);
|
|
87
87
|
const { onClick } = useRecordSelection();
|
|
88
88
|
const searchPaneWidth = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null)[0];
|
|
89
|
+
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
89
90
|
const verticalSorters = useMediaQuery('(max-width: 1919px)') || (searchPaneWidth ?? Number.MAX_SAFE_INTEGER) < 900;
|
|
90
91
|
const getSelectedId = useCallback((event) => {
|
|
91
92
|
const target = event.target;
|
|
@@ -102,6 +103,6 @@ const SearchPane = () => {
|
|
|
102
103
|
bottom: 0,
|
|
103
104
|
borderBottomLeftRadius: theme.shape.borderRadius,
|
|
104
105
|
borderBottomRightRadius: theme.shape.borderRadius
|
|
105
|
-
}) }))] }), _jsx(QuerySettings, { verticalSorters: verticalSorters, boxSx: { position: 'relative', pt: 1.5 } })] }), response && (_jsxs(Stack, { direction: "row", alignItems: "center", sx: { pt: 1 }, children: [_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) }), _jsx(Box, { flex: 1 }), _jsx(SearchPagination, { total: response.total, limit:
|
|
106
|
+
}) }))] }), _jsx(QuerySettings, { verticalSorters: verticalSorters, boxSx: { position: 'relative', pt: 1.5 } })] }), response && (_jsxs(Stack, { direction: "row", alignItems: "center", sx: { pt: 1 }, children: [_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) }), _jsx(Box, { flex: 1 }), _jsx(SearchPagination, { total: response.total, limit: pageCount, offset: response.offset, onChange: nextOffset => setOffset(nextOffset) })] }))] }), _jsx(VSBoxContent, { mr: -1, ml: -1, mt: 1, children: _jsx(RecordContextMenu, { getSelectedId: getSelectedId, children: !response ? (_jsx(AppListEmpty, {})) : (response.items.map(record => _jsx(Item, { record: record, onClick: onClick }, record.howler.id))) }) })] }) }) }));
|
|
106
107
|
};
|
|
107
108
|
export default memo(SearchPane);
|
|
@@ -5,6 +5,7 @@ import { Badge, Box, CardContent, Collapse, IconButton, Skeleton, Stack, Tab, Ta
|
|
|
5
5
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
6
6
|
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
7
7
|
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
8
|
+
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
8
9
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
9
10
|
import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
|
|
10
11
|
import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
|
|
@@ -22,8 +23,10 @@ import RecordRelated from '@cccsaurora/howler-ui/components/elements/record/Reco
|
|
|
22
23
|
import RecordWorklog from '@cccsaurora/howler-ui/components/elements/record/RecordWorklog';
|
|
23
24
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
24
25
|
import useMyUserList from '@cccsaurora/howler-ui/components/hooks/useMyUserList';
|
|
25
|
-
import
|
|
26
|
+
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
27
|
+
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
26
28
|
import { useTranslation } from 'react-i18next';
|
|
29
|
+
import { usePluginStore } from 'react-pluggable';
|
|
27
30
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
28
31
|
import { useContextSelector } from 'use-context-selector';
|
|
29
32
|
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
@@ -43,6 +46,8 @@ const HitViewer = () => {
|
|
|
43
46
|
const isUnderLg = useMediaQuery(theme.breakpoints.down('lg'));
|
|
44
47
|
const [orientation, setOrientation] = useMyLocalStorageItem(StorageKey.VIEWER_ORIENTATION, Orientation.VERTICAL);
|
|
45
48
|
const { getMatchingOverview, getMatchingDossiers, getMatchingAnalytic } = useMatchers();
|
|
49
|
+
const { emit, open } = useContext(SocketContext);
|
|
50
|
+
const { executeFunction } = usePluginStore();
|
|
46
51
|
const getHit = useContextSelector(RecordContext, ctx => ctx.getRecord);
|
|
47
52
|
const hit = useContextSelector(RecordContext, ctx => ctx.records[params.id]);
|
|
48
53
|
const [userIds, setUserIds] = useState(new Set());
|
|
@@ -62,7 +67,7 @@ const HitViewer = () => {
|
|
|
62
67
|
}
|
|
63
68
|
catch (err) {
|
|
64
69
|
if (err.cause?.api_status_code === 404) {
|
|
65
|
-
navigate('/404');
|
|
70
|
+
void navigate('/404');
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
}, [hit, getMatchingAnalytic, getHit, params.id, navigate]);
|
|
@@ -71,9 +76,34 @@ const HitViewer = () => {
|
|
|
71
76
|
setOrientation(Orientation.HORIZONTAL);
|
|
72
77
|
}
|
|
73
78
|
}, [isUnderLg, setOrientation]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!hit) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
howlerPluginStore.plugins.forEach(plugin => {
|
|
84
|
+
executeFunction(`${plugin}.on`, 'viewing');
|
|
85
|
+
});
|
|
86
|
+
}, [executeFunction, hit]);
|
|
74
87
|
useEffect(() => {
|
|
75
88
|
void fetchData();
|
|
76
89
|
}, [params.id, fetchData, hit]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!params.id || !open) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
emit({
|
|
95
|
+
broadcast: false,
|
|
96
|
+
action: 'viewing',
|
|
97
|
+
id: params.id
|
|
98
|
+
});
|
|
99
|
+
return () => {
|
|
100
|
+
emit({
|
|
101
|
+
broadcast: false,
|
|
102
|
+
action: 'stop_viewing',
|
|
103
|
+
id: params.id
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
}, [emit, params.id, open]);
|
|
77
107
|
const onOrientationChange = useCallback(() => setOrientation(orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL), [orientation, setOrientation]);
|
|
78
108
|
useEffect(() => {
|
|
79
109
|
void getMatchingOverview(hit).then(_overview => setHasOverview(!!_overview));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
5
|
+
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
const mockEmit = vi.hoisted(() => vi.fn());
|
|
8
|
+
const mockExecutePlugin = vi.hoisted(() => vi.fn());
|
|
9
|
+
const mockOpen = vi.hoisted(() => ({ current: true }));
|
|
10
|
+
const mockParams = vi.hoisted(() => ({ id: 'hit-1' }));
|
|
11
|
+
const mockGetHit = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
12
|
+
const mockGetMatchingAnalytic = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
13
|
+
const mockGetMatchingDossiers = vi.hoisted(() => vi.fn().mockResolvedValue([]));
|
|
14
|
+
const mockGetMatchingOverview = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
15
|
+
const mockNavigate = vi.hoisted(() => vi.fn());
|
|
16
|
+
const mockUseMediaQuery = vi.hoisted(() => vi.fn(() => false));
|
|
17
|
+
const mockSetOrientation = vi.hoisted(() => vi.fn());
|
|
18
|
+
vi.mock('@mui/material', async () => {
|
|
19
|
+
const actual = await vi.importActual('@mui/material');
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
useMediaQuery: mockUseMediaQuery
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
vi.mock('plugins/store', () => ({
|
|
26
|
+
default: {
|
|
27
|
+
plugins: ['test-plugin']
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
vi.mock('react-pluggable', () => ({
|
|
31
|
+
usePluginStore: () => ({ executeFunction: mockExecutePlugin })
|
|
32
|
+
}));
|
|
33
|
+
vi.mock('react-i18next', () => ({
|
|
34
|
+
useTranslation: () => ({
|
|
35
|
+
t: (key) => key,
|
|
36
|
+
i18n: { language: 'en' }
|
|
37
|
+
})
|
|
38
|
+
}));
|
|
39
|
+
vi.mock('react-router-dom', async () => {
|
|
40
|
+
const actual = await vi.importActual('react-router-dom');
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
useNavigate: () => mockNavigate,
|
|
44
|
+
useParams: () => mockParams
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
vi.mock('components/app/hooks/useMatchers', () => ({
|
|
48
|
+
default: () => ({
|
|
49
|
+
getMatchingAnalytic: mockGetMatchingAnalytic,
|
|
50
|
+
getMatchingDossiers: mockGetMatchingDossiers,
|
|
51
|
+
getMatchingOverview: mockGetMatchingOverview
|
|
52
|
+
})
|
|
53
|
+
}));
|
|
54
|
+
vi.mock('components/hooks/useMyLocalStorage', () => ({
|
|
55
|
+
useMyLocalStorageItem: () => [Orientation.VERTICAL, mockSetOrientation]
|
|
56
|
+
}));
|
|
57
|
+
vi.mock('components/hooks/useMyUserList', () => ({
|
|
58
|
+
default: () => []
|
|
59
|
+
}));
|
|
60
|
+
vi.mock('utils/recordFunctions', () => ({
|
|
61
|
+
getUserList: () => new Set(['analyst'])
|
|
62
|
+
}));
|
|
63
|
+
vi.mock('utils/utils', () => ({
|
|
64
|
+
tryParse: (value) => `parsed:${value}`
|
|
65
|
+
}));
|
|
66
|
+
vi.mock('components/elements/hit/HitActions', () => ({
|
|
67
|
+
default: ({ orientation }) => _jsxs("div", { children: ["actions:", orientation] })
|
|
68
|
+
}));
|
|
69
|
+
vi.mock('components/elements/display/icons/SocketBadge', () => ({
|
|
70
|
+
default: () => null
|
|
71
|
+
}));
|
|
72
|
+
vi.mock('components/elements/hit/HitBanner', () => ({
|
|
73
|
+
default: () => _jsx("div", { children: "banner" })
|
|
74
|
+
}));
|
|
75
|
+
vi.mock('components/elements/hit/HitOutline', () => ({
|
|
76
|
+
default: () => _jsx("div", { children: "outline" })
|
|
77
|
+
}));
|
|
78
|
+
vi.mock('components/elements/hit/HitLabels', () => ({
|
|
79
|
+
default: () => _jsx("div", { children: "labels" })
|
|
80
|
+
}));
|
|
81
|
+
vi.mock('components/elements/hit/HitLinks', () => ({
|
|
82
|
+
default: ({ analytic, dossiers }) => (_jsxs("div", { children: ["links:", analytic?.analytic_id, ":", dossiers.length] }))
|
|
83
|
+
}));
|
|
84
|
+
vi.mock('components/elements/hit/HitOverview', () => ({
|
|
85
|
+
default: () => _jsx("div", { children: "overview-content" })
|
|
86
|
+
}));
|
|
87
|
+
vi.mock('components/elements/ObjectDetails', () => ({
|
|
88
|
+
default: () => _jsx("div", { children: "details-content" })
|
|
89
|
+
}));
|
|
90
|
+
vi.mock('components/elements/display/json/JSONViewer', () => ({
|
|
91
|
+
default: ({ data }) => _jsx("div", { id: "json-content", children: JSON.stringify(data) })
|
|
92
|
+
}));
|
|
93
|
+
vi.mock('components/elements/record/RecordComments', () => ({
|
|
94
|
+
default: () => _jsx("div", { children: "comments-content" })
|
|
95
|
+
}));
|
|
96
|
+
vi.mock('components/elements/record/RecordWorklog', () => ({
|
|
97
|
+
default: () => _jsx("div", { children: "worklog-content" })
|
|
98
|
+
}));
|
|
99
|
+
vi.mock('components/elements/record/RecordRelated', () => ({
|
|
100
|
+
default: () => _jsx("div", { children: "related-content" })
|
|
101
|
+
}));
|
|
102
|
+
vi.mock('./LeadRenderer', () => ({
|
|
103
|
+
default: ({ lead }) => _jsxs("div", { children: ["lead-content:", lead.label.en] })
|
|
104
|
+
}));
|
|
105
|
+
import HitViewer, { Orientation } from './HitViewer';
|
|
106
|
+
const hit = {
|
|
107
|
+
__index: 'hit',
|
|
108
|
+
timestamp: '2026-01-01T00:00:00Z',
|
|
109
|
+
howler: {
|
|
110
|
+
id: 'hit-1',
|
|
111
|
+
analytic: 'analytic-1',
|
|
112
|
+
assignment: 'analyst',
|
|
113
|
+
hash: 'hash-1',
|
|
114
|
+
data: ['{"source":"data"}'],
|
|
115
|
+
dossier: [
|
|
116
|
+
{
|
|
117
|
+
label: { en: 'Local lead', fr: 'Piste locale' },
|
|
118
|
+
format: 'markdown',
|
|
119
|
+
content: 'local'
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
comment: [{}]
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const recordContextValue = {
|
|
126
|
+
records: { 'hit-1': hit },
|
|
127
|
+
getRecord: mockGetHit
|
|
128
|
+
};
|
|
129
|
+
const createWrapper = () => {
|
|
130
|
+
const Wrapper = ({ children }) => (_jsx(SocketContext.Provider, { value: {
|
|
131
|
+
emit: mockEmit,
|
|
132
|
+
open: mockOpen.current,
|
|
133
|
+
addListener: vi.fn(),
|
|
134
|
+
removeListener: vi.fn(),
|
|
135
|
+
status: 1,
|
|
136
|
+
reconnect: vi.fn(),
|
|
137
|
+
viewers: {},
|
|
138
|
+
fetchViewers: vi.fn()
|
|
139
|
+
}, children: _jsx(RecordContext.Provider, { value: recordContextValue, children: children }) }));
|
|
140
|
+
return Wrapper;
|
|
141
|
+
};
|
|
142
|
+
const renderViewer = () => render(_jsx(HitViewer, {}), { wrapper: createWrapper() });
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
mockEmit.mockReset();
|
|
145
|
+
mockExecutePlugin.mockReset();
|
|
146
|
+
mockOpen.current = true;
|
|
147
|
+
mockParams.id = 'hit-1';
|
|
148
|
+
mockUseMediaQuery.mockReset().mockReturnValue(false);
|
|
149
|
+
mockSetOrientation.mockReset();
|
|
150
|
+
mockGetHit.mockReset().mockResolvedValue(undefined);
|
|
151
|
+
mockGetMatchingAnalytic.mockReset().mockResolvedValue(undefined);
|
|
152
|
+
mockGetMatchingDossiers.mockReset().mockResolvedValue([]);
|
|
153
|
+
mockGetMatchingOverview.mockReset().mockResolvedValue(undefined);
|
|
154
|
+
mockNavigate.mockReset();
|
|
155
|
+
recordContextValue.records = { 'hit-1': hit };
|
|
156
|
+
});
|
|
157
|
+
describe('HitViewer', () => {
|
|
158
|
+
it('loads a missing hit and shows the loading state', async () => {
|
|
159
|
+
recordContextValue.records = {};
|
|
160
|
+
renderViewer();
|
|
161
|
+
expect(screen.queryByText('details-content')).not.toBeInTheDocument();
|
|
162
|
+
await waitFor(() => expect(mockGetHit).toHaveBeenCalledWith('hit-1', true));
|
|
163
|
+
});
|
|
164
|
+
it('navigates to the not-found page when loading fails with a 404', async () => {
|
|
165
|
+
recordContextValue.records = {};
|
|
166
|
+
mockGetHit.mockRejectedValue({ cause: { api_status_code: 404 } });
|
|
167
|
+
renderViewer();
|
|
168
|
+
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/404'));
|
|
169
|
+
});
|
|
170
|
+
it('fetches matching data, notifies plugins, and renders the details view', async () => {
|
|
171
|
+
const analytic = { analytic_id: 'analytic-1' };
|
|
172
|
+
const dossiers = [{ leads: [] }];
|
|
173
|
+
mockGetMatchingAnalytic.mockResolvedValue(analytic);
|
|
174
|
+
mockGetMatchingDossiers.mockResolvedValue(dossiers);
|
|
175
|
+
renderViewer();
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(mockGetMatchingAnalytic).toHaveBeenCalledWith(hit);
|
|
178
|
+
expect(mockGetMatchingDossiers).toHaveBeenCalledWith(hit);
|
|
179
|
+
expect(screen.getByText('links:analytic-1:1')).toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
expect(screen.getByText('details-content')).toBeInTheDocument();
|
|
182
|
+
expect(mockExecutePlugin).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(mockExecutePlugin).toHaveBeenCalledWith('test-plugin.on', 'viewing');
|
|
184
|
+
});
|
|
185
|
+
it('emits viewing and stop_viewing socket events', async () => {
|
|
186
|
+
const { unmount } = renderViewer();
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(mockEmit).toHaveBeenCalledWith({
|
|
189
|
+
broadcast: false,
|
|
190
|
+
action: 'viewing',
|
|
191
|
+
id: 'hit-1'
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
mockEmit.mockClear();
|
|
195
|
+
unmount();
|
|
196
|
+
expect(mockEmit).toHaveBeenCalledWith({
|
|
197
|
+
broadcast: false,
|
|
198
|
+
action: 'stop_viewing',
|
|
199
|
+
id: 'hit-1'
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('does not emit socket events when the socket is closed or no hit id is present', async () => {
|
|
203
|
+
mockOpen.current = false;
|
|
204
|
+
renderViewer();
|
|
205
|
+
await waitFor(() => expect(mockGetMatchingOverview).toHaveBeenCalledWith(hit));
|
|
206
|
+
expect(mockEmit).not.toHaveBeenCalled();
|
|
207
|
+
mockParams.id = undefined;
|
|
208
|
+
renderViewer();
|
|
209
|
+
expect(mockEmit).not.toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
it('opens an overview by default and lets the analyst select all content tabs', async () => {
|
|
212
|
+
const user = userEvent.setup();
|
|
213
|
+
mockGetMatchingOverview.mockResolvedValue({ content: 'overview' });
|
|
214
|
+
mockGetMatchingDossiers.mockResolvedValue([
|
|
215
|
+
{
|
|
216
|
+
leads: [
|
|
217
|
+
{
|
|
218
|
+
label: { en: 'External lead', fr: 'Piste externe' },
|
|
219
|
+
format: 'markdown',
|
|
220
|
+
content: 'external'
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
]);
|
|
225
|
+
renderViewer();
|
|
226
|
+
await screen.findByText('overview-content');
|
|
227
|
+
await user.click(screen.getByRole('tab', { name: 'hit.viewer.details' }));
|
|
228
|
+
expect(screen.getByText('details-content')).toBeInTheDocument();
|
|
229
|
+
await user.click(screen.getByRole('tab', { name: 'Local lead' }));
|
|
230
|
+
expect(screen.getByText('lead-content:Local lead')).toBeInTheDocument();
|
|
231
|
+
await user.click(screen.getByRole('tab', { name: 'External lead' }));
|
|
232
|
+
expect(screen.getByText('lead-content:External lead')).toBeInTheDocument();
|
|
233
|
+
const tabs = screen.getAllByRole('tab');
|
|
234
|
+
await user.click(tabs.at(-5));
|
|
235
|
+
expect(screen.getByTestId('json-content')).toHaveTextContent(JSON.stringify(['parsed:{"source":"data"}']));
|
|
236
|
+
await user.click(tabs.at(-4));
|
|
237
|
+
expect(screen.getByTestId('json-content')).toHaveTextContent(JSON.stringify(hit));
|
|
238
|
+
await user.click(tabs.at(-3));
|
|
239
|
+
expect(screen.getByText('comments-content')).toBeInTheDocument();
|
|
240
|
+
await user.click(tabs.at(-2));
|
|
241
|
+
expect(screen.getByText('worklog-content')).toBeInTheDocument();
|
|
242
|
+
await user.click(tabs.at(-1));
|
|
243
|
+
expect(screen.getByText('related-content')).toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
it('toggles the desktop layout and navigates to its matching analytic', async () => {
|
|
246
|
+
const user = userEvent.setup();
|
|
247
|
+
mockGetMatchingAnalytic.mockResolvedValue({ analytic_id: 'analytic-1' });
|
|
248
|
+
renderViewer();
|
|
249
|
+
await screen.findByText('links:analytic-1:0');
|
|
250
|
+
const buttons = screen.getAllByRole('button');
|
|
251
|
+
await user.click(buttons[0]);
|
|
252
|
+
expect(mockSetOrientation).toHaveBeenCalledWith(Orientation.HORIZONTAL);
|
|
253
|
+
await user.click(buttons[1]);
|
|
254
|
+
expect(mockNavigate).toHaveBeenCalledWith('/analytics/analytic-1');
|
|
255
|
+
});
|
|
256
|
+
it('forces a horizontal layout below the large breakpoint', async () => {
|
|
257
|
+
mockUseMediaQuery.mockReturnValue(true);
|
|
258
|
+
renderViewer();
|
|
259
|
+
await waitFor(() => expect(mockSetOrientation).toHaveBeenCalledWith(Orientation.HORIZONTAL));
|
|
260
|
+
});
|
|
261
|
+
});
|