@semiont/react-ui 0.2.35 → 0.2.37
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/dist/index.d.mts +8 -0
- package/dist/index.mjs +252 -166
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/CodeMirrorRenderer.tsx +71 -203
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +142 -0
- package/src/components/__tests__/LiveRegion.hooks.test.tsx +79 -0
- package/src/components/__tests__/ResizeHandle.test.tsx +165 -0
- package/src/components/__tests__/SessionExpiryBanner.test.tsx +123 -0
- package/src/components/__tests__/StatusDisplay.test.tsx +160 -0
- package/src/components/__tests__/Toolbar.test.tsx +110 -0
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +285 -0
- package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +273 -0
- package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +90 -0
- package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +129 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +180 -0
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +90 -0
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +169 -0
- package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +371 -0
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +2 -0
- package/src/components/resource/AnnotateView.tsx +27 -153
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +349 -0
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +492 -0
- package/src/components/resource/__tests__/event-formatting.test.ts +273 -0
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +226 -0
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +188 -0
- package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +69 -0
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +445 -0
- package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +271 -0
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +210 -0
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +190 -0
- package/src/components/viewers/__tests__/ImageViewer.test.tsx +63 -0
- package/src/integrations/__tests__/css-modules-helper.test.tsx +225 -0
- package/src/integrations/__tests__/styled-components-theme.test.ts +179 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import { renderWithProviders } from '../../../test-utils';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import { ProposeEntitiesModal } from '../ProposeEntitiesModal';
|
|
7
|
+
|
|
8
|
+
// Mock HeadlessUI to avoid jsdom OOM issues
|
|
9
|
+
vi.mock('@headlessui/react', () => ({
|
|
10
|
+
Dialog: ({ children, onClose, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
|
|
11
|
+
DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
12
|
+
DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
|
|
13
|
+
DialogDescription: ({ children, ...props }: any) => <p {...props}>{children}</p>,
|
|
14
|
+
Transition: ({ show, children }: any) => show ? <>{children}</> : null,
|
|
15
|
+
TransitionChild: ({ children }: any) => <>{children}</>,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Stable entity types array to avoid infinite re-render loops
|
|
19
|
+
const mockEntityTypes = ['Person', 'Organization', 'Location'];
|
|
20
|
+
const stableQueryResult = { data: { entityTypes: mockEntityTypes } };
|
|
21
|
+
|
|
22
|
+
vi.mock('../../../lib/api-hooks', () => ({
|
|
23
|
+
useEntityTypes: vi.fn(() => ({
|
|
24
|
+
list: {
|
|
25
|
+
useQuery: () => stableQueryResult,
|
|
26
|
+
},
|
|
27
|
+
})),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe('ProposeEntitiesModal', () => {
|
|
31
|
+
const defaultProps = {
|
|
32
|
+
isOpen: true,
|
|
33
|
+
onConfirm: vi.fn(),
|
|
34
|
+
onCancel: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
sessionStorage.clear();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders modal title when open', () => {
|
|
43
|
+
renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
|
|
44
|
+
expect(screen.getByText('Detect Entity References')).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does not render when closed', () => {
|
|
48
|
+
renderWithProviders(
|
|
49
|
+
<ProposeEntitiesModal {...defaultProps} isOpen={false} />
|
|
50
|
+
);
|
|
51
|
+
expect(screen.queryByText('Detect Entity References')).not.toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders available entity type buttons', () => {
|
|
55
|
+
renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
|
|
56
|
+
expect(screen.getByText('Person')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText('Organization')).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText('Location')).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('toggles entity type selection on click', () => {
|
|
62
|
+
renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
|
|
63
|
+
|
|
64
|
+
fireEvent.click(screen.getByText('Person'));
|
|
65
|
+
expect(screen.getByText('1 type selected')).toBeInTheDocument();
|
|
66
|
+
|
|
67
|
+
fireEvent.click(screen.getByText('Organization'));
|
|
68
|
+
expect(screen.getByText('2 types selected')).toBeInTheDocument();
|
|
69
|
+
|
|
70
|
+
// Deselect
|
|
71
|
+
fireEvent.click(screen.getByText('Person'));
|
|
72
|
+
expect(screen.getByText('1 type selected')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('disables confirm button when no types selected', () => {
|
|
76
|
+
renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
|
|
77
|
+
const buttons = screen.getAllByRole('button');
|
|
78
|
+
const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
|
|
79
|
+
expect(confirmButton).toBeDisabled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('enables confirm button when types are selected', () => {
|
|
83
|
+
renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
|
|
84
|
+
fireEvent.click(screen.getByText('Person'));
|
|
85
|
+
|
|
86
|
+
const buttons = screen.getAllByRole('button');
|
|
87
|
+
const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
|
|
88
|
+
expect(confirmButton).not.toBeDisabled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('calls onConfirm with selected types', () => {
|
|
92
|
+
const onConfirm = vi.fn();
|
|
93
|
+
renderWithProviders(
|
|
94
|
+
<ProposeEntitiesModal {...defaultProps} onConfirm={onConfirm} />
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
fireEvent.click(screen.getByText('Person'));
|
|
98
|
+
fireEvent.click(screen.getByText('Location'));
|
|
99
|
+
|
|
100
|
+
const buttons = screen.getAllByRole('button');
|
|
101
|
+
const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
|
|
102
|
+
fireEvent.click(confirmButton!);
|
|
103
|
+
|
|
104
|
+
expect(onConfirm).toHaveBeenCalledWith(['Person', 'Location']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('calls onCancel when cancel button is clicked', () => {
|
|
108
|
+
const onCancel = vi.fn();
|
|
109
|
+
renderWithProviders(
|
|
110
|
+
<ProposeEntitiesModal {...defaultProps} onCancel={onCancel} />
|
|
111
|
+
);
|
|
112
|
+
fireEvent.click(screen.getByText('Cancel'));
|
|
113
|
+
expect(onCancel).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('saves preferences to sessionStorage on confirm', () => {
|
|
117
|
+
renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
|
|
118
|
+
|
|
119
|
+
fireEvent.click(screen.getByText('Person'));
|
|
120
|
+
|
|
121
|
+
const buttons = screen.getAllByRole('button');
|
|
122
|
+
const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
|
|
123
|
+
fireEvent.click(confirmButton!);
|
|
124
|
+
|
|
125
|
+
expect(sessionStorage.getItem('userPreferredEntityTypes')).toBe(
|
|
126
|
+
JSON.stringify(['Person'])
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import { renderWithProviders } from '../../../test-utils';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
import { ResourceSearchModal } from '../ResourceSearchModal';
|
|
7
|
+
|
|
8
|
+
// Mock HeadlessUI to avoid jsdom OOM issues
|
|
9
|
+
vi.mock('@headlessui/react', () => ({
|
|
10
|
+
Dialog: ({ children, onClose, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
|
|
11
|
+
DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
12
|
+
DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
|
|
13
|
+
Transition: ({ show, children }: any) => show ? <>{children}</> : null,
|
|
14
|
+
TransitionChild: ({ children }: any) => <>{children}</>,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock api-hooks
|
|
18
|
+
const mockUseQuery = vi.fn(() => ({
|
|
19
|
+
data: null,
|
|
20
|
+
isFetching: false,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('../../../lib/api-hooks', () => ({
|
|
24
|
+
useResources: vi.fn(() => ({
|
|
25
|
+
search: {
|
|
26
|
+
useQuery: mockUseQuery,
|
|
27
|
+
},
|
|
28
|
+
})),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// Mock search announcements
|
|
32
|
+
vi.mock('../../../hooks/useSearchAnnouncements', () => ({
|
|
33
|
+
useSearchAnnouncements: vi.fn(() => ({
|
|
34
|
+
announceSearchResults: vi.fn(),
|
|
35
|
+
announceSearching: vi.fn(),
|
|
36
|
+
announceNavigation: vi.fn(),
|
|
37
|
+
})),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
describe('ResourceSearchModal', () => {
|
|
41
|
+
const defaultProps = {
|
|
42
|
+
isOpen: true,
|
|
43
|
+
onClose: vi.fn(),
|
|
44
|
+
onSelect: vi.fn(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
mockUseQuery.mockReturnValue({ data: null, isFetching: false });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders modal with title when open', () => {
|
|
53
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} />);
|
|
54
|
+
expect(screen.getByText('Search Resources')).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not render when closed', () => {
|
|
58
|
+
renderWithProviders(
|
|
59
|
+
<ResourceSearchModal {...defaultProps} isOpen={false} />
|
|
60
|
+
);
|
|
61
|
+
expect(screen.queryByText('Search Resources')).not.toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders search input with placeholder', () => {
|
|
65
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} />);
|
|
66
|
+
expect(screen.getByPlaceholderText('Search for resources...')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('uses custom translations', () => {
|
|
70
|
+
renderWithProviders(
|
|
71
|
+
<ResourceSearchModal
|
|
72
|
+
{...defaultProps}
|
|
73
|
+
translations={{ title: 'Find Docs', placeholder: 'Type here...' }}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
expect(screen.getByText('Find Docs')).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByPlaceholderText('Type here...')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('shows loading state when fetching', () => {
|
|
81
|
+
mockUseQuery.mockReturnValue({ data: null, isFetching: true });
|
|
82
|
+
renderWithProviders(<ResourceSearchModal {...defaultProps} />);
|
|
83
|
+
expect(screen.getByText('Searching...')).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('shows no results message when search has no matches', () => {
|
|
87
|
+
mockUseQuery.mockReturnValue({
|
|
88
|
+
data: { resources: [] },
|
|
89
|
+
isFetching: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
renderWithProviders(
|
|
93
|
+
<ResourceSearchModal {...defaultProps} searchTerm="xyz" />
|
|
94
|
+
);
|
|
95
|
+
expect(screen.getByText('No documents found')).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('renders search results', () => {
|
|
99
|
+
mockUseQuery.mockReturnValue({
|
|
100
|
+
data: {
|
|
101
|
+
resources: [
|
|
102
|
+
{
|
|
103
|
+
'@id': 'res-1',
|
|
104
|
+
name: 'Test Document',
|
|
105
|
+
content: 'Some content here',
|
|
106
|
+
representations: [{ mediaType: 'text/plain' }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
isFetching: false,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
renderWithProviders(
|
|
114
|
+
<ResourceSearchModal {...defaultProps} searchTerm="test" />
|
|
115
|
+
);
|
|
116
|
+
expect(screen.getByText('Test Document')).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('calls onSelect and onClose when a result is clicked', () => {
|
|
120
|
+
const onSelect = vi.fn();
|
|
121
|
+
const onClose = vi.fn();
|
|
122
|
+
|
|
123
|
+
mockUseQuery.mockReturnValue({
|
|
124
|
+
data: {
|
|
125
|
+
resources: [
|
|
126
|
+
{
|
|
127
|
+
'@id': 'res-1',
|
|
128
|
+
name: 'Test Document',
|
|
129
|
+
content: 'Some content',
|
|
130
|
+
representations: [{ mediaType: 'text/plain' }],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
isFetching: false,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
renderWithProviders(
|
|
138
|
+
<ResourceSearchModal
|
|
139
|
+
{...defaultProps}
|
|
140
|
+
searchTerm="test"
|
|
141
|
+
onSelect={onSelect}
|
|
142
|
+
onClose={onClose}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
fireEvent.click(screen.getByText('Test Document'));
|
|
147
|
+
expect(onSelect).toHaveBeenCalledWith('res-1');
|
|
148
|
+
expect(onClose).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('shows media type for image results', () => {
|
|
152
|
+
mockUseQuery.mockReturnValue({
|
|
153
|
+
data: {
|
|
154
|
+
resources: [
|
|
155
|
+
{
|
|
156
|
+
'@id': 'res-img',
|
|
157
|
+
name: 'Photo',
|
|
158
|
+
content: 'image data',
|
|
159
|
+
representations: [{ mediaType: 'image/png' }],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
isFetching: false,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
renderWithProviders(
|
|
167
|
+
<ResourceSearchModal {...defaultProps} searchTerm="photo" />
|
|
168
|
+
);
|
|
169
|
+
expect(screen.getByText('image/png')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('calls onClose when close button is clicked', () => {
|
|
173
|
+
const onClose = vi.fn();
|
|
174
|
+
renderWithProviders(
|
|
175
|
+
<ResourceSearchModal {...defaultProps} onClose={onClose} />
|
|
176
|
+
);
|
|
177
|
+
fireEvent.click(screen.getByLabelText('✕'));
|
|
178
|
+
expect(onClose).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { renderWithProviders, resetEventBusForTesting } from '../../../test-utils';
|
|
6
|
+
import { ObservableLink } from '../ObservableLink';
|
|
7
|
+
|
|
8
|
+
describe('ObservableLink', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
resetEventBusForTesting();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('renders anchor with href', () => {
|
|
14
|
+
renderWithProviders(
|
|
15
|
+
<ObservableLink href="/discover">Discover</ObservableLink>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const link = screen.getByRole('link', { name: 'Discover' });
|
|
19
|
+
expect(link).toHaveAttribute('href', '/discover');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders children', () => {
|
|
23
|
+
renderWithProviders(
|
|
24
|
+
<ObservableLink href="/test">
|
|
25
|
+
<span data-testid="child">Click me</span>
|
|
26
|
+
</ObservableLink>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText('Click me')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('emits browse:link-clicked with href and label on click', () => {
|
|
34
|
+
const handler = vi.fn();
|
|
35
|
+
|
|
36
|
+
const { eventBus } = renderWithProviders(
|
|
37
|
+
<ObservableLink href="/discover" label="Discover">
|
|
38
|
+
Discover Resources
|
|
39
|
+
</ObservableLink>,
|
|
40
|
+
{ returnEventBus: true }
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const subscription = eventBus!.get('browse:link-clicked').subscribe(handler);
|
|
44
|
+
|
|
45
|
+
const link = screen.getByRole('link');
|
|
46
|
+
fireEvent.click(link);
|
|
47
|
+
|
|
48
|
+
expect(handler).toHaveBeenCalledWith({
|
|
49
|
+
href: '/discover',
|
|
50
|
+
label: 'Discover',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
subscription.unsubscribe();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('calls original onClick handler if provided', () => {
|
|
57
|
+
const onClick = vi.fn();
|
|
58
|
+
|
|
59
|
+
renderWithProviders(
|
|
60
|
+
<ObservableLink href="/test" onClick={onClick}>
|
|
61
|
+
Click
|
|
62
|
+
</ObservableLink>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const link = screen.getByRole('link');
|
|
66
|
+
fireEvent.click(link);
|
|
67
|
+
|
|
68
|
+
expect(onClick).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('passes through additional anchor props', () => {
|
|
72
|
+
renderWithProviders(
|
|
73
|
+
<ObservableLink
|
|
74
|
+
href="/external"
|
|
75
|
+
className="custom-link"
|
|
76
|
+
target="_blank"
|
|
77
|
+
rel="noopener noreferrer"
|
|
78
|
+
data-testid="my-link"
|
|
79
|
+
>
|
|
80
|
+
External
|
|
81
|
+
</ObservableLink>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const link = screen.getByTestId('my-link');
|
|
85
|
+
expect(link).toHaveClass('custom-link');
|
|
86
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
87
|
+
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
|
88
|
+
expect(link).toHaveAttribute('href', '/external');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { renderWithProviders, resetEventBusForTesting } from '../../../test-utils';
|
|
6
|
+
import { SimpleNavigation } from '../SimpleNavigation';
|
|
7
|
+
import type { SimpleNavigationItem } from '../SimpleNavigation';
|
|
8
|
+
|
|
9
|
+
const MockChevronLeft = (props: any) => <span data-testid="chevron" {...props} />;
|
|
10
|
+
const MockBars = (props: any) => <span data-testid="bars" {...props} />;
|
|
11
|
+
|
|
12
|
+
const MockLink = ({ href, children, ...props }: any) => (
|
|
13
|
+
<a href={href} {...props}>{children}</a>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const MockIcon1 = (props: any) => <span data-testid="icon-1" {...props} />;
|
|
17
|
+
const MockIcon2 = (props: any) => <span data-testid="icon-2" {...props} />;
|
|
18
|
+
|
|
19
|
+
const defaultItems: SimpleNavigationItem[] = [
|
|
20
|
+
{ name: 'Dashboard', href: '/admin/dashboard', icon: MockIcon1 },
|
|
21
|
+
{ name: 'Users', href: '/admin/users', icon: MockIcon2, description: 'Manage users' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const defaultProps = {
|
|
25
|
+
title: 'Administration',
|
|
26
|
+
items: defaultItems,
|
|
27
|
+
currentPath: '/admin/dashboard',
|
|
28
|
+
LinkComponent: MockLink,
|
|
29
|
+
isCollapsed: false,
|
|
30
|
+
icons: {
|
|
31
|
+
chevronLeft: MockChevronLeft,
|
|
32
|
+
bars: MockBars,
|
|
33
|
+
},
|
|
34
|
+
collapseSidebarLabel: 'Collapse sidebar',
|
|
35
|
+
expandSidebarLabel: 'Expand sidebar',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe('SimpleNavigation', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
resetEventBusForTesting();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('title visibility', () => {
|
|
44
|
+
it('renders title when not collapsed', () => {
|
|
45
|
+
renderWithProviders(<SimpleNavigation {...defaultProps} />);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText('Administration')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('hides title when collapsed', () => {
|
|
51
|
+
renderWithProviders(
|
|
52
|
+
<SimpleNavigation {...defaultProps} isCollapsed={true} />
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(screen.queryByText('Administration')).not.toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('navigation items', () => {
|
|
60
|
+
it('renders navigation items', () => {
|
|
61
|
+
renderWithProviders(<SimpleNavigation {...defaultProps} />);
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByText('Users')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('marks active item with aria-current="page"', () => {
|
|
68
|
+
renderWithProviders(<SimpleNavigation {...defaultProps} />);
|
|
69
|
+
|
|
70
|
+
const dashboardLink = screen.getByText('Dashboard').closest('a');
|
|
71
|
+
expect(dashboardLink).toHaveAttribute('aria-current', 'page');
|
|
72
|
+
|
|
73
|
+
const usersLink = screen.getByText('Users').closest('a');
|
|
74
|
+
expect(usersLink).not.toHaveAttribute('aria-current');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('shows item text when not collapsed', () => {
|
|
78
|
+
renderWithProviders(<SimpleNavigation {...defaultProps} />);
|
|
79
|
+
|
|
80
|
+
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
|
81
|
+
expect(screen.getByText('Users')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('shows only icons when collapsed', () => {
|
|
85
|
+
renderWithProviders(
|
|
86
|
+
<SimpleNavigation {...defaultProps} isCollapsed={true} />
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Text spans should not be present in collapsed mode
|
|
90
|
+
expect(screen.queryByText('Dashboard')).not.toBeInTheDocument();
|
|
91
|
+
expect(screen.queryByText('Users')).not.toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
// Icons should still be rendered
|
|
94
|
+
const icons = screen.getAllByTestId(/^icon-/);
|
|
95
|
+
expect(icons.length).toBe(2);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('sidebar toggle', () => {
|
|
100
|
+
it('emits browse:sidebar-toggle on collapse button click', () => {
|
|
101
|
+
const handler = vi.fn();
|
|
102
|
+
|
|
103
|
+
const { eventBus } = renderWithProviders(
|
|
104
|
+
<SimpleNavigation {...defaultProps} />,
|
|
105
|
+
{ returnEventBus: true }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const subscription = eventBus!.get('browse:sidebar-toggle').subscribe(handler);
|
|
109
|
+
|
|
110
|
+
const collapseButton = screen.getByLabelText('Collapse sidebar');
|
|
111
|
+
fireEvent.click(collapseButton);
|
|
112
|
+
|
|
113
|
+
expect(handler).toHaveBeenCalledWith(undefined);
|
|
114
|
+
|
|
115
|
+
subscription.unsubscribe();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('shows expand label when collapsed', () => {
|
|
119
|
+
renderWithProviders(
|
|
120
|
+
<SimpleNavigation {...defaultProps} isCollapsed={true} />
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByLabelText('Expand sidebar')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('dropdown', () => {
|
|
128
|
+
it('opens dropdown when header button clicked if dropdownContent provided', () => {
|
|
129
|
+
const dropdownContent = (onClose: () => void) => (
|
|
130
|
+
<div data-testid="dropdown-content">
|
|
131
|
+
<button onClick={onClose}>Close</button>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
renderWithProviders(
|
|
136
|
+
<SimpleNavigation {...defaultProps} dropdownContent={dropdownContent} />
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Dropdown should not be visible initially
|
|
140
|
+
expect(screen.queryByTestId('dropdown-content')).not.toBeInTheDocument();
|
|
141
|
+
|
|
142
|
+
// Click header button to open dropdown
|
|
143
|
+
const headerButton = screen.getByRole('button', { name: /Administration/i });
|
|
144
|
+
fireEvent.click(headerButton);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByTestId('dropdown-content')).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('closes dropdown on outside click', () => {
|
|
150
|
+
const dropdownContent = (onClose: () => void) => (
|
|
151
|
+
<div data-testid="dropdown-content">Dropdown</div>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
renderWithProviders(
|
|
155
|
+
<SimpleNavigation {...defaultProps} dropdownContent={dropdownContent} />
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Open dropdown
|
|
159
|
+
const headerButton = screen.getByRole('button', { name: /Administration/i });
|
|
160
|
+
fireEvent.click(headerButton);
|
|
161
|
+
expect(screen.getByTestId('dropdown-content')).toBeInTheDocument();
|
|
162
|
+
|
|
163
|
+
// Click outside
|
|
164
|
+
fireEvent.mouseDown(document.body);
|
|
165
|
+
|
|
166
|
+
expect(screen.queryByTestId('dropdown-content')).not.toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|