@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.
Files changed (34) hide show
  1. package/dist/index.d.mts +8 -0
  2. package/dist/index.mjs +252 -166
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +3 -3
  5. package/src/components/CodeMirrorRenderer.tsx +71 -203
  6. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +142 -0
  7. package/src/components/__tests__/LiveRegion.hooks.test.tsx +79 -0
  8. package/src/components/__tests__/ResizeHandle.test.tsx +165 -0
  9. package/src/components/__tests__/SessionExpiryBanner.test.tsx +123 -0
  10. package/src/components/__tests__/StatusDisplay.test.tsx +160 -0
  11. package/src/components/__tests__/Toolbar.test.tsx +110 -0
  12. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +285 -0
  13. package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +273 -0
  14. package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +90 -0
  15. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +129 -0
  16. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +180 -0
  17. package/src/components/navigation/__tests__/ObservableLink.test.tsx +90 -0
  18. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +169 -0
  19. package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +371 -0
  20. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +2 -0
  21. package/src/components/resource/AnnotateView.tsx +27 -153
  22. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +349 -0
  23. package/src/components/resource/__tests__/HistoryEvent.test.tsx +492 -0
  24. package/src/components/resource/__tests__/event-formatting.test.ts +273 -0
  25. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +226 -0
  26. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +188 -0
  27. package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +69 -0
  28. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +445 -0
  29. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +271 -0
  30. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +210 -0
  31. package/src/components/settings/__tests__/SettingsPanel.test.tsx +190 -0
  32. package/src/components/viewers/__tests__/ImageViewer.test.tsx +63 -0
  33. package/src/integrations/__tests__/css-modules-helper.test.tsx +225 -0
  34. 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
+ });