@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,349 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { screen } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { AnnotationHistory } from '../AnnotationHistory';
|
|
6
|
+
import { renderWithProviders } from '../../../test-utils';
|
|
7
|
+
import type { StoredEvent, ResourceUri } from '@semiont/core';
|
|
8
|
+
|
|
9
|
+
// Mock @semiont/core - must use importOriginal to preserve EventBus etc.
|
|
10
|
+
vi.mock('@semiont/core', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof import('@semiont/core')>();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
getAnnotationUriFromEvent: vi.fn(() => null),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Mock TranslationContext
|
|
19
|
+
vi.mock('../../../contexts/TranslationContext', () => ({
|
|
20
|
+
useTranslations: vi.fn(() => (key: string) => {
|
|
21
|
+
const translations: Record<string, string> = {
|
|
22
|
+
history: 'History',
|
|
23
|
+
loading: 'Loading...',
|
|
24
|
+
};
|
|
25
|
+
return translations[key] || key;
|
|
26
|
+
}),
|
|
27
|
+
TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Mock useResources from api-hooks
|
|
31
|
+
const mockEventsUseQuery = vi.fn();
|
|
32
|
+
const mockAnnotationsUseQuery = vi.fn();
|
|
33
|
+
|
|
34
|
+
vi.mock('../../../lib/api-hooks', () => ({
|
|
35
|
+
useResources: () => ({
|
|
36
|
+
events: { useQuery: mockEventsUseQuery },
|
|
37
|
+
annotations: { useQuery: mockAnnotationsUseQuery },
|
|
38
|
+
}),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Mock HistoryEvent to avoid deep rendering and mocking all its dependencies
|
|
42
|
+
const MockHistoryEvent = vi.fn(({ event }: any) => (
|
|
43
|
+
<div data-testid={`history-event-${event.event.id}`}>
|
|
44
|
+
{event.event.type}
|
|
45
|
+
</div>
|
|
46
|
+
));
|
|
47
|
+
|
|
48
|
+
vi.mock('../HistoryEvent', () => ({
|
|
49
|
+
HistoryEvent: (props: any) => MockHistoryEvent(props),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
import { getAnnotationUriFromEvent } from '@semiont/core';
|
|
53
|
+
const mockGetAnnotationUri = getAnnotationUriFromEvent as ReturnType<typeof vi.fn>;
|
|
54
|
+
|
|
55
|
+
const testRUri = 'http://localhost/resources/res-1' as ResourceUri;
|
|
56
|
+
|
|
57
|
+
function makeStoredEvent(id: string, type: string, seq: number, overrides: Record<string, any> = {}): StoredEvent {
|
|
58
|
+
return {
|
|
59
|
+
event: {
|
|
60
|
+
id,
|
|
61
|
+
type,
|
|
62
|
+
timestamp: '2026-03-06T12:00:00Z',
|
|
63
|
+
resourceId: 'http://localhost/resources/res-1',
|
|
64
|
+
userId: 'user-1',
|
|
65
|
+
version: 1,
|
|
66
|
+
payload: {},
|
|
67
|
+
...overrides,
|
|
68
|
+
},
|
|
69
|
+
metadata: {
|
|
70
|
+
sequenceNumber: seq,
|
|
71
|
+
storedAt: '2026-03-06T12:00:00Z',
|
|
72
|
+
},
|
|
73
|
+
} as StoredEvent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const MockLink = ({ href, children, ...props }: any) => <a href={href} {...props}>{children}</a>;
|
|
77
|
+
const mockRoutes = {
|
|
78
|
+
resourceDetail: (id: string) => `/resources/${id}`,
|
|
79
|
+
} as any;
|
|
80
|
+
|
|
81
|
+
describe('AnnotationHistory', () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
mockGetAnnotationUri.mockReturnValue(null);
|
|
85
|
+
mockAnnotationsUseQuery.mockReturnValue({ data: { annotations: [] } });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('renders loading state', () => {
|
|
89
|
+
mockEventsUseQuery.mockReturnValue({ data: undefined, isLoading: true, isError: false });
|
|
90
|
+
|
|
91
|
+
renderWithProviders(
|
|
92
|
+
<AnnotationHistory
|
|
93
|
+
rUri={testRUri}
|
|
94
|
+
Link={MockLink}
|
|
95
|
+
routes={mockRoutes}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(screen.getByText('History')).toBeInTheDocument();
|
|
100
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('renders null on error', () => {
|
|
104
|
+
mockEventsUseQuery.mockReturnValue({ data: undefined, isLoading: false, isError: true });
|
|
105
|
+
|
|
106
|
+
const { container } = renderWithProviders(
|
|
107
|
+
<AnnotationHistory
|
|
108
|
+
rUri={testRUri}
|
|
109
|
+
Link={MockLink}
|
|
110
|
+
routes={mockRoutes}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(container.innerHTML).toBe('');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('renders null when no events', () => {
|
|
118
|
+
mockEventsUseQuery.mockReturnValue({ data: { events: [] }, isLoading: false, isError: false });
|
|
119
|
+
|
|
120
|
+
const { container } = renderWithProviders(
|
|
121
|
+
<AnnotationHistory
|
|
122
|
+
rUri={testRUri}
|
|
123
|
+
Link={MockLink}
|
|
124
|
+
routes={mockRoutes}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(container.innerHTML).toBe('');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('renders events sorted by sequence number', () => {
|
|
132
|
+
const events = [
|
|
133
|
+
makeStoredEvent('evt-3', 'annotation.added', 3),
|
|
134
|
+
makeStoredEvent('evt-1', 'resource.created', 1),
|
|
135
|
+
makeStoredEvent('evt-2', 'annotation.added', 2),
|
|
136
|
+
];
|
|
137
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
138
|
+
|
|
139
|
+
renderWithProviders(
|
|
140
|
+
<AnnotationHistory
|
|
141
|
+
rUri={testRUri}
|
|
142
|
+
Link={MockLink}
|
|
143
|
+
routes={mockRoutes}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(screen.getByText('History')).toBeInTheDocument();
|
|
148
|
+
// All three events rendered
|
|
149
|
+
expect(screen.getByTestId('history-event-evt-1')).toBeInTheDocument();
|
|
150
|
+
expect(screen.getByTestId('history-event-evt-2')).toBeInTheDocument();
|
|
151
|
+
expect(screen.getByTestId('history-event-evt-3')).toBeInTheDocument();
|
|
152
|
+
|
|
153
|
+
// Verify HistoryEvent was called with events in sequence order
|
|
154
|
+
const calls = MockHistoryEvent.mock.calls;
|
|
155
|
+
expect(calls[0][0].event.event.id).toBe('evt-1');
|
|
156
|
+
expect(calls[1][0].event.event.id).toBe('evt-2');
|
|
157
|
+
expect(calls[2][0].event.event.id).toBe('evt-3');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('filters out job events', () => {
|
|
161
|
+
const events = [
|
|
162
|
+
makeStoredEvent('evt-1', 'resource.created', 1),
|
|
163
|
+
makeStoredEvent('evt-2', 'job.started', 2),
|
|
164
|
+
makeStoredEvent('evt-3', 'job.progress', 3),
|
|
165
|
+
makeStoredEvent('evt-4', 'job.completed', 4),
|
|
166
|
+
makeStoredEvent('evt-5', 'annotation.added', 5),
|
|
167
|
+
];
|
|
168
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
169
|
+
|
|
170
|
+
renderWithProviders(
|
|
171
|
+
<AnnotationHistory
|
|
172
|
+
rUri={testRUri}
|
|
173
|
+
Link={MockLink}
|
|
174
|
+
routes={mockRoutes}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Only non-job events should render
|
|
179
|
+
expect(screen.getByTestId('history-event-evt-1')).toBeInTheDocument();
|
|
180
|
+
expect(screen.getByTestId('history-event-evt-5')).toBeInTheDocument();
|
|
181
|
+
expect(screen.queryByTestId('history-event-evt-2')).not.toBeInTheDocument();
|
|
182
|
+
expect(screen.queryByTestId('history-event-evt-3')).not.toBeInTheDocument();
|
|
183
|
+
expect(screen.queryByTestId('history-event-evt-4')).not.toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('passes isRelated=true when hoveredAnnotationId matches event', () => {
|
|
187
|
+
const annotationUri = 'http://localhost/annotations/ann-1';
|
|
188
|
+
mockGetAnnotationUri.mockReturnValue(annotationUri);
|
|
189
|
+
|
|
190
|
+
const events = [
|
|
191
|
+
makeStoredEvent('evt-1', 'annotation.added', 1),
|
|
192
|
+
];
|
|
193
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
194
|
+
|
|
195
|
+
renderWithProviders(
|
|
196
|
+
<AnnotationHistory
|
|
197
|
+
rUri={testRUri}
|
|
198
|
+
hoveredAnnotationId={annotationUri}
|
|
199
|
+
Link={MockLink}
|
|
200
|
+
routes={mockRoutes}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const call = MockHistoryEvent.mock.calls[0][0];
|
|
205
|
+
expect(call.isRelated).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('passes isRelated=false when hoveredAnnotationId does not match', () => {
|
|
209
|
+
mockGetAnnotationUri.mockReturnValue('http://localhost/annotations/ann-other');
|
|
210
|
+
|
|
211
|
+
const events = [
|
|
212
|
+
makeStoredEvent('evt-1', 'annotation.added', 1),
|
|
213
|
+
];
|
|
214
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
215
|
+
|
|
216
|
+
renderWithProviders(
|
|
217
|
+
<AnnotationHistory
|
|
218
|
+
rUri={testRUri}
|
|
219
|
+
hoveredAnnotationId="http://localhost/annotations/ann-1"
|
|
220
|
+
Link={MockLink}
|
|
221
|
+
routes={mockRoutes}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const call = MockHistoryEvent.mock.calls[0][0];
|
|
226
|
+
expect(call.isRelated).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('passes isRelated=false when no hoveredAnnotationId', () => {
|
|
230
|
+
const events = [
|
|
231
|
+
makeStoredEvent('evt-1', 'annotation.added', 1),
|
|
232
|
+
];
|
|
233
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
234
|
+
|
|
235
|
+
renderWithProviders(
|
|
236
|
+
<AnnotationHistory
|
|
237
|
+
rUri={testRUri}
|
|
238
|
+
Link={MockLink}
|
|
239
|
+
routes={mockRoutes}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const call = MockHistoryEvent.mock.calls[0][0];
|
|
244
|
+
expect(call.isRelated).toBe(false);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('passes onEventClick and onEventHover to HistoryEvent', () => {
|
|
248
|
+
const onEventClick = vi.fn();
|
|
249
|
+
const onEventHover = vi.fn();
|
|
250
|
+
|
|
251
|
+
const events = [
|
|
252
|
+
makeStoredEvent('evt-1', 'resource.created', 1),
|
|
253
|
+
];
|
|
254
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
255
|
+
|
|
256
|
+
renderWithProviders(
|
|
257
|
+
<AnnotationHistory
|
|
258
|
+
rUri={testRUri}
|
|
259
|
+
onEventClick={onEventClick}
|
|
260
|
+
onEventHover={onEventHover}
|
|
261
|
+
Link={MockLink}
|
|
262
|
+
routes={mockRoutes}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const call = MockHistoryEvent.mock.calls[0][0];
|
|
267
|
+
expect(call.onEventClick).toBe(onEventClick);
|
|
268
|
+
expect(call.onEventHover).toBe(onEventHover);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('does not pass onEventClick/onEventHover when not provided', () => {
|
|
272
|
+
const events = [
|
|
273
|
+
makeStoredEvent('evt-1', 'resource.created', 1),
|
|
274
|
+
];
|
|
275
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
276
|
+
|
|
277
|
+
renderWithProviders(
|
|
278
|
+
<AnnotationHistory
|
|
279
|
+
rUri={testRUri}
|
|
280
|
+
Link={MockLink}
|
|
281
|
+
routes={mockRoutes}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const call = MockHistoryEvent.mock.calls[0][0];
|
|
286
|
+
expect(call.onEventClick).toBeUndefined();
|
|
287
|
+
expect(call.onEventHover).toBeUndefined();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('passes annotations from useQuery to HistoryEvent', () => {
|
|
291
|
+
const mockAnnotations = [{ id: 'ann-1', body: [] }];
|
|
292
|
+
mockAnnotationsUseQuery.mockReturnValue({ data: { annotations: mockAnnotations } });
|
|
293
|
+
|
|
294
|
+
const events = [
|
|
295
|
+
makeStoredEvent('evt-1', 'resource.created', 1),
|
|
296
|
+
];
|
|
297
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
298
|
+
|
|
299
|
+
renderWithProviders(
|
|
300
|
+
<AnnotationHistory
|
|
301
|
+
rUri={testRUri}
|
|
302
|
+
Link={MockLink}
|
|
303
|
+
routes={mockRoutes}
|
|
304
|
+
/>
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const call = MockHistoryEvent.mock.calls[0][0];
|
|
308
|
+
expect(call.annotations).toEqual(mockAnnotations);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('defaults annotations to empty array when no data', () => {
|
|
312
|
+
mockAnnotationsUseQuery.mockReturnValue({ data: undefined });
|
|
313
|
+
|
|
314
|
+
const events = [
|
|
315
|
+
makeStoredEvent('evt-1', 'resource.created', 1),
|
|
316
|
+
];
|
|
317
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
318
|
+
|
|
319
|
+
renderWithProviders(
|
|
320
|
+
<AnnotationHistory
|
|
321
|
+
rUri={testRUri}
|
|
322
|
+
Link={MockLink}
|
|
323
|
+
routes={mockRoutes}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const call = MockHistoryEvent.mock.calls[0][0];
|
|
328
|
+
expect(call.annotations).toEqual([]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('renders history panel structure with title and list', () => {
|
|
332
|
+
const events = [
|
|
333
|
+
makeStoredEvent('evt-1', 'resource.created', 1),
|
|
334
|
+
];
|
|
335
|
+
mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
|
|
336
|
+
|
|
337
|
+
const { container } = renderWithProviders(
|
|
338
|
+
<AnnotationHistory
|
|
339
|
+
rUri={testRUri}
|
|
340
|
+
Link={MockLink}
|
|
341
|
+
routes={mockRoutes}
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(container.querySelector('.semiont-history-panel')).toBeInTheDocument();
|
|
346
|
+
expect(container.querySelector('.semiont-history-panel__title')).toBeInTheDocument();
|
|
347
|
+
expect(container.querySelector('.semiont-history-panel__list')).toBeInTheDocument();
|
|
348
|
+
});
|
|
349
|
+
});
|