@semiont/react-ui 0.2.36 → 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 +1 -1
- 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/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,165 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { ResizeHandle } from '../ResizeHandle';
|
|
6
|
+
|
|
7
|
+
describe('ResizeHandle', () => {
|
|
8
|
+
const defaultProps = {
|
|
9
|
+
onResize: vi.fn(),
|
|
10
|
+
minWidth: 200,
|
|
11
|
+
maxWidth: 800,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('rendering', () => {
|
|
15
|
+
it('renders with role="separator" and aria-orientation="vertical"', () => {
|
|
16
|
+
render(<ResizeHandle {...defaultProps} />);
|
|
17
|
+
|
|
18
|
+
const handle = screen.getByRole('separator');
|
|
19
|
+
expect(handle).toBeInTheDocument();
|
|
20
|
+
expect(handle).toHaveAttribute('aria-orientation', 'vertical');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('uses custom ariaLabel', () => {
|
|
24
|
+
render(<ResizeHandle {...defaultProps} ariaLabel="Resize sidebar" />);
|
|
25
|
+
|
|
26
|
+
const handle = screen.getByRole('separator');
|
|
27
|
+
expect(handle).toHaveAttribute('aria-label', 'Resize sidebar');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses default ariaLabel when not specified', () => {
|
|
31
|
+
render(<ResizeHandle {...defaultProps} />);
|
|
32
|
+
|
|
33
|
+
const handle = screen.getByRole('separator');
|
|
34
|
+
expect(handle).toHaveAttribute('aria-label', 'Resize panel');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('has tabIndex 0 for keyboard focus', () => {
|
|
38
|
+
render(<ResizeHandle {...defaultProps} />);
|
|
39
|
+
|
|
40
|
+
const handle = screen.getByRole('separator');
|
|
41
|
+
expect(handle).toHaveAttribute('tabindex', '0');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('keyboard resize with left position', () => {
|
|
46
|
+
it('calls onResize on ArrowLeft (wider) for left position', () => {
|
|
47
|
+
const onResize = vi.fn();
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<div style={{ width: '400px' }}>
|
|
50
|
+
<ResizeHandle onResize={onResize} minWidth={200} maxWidth={800} position="left" />
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Mock offsetWidth on the parent div
|
|
55
|
+
Object.defineProperty(container.firstChild, 'offsetWidth', { value: 400 });
|
|
56
|
+
|
|
57
|
+
const handle = screen.getByRole('separator');
|
|
58
|
+
fireEvent.keyDown(handle, { key: 'ArrowLeft' });
|
|
59
|
+
|
|
60
|
+
expect(onResize).toHaveBeenCalledWith(410); // 400 + 10
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('calls onResize on ArrowRight (narrower) for left position', () => {
|
|
64
|
+
const onResize = vi.fn();
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<div style={{ width: '400px' }}>
|
|
67
|
+
<ResizeHandle onResize={onResize} minWidth={200} maxWidth={800} position="left" />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
Object.defineProperty(container.firstChild, 'offsetWidth', { value: 400 });
|
|
72
|
+
|
|
73
|
+
const handle = screen.getByRole('separator');
|
|
74
|
+
fireEvent.keyDown(handle, { key: 'ArrowRight' });
|
|
75
|
+
|
|
76
|
+
expect(onResize).toHaveBeenCalledWith(390); // 400 - 10
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('keyboard resize with right position', () => {
|
|
81
|
+
it('calls onResize on ArrowRight (wider) for right position', () => {
|
|
82
|
+
const onResize = vi.fn();
|
|
83
|
+
const { container } = render(
|
|
84
|
+
<div style={{ width: '400px' }}>
|
|
85
|
+
<ResizeHandle onResize={onResize} minWidth={200} maxWidth={800} position="right" />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
Object.defineProperty(container.firstChild, 'offsetWidth', { value: 400 });
|
|
90
|
+
|
|
91
|
+
const handle = screen.getByRole('separator');
|
|
92
|
+
fireEvent.keyDown(handle, { key: 'ArrowRight' });
|
|
93
|
+
|
|
94
|
+
expect(onResize).toHaveBeenCalledWith(410); // 400 + 10
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('calls onResize on ArrowLeft (narrower) for right position', () => {
|
|
98
|
+
const onResize = vi.fn();
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<div style={{ width: '400px' }}>
|
|
101
|
+
<ResizeHandle onResize={onResize} minWidth={200} maxWidth={800} position="right" />
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
Object.defineProperty(container.firstChild, 'offsetWidth', { value: 400 });
|
|
106
|
+
|
|
107
|
+
const handle = screen.getByRole('separator');
|
|
108
|
+
fireEvent.keyDown(handle, { key: 'ArrowLeft' });
|
|
109
|
+
|
|
110
|
+
expect(onResize).toHaveBeenCalledWith(390); // 400 - 10
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('constraints', () => {
|
|
115
|
+
it('respects minWidth constraint on keyboard resize', () => {
|
|
116
|
+
const onResize = vi.fn();
|
|
117
|
+
const { container } = render(
|
|
118
|
+
<div style={{ width: '210px' }}>
|
|
119
|
+
<ResizeHandle onResize={onResize} minWidth={200} maxWidth={800} position="left" />
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
Object.defineProperty(container.firstChild, 'offsetWidth', { value: 210 });
|
|
124
|
+
|
|
125
|
+
const handle = screen.getByRole('separator');
|
|
126
|
+
fireEvent.keyDown(handle, { key: 'ArrowRight' }); // narrower for left position
|
|
127
|
+
|
|
128
|
+
expect(onResize).toHaveBeenCalledWith(200); // clamped to minWidth
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('respects maxWidth constraint on keyboard resize', () => {
|
|
132
|
+
const onResize = vi.fn();
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<div style={{ width: '795px' }}>
|
|
135
|
+
<ResizeHandle onResize={onResize} minWidth={200} maxWidth={800} position="left" />
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
Object.defineProperty(container.firstChild, 'offsetWidth', { value: 795 });
|
|
140
|
+
|
|
141
|
+
const handle = screen.getByRole('separator');
|
|
142
|
+
fireEvent.keyDown(handle, { key: 'ArrowLeft' }); // wider for left position
|
|
143
|
+
|
|
144
|
+
expect(onResize).toHaveBeenCalledWith(800); // clamped to maxWidth
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('shift key step', () => {
|
|
149
|
+
it('Shift+Arrow uses 50px step instead of 10px', () => {
|
|
150
|
+
const onResize = vi.fn();
|
|
151
|
+
const { container } = render(
|
|
152
|
+
<div style={{ width: '400px' }}>
|
|
153
|
+
<ResizeHandle onResize={onResize} minWidth={200} maxWidth={800} position="left" />
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
Object.defineProperty(container.firstChild, 'offsetWidth', { value: 400 });
|
|
158
|
+
|
|
159
|
+
const handle = screen.getByRole('separator');
|
|
160
|
+
fireEvent.keyDown(handle, { key: 'ArrowLeft', shiftKey: true });
|
|
161
|
+
|
|
162
|
+
expect(onResize).toHaveBeenCalledWith(450); // 400 + 50
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
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 } from '../../test-utils';
|
|
6
|
+
|
|
7
|
+
// Mock the hooks and utilities that SessionExpiryBanner imports from @semiont/react-ui
|
|
8
|
+
vi.mock('@semiont/react-ui', async () => {
|
|
9
|
+
const actual = await vi.importActual('@semiont/react-ui');
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
useSessionExpiry: vi.fn(),
|
|
13
|
+
formatTime: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
import { useSessionExpiry, formatTime } from '@semiont/react-ui';
|
|
18
|
+
import type { MockedFunction } from 'vitest';
|
|
19
|
+
import { SessionExpiryBanner } from '../SessionExpiryBanner';
|
|
20
|
+
|
|
21
|
+
const mockUseSessionExpiry = useSessionExpiry as MockedFunction<typeof useSessionExpiry>;
|
|
22
|
+
const mockFormatTime = formatTime as MockedFunction<typeof formatTime>;
|
|
23
|
+
|
|
24
|
+
describe('SessionExpiryBanner', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return null when session is not expiring soon', () => {
|
|
30
|
+
mockUseSessionExpiry.mockReturnValue({
|
|
31
|
+
timeRemaining: 600000,
|
|
32
|
+
isExpiringSoon: false,
|
|
33
|
+
});
|
|
34
|
+
mockFormatTime.mockReturnValue('10m');
|
|
35
|
+
|
|
36
|
+
const { container } = renderWithProviders(<SessionExpiryBanner />);
|
|
37
|
+
|
|
38
|
+
expect(container.firstChild).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return null when formatTime returns null', () => {
|
|
42
|
+
mockUseSessionExpiry.mockReturnValue({
|
|
43
|
+
timeRemaining: 0,
|
|
44
|
+
isExpiringSoon: true,
|
|
45
|
+
});
|
|
46
|
+
mockFormatTime.mockReturnValue(null);
|
|
47
|
+
|
|
48
|
+
const { container } = renderWithProviders(<SessionExpiryBanner />);
|
|
49
|
+
|
|
50
|
+
expect(container.firstChild).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should render banner with time remaining when expiring soon', () => {
|
|
54
|
+
mockUseSessionExpiry.mockReturnValue({
|
|
55
|
+
timeRemaining: 180000,
|
|
56
|
+
isExpiringSoon: true,
|
|
57
|
+
});
|
|
58
|
+
mockFormatTime.mockReturnValue('3m');
|
|
59
|
+
|
|
60
|
+
renderWithProviders(<SessionExpiryBanner />);
|
|
61
|
+
|
|
62
|
+
expect(screen.getByText(/Session expiring soon/)).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText(/3m remaining/)).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should hide banner when dismiss button is clicked', () => {
|
|
67
|
+
mockUseSessionExpiry.mockReturnValue({
|
|
68
|
+
timeRemaining: 180000,
|
|
69
|
+
isExpiringSoon: true,
|
|
70
|
+
});
|
|
71
|
+
mockFormatTime.mockReturnValue('3m');
|
|
72
|
+
|
|
73
|
+
renderWithProviders(<SessionExpiryBanner />);
|
|
74
|
+
|
|
75
|
+
// Banner is visible
|
|
76
|
+
expect(screen.getByText(/Session expiring soon/)).toBeInTheDocument();
|
|
77
|
+
|
|
78
|
+
// Click dismiss
|
|
79
|
+
const dismissButton = screen.getByLabelText('Dismiss warning');
|
|
80
|
+
fireEvent.click(dismissButton);
|
|
81
|
+
|
|
82
|
+
// Banner should be gone
|
|
83
|
+
expect(screen.queryByText(/Session expiring soon/)).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should have role="alert"', () => {
|
|
87
|
+
mockUseSessionExpiry.mockReturnValue({
|
|
88
|
+
timeRemaining: 180000,
|
|
89
|
+
isExpiringSoon: true,
|
|
90
|
+
});
|
|
91
|
+
mockFormatTime.mockReturnValue('3m');
|
|
92
|
+
|
|
93
|
+
renderWithProviders(<SessionExpiryBanner />);
|
|
94
|
+
|
|
95
|
+
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should have aria-live="polite"', () => {
|
|
99
|
+
mockUseSessionExpiry.mockReturnValue({
|
|
100
|
+
timeRemaining: 180000,
|
|
101
|
+
isExpiringSoon: true,
|
|
102
|
+
});
|
|
103
|
+
mockFormatTime.mockReturnValue('3m');
|
|
104
|
+
|
|
105
|
+
renderWithProviders(<SessionExpiryBanner />);
|
|
106
|
+
|
|
107
|
+
const banner = screen.getByRole('alert');
|
|
108
|
+
expect(banner).toHaveAttribute('aria-live', 'polite');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should have data-visible attribute when shown', () => {
|
|
112
|
+
mockUseSessionExpiry.mockReturnValue({
|
|
113
|
+
timeRemaining: 60000,
|
|
114
|
+
isExpiringSoon: true,
|
|
115
|
+
});
|
|
116
|
+
mockFormatTime.mockReturnValue('1m');
|
|
117
|
+
|
|
118
|
+
renderWithProviders(<SessionExpiryBanner />);
|
|
119
|
+
|
|
120
|
+
const banner = screen.getByRole('alert');
|
|
121
|
+
expect(banner).toHaveAttribute('data-visible', 'true');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { screen } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { renderWithProviders } from '../../test-utils';
|
|
6
|
+
|
|
7
|
+
// Mock api-hooks
|
|
8
|
+
vi.mock('../../lib/api-hooks', () => ({
|
|
9
|
+
useHealth: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { useHealth } from '../../lib/api-hooks';
|
|
13
|
+
import type { MockedFunction } from 'vitest';
|
|
14
|
+
import { StatusDisplay } from '../StatusDisplay';
|
|
15
|
+
|
|
16
|
+
const mockUseHealth = useHealth as MockedFunction<typeof useHealth>;
|
|
17
|
+
|
|
18
|
+
function createMockHealth(queryResult: { data?: unknown; isLoading?: boolean; error?: unknown }) {
|
|
19
|
+
return {
|
|
20
|
+
check: {
|
|
21
|
+
useQuery: vi.fn(),
|
|
22
|
+
},
|
|
23
|
+
status: {
|
|
24
|
+
useQuery: vi.fn().mockReturnValue({
|
|
25
|
+
data: queryResult.data ?? undefined,
|
|
26
|
+
isLoading: queryResult.isLoading ?? false,
|
|
27
|
+
error: queryResult.error ?? undefined,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('StatusDisplay', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should show "Authentication required" when not fully authenticated', () => {
|
|
39
|
+
mockUseHealth.mockReturnValue(createMockHealth({ data: undefined, isLoading: false }) as ReturnType<typeof useHealth>);
|
|
40
|
+
|
|
41
|
+
renderWithProviders(
|
|
42
|
+
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(screen.getByText(/Authentication required/)).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should show "Connecting..." when loading', () => {
|
|
49
|
+
mockUseHealth.mockReturnValue(createMockHealth({ isLoading: true }) as ReturnType<typeof useHealth>);
|
|
50
|
+
|
|
51
|
+
renderWithProviders(
|
|
52
|
+
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(screen.getByText(/Connecting\.\.\./)).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should show status when data is available', () => {
|
|
59
|
+
mockUseHealth.mockReturnValue(createMockHealth({
|
|
60
|
+
data: { status: 'healthy', version: '1.2.3' },
|
|
61
|
+
}) as ReturnType<typeof useHealth>);
|
|
62
|
+
|
|
63
|
+
renderWithProviders(
|
|
64
|
+
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(screen.getByText(/healthy/)).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText(/v1\.2\.3/)).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should show "Connection failed" on error', () => {
|
|
72
|
+
mockUseHealth.mockReturnValue(createMockHealth({
|
|
73
|
+
error: new Error('Network error'),
|
|
74
|
+
}) as ReturnType<typeof useHealth>);
|
|
75
|
+
|
|
76
|
+
renderWithProviders(
|
|
77
|
+
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(screen.getByText(/Connection failed/)).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should show re-login message for 401 errors', () => {
|
|
84
|
+
mockUseHealth.mockReturnValue(createMockHealth({
|
|
85
|
+
error: new Error('401 Unauthorized'),
|
|
86
|
+
}) as ReturnType<typeof useHealth>);
|
|
87
|
+
|
|
88
|
+
renderWithProviders(
|
|
89
|
+
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(screen.getByText(/sign out and sign in again/i)).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should show warning for authenticated users missing backend token', () => {
|
|
96
|
+
mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
|
|
97
|
+
|
|
98
|
+
renderWithProviders(
|
|
99
|
+
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={true} hasValidBackendToken={false} />
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(screen.getByText(/sign out and sign in again to reconnect/i)).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should have role="status"', () => {
|
|
106
|
+
mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
|
|
107
|
+
|
|
108
|
+
renderWithProviders(
|
|
109
|
+
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should have aria-live="polite"', () => {
|
|
116
|
+
mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
|
|
117
|
+
|
|
118
|
+
renderWithProviders(
|
|
119
|
+
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const status = screen.getByRole('status');
|
|
123
|
+
expect(status).toHaveAttribute('aria-live', 'polite');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should show sign-in hint when not authenticated', () => {
|
|
127
|
+
mockUseHealth.mockReturnValue(createMockHealth({ data: undefined }) as ReturnType<typeof useHealth>);
|
|
128
|
+
|
|
129
|
+
renderWithProviders(
|
|
130
|
+
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(screen.getByText('Sign in to view backend status')).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should show error hint when there is an error', () => {
|
|
137
|
+
mockUseHealth.mockReturnValue(createMockHealth({
|
|
138
|
+
error: new Error('Connection refused'),
|
|
139
|
+
}) as ReturnType<typeof useHealth>);
|
|
140
|
+
|
|
141
|
+
renderWithProviders(
|
|
142
|
+
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(screen.getByText(/Check that the backend server is running/)).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should set data-status attribute based on state', () => {
|
|
149
|
+
mockUseHealth.mockReturnValue(createMockHealth({
|
|
150
|
+
data: { status: 'healthy', version: '1.0.0' },
|
|
151
|
+
}) as ReturnType<typeof useHealth>);
|
|
152
|
+
|
|
153
|
+
renderWithProviders(
|
|
154
|
+
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const status = screen.getByRole('status');
|
|
158
|
+
expect(status).toHaveAttribute('data-status', 'success');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { Toolbar } from '../Toolbar';
|
|
7
|
+
|
|
8
|
+
describe('Toolbar', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
resetEventBusForTesting();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('document context', () => {
|
|
14
|
+
it('renders all document context buttons when not archived', () => {
|
|
15
|
+
renderWithProviders(
|
|
16
|
+
<Toolbar context="document" activePanel={null} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(screen.getByLabelText('Toolbar.annotations')).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByLabelText('Toolbar.resourceInfo')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByLabelText('Toolbar.history')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByLabelText('Toolbar.collaboration')).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByLabelText('JSON-LD')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByLabelText('Toolbar.userAccount')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByLabelText('Toolbar.settings')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('hides annotations button when archived', () => {
|
|
29
|
+
renderWithProviders(
|
|
30
|
+
<Toolbar context="document" activePanel={null} isArchived={true} />
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(screen.queryByLabelText('Toolbar.annotations')).not.toBeInTheDocument();
|
|
34
|
+
// Other document buttons should still be present
|
|
35
|
+
expect(screen.getByLabelText('Toolbar.resourceInfo')).toBeInTheDocument();
|
|
36
|
+
expect(screen.getByLabelText('Toolbar.history')).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('simple context', () => {
|
|
41
|
+
it('renders only user and settings in simple context', () => {
|
|
42
|
+
renderWithProviders(
|
|
43
|
+
<Toolbar context="simple" activePanel={null} />
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(screen.getByLabelText('Toolbar.userAccount')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByLabelText('Toolbar.settings')).toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
// Document-specific buttons should not be present
|
|
50
|
+
expect(screen.queryByLabelText('Toolbar.annotations')).not.toBeInTheDocument();
|
|
51
|
+
expect(screen.queryByLabelText('Toolbar.resourceInfo')).not.toBeInTheDocument();
|
|
52
|
+
expect(screen.queryByLabelText('Toolbar.history')).not.toBeInTheDocument();
|
|
53
|
+
expect(screen.queryByLabelText('Toolbar.collaboration')).not.toBeInTheDocument();
|
|
54
|
+
expect(screen.queryByLabelText('JSON-LD')).not.toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('active panel', () => {
|
|
59
|
+
it('marks active panel button as pressed', () => {
|
|
60
|
+
renderWithProviders(
|
|
61
|
+
<Toolbar context="document" activePanel="info" />
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const infoButton = screen.getByLabelText('Toolbar.resourceInfo');
|
|
65
|
+
expect(infoButton).toHaveAttribute('aria-pressed', 'true');
|
|
66
|
+
|
|
67
|
+
const historyButton = screen.getByLabelText('Toolbar.history');
|
|
68
|
+
expect(historyButton).toHaveAttribute('aria-pressed', 'false');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('event emission', () => {
|
|
73
|
+
it('emits browse:panel-toggle with panel name on click', () => {
|
|
74
|
+
const handler = vi.fn();
|
|
75
|
+
|
|
76
|
+
const { eventBus } = renderWithProviders(
|
|
77
|
+
<Toolbar context="document" activePanel={null} />,
|
|
78
|
+
{ returnEventBus: true }
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const subscription = eventBus!.get('browse:panel-toggle').subscribe(handler);
|
|
82
|
+
|
|
83
|
+
fireEvent.click(screen.getByLabelText('Toolbar.resourceInfo'));
|
|
84
|
+
expect(handler).toHaveBeenCalledWith({ panel: 'info' });
|
|
85
|
+
|
|
86
|
+
handler.mockClear();
|
|
87
|
+
fireEvent.click(screen.getByLabelText('Toolbar.annotations'));
|
|
88
|
+
expect(handler).toHaveBeenCalledWith({ panel: 'annotations' });
|
|
89
|
+
|
|
90
|
+
subscription.unsubscribe();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('accessibility', () => {
|
|
95
|
+
it('buttons have aria-label and aria-pressed', () => {
|
|
96
|
+
renderWithProviders(
|
|
97
|
+
<Toolbar context="document" activePanel="settings" />
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const buttons = screen.getAllByRole('button');
|
|
101
|
+
buttons.forEach((button) => {
|
|
102
|
+
expect(button).toHaveAttribute('aria-label');
|
|
103
|
+
expect(button).toHaveAttribute('aria-pressed');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const settingsButton = screen.getByLabelText('Toolbar.settings');
|
|
107
|
+
expect(settingsButton).toHaveAttribute('aria-pressed', 'true');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|