@semiont/react-ui 0.4.20 → 0.4.21

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 (108) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
  3. package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-KEDFYI6N.mjs +7788 -0
  6. package/dist/chunk-KEDFYI6N.mjs.map +1 -0
  7. package/dist/index.d.mts +171 -1140
  8. package/dist/index.mjs +3263 -13644
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +46 -21
  11. package/dist/test-utils.mjs +2499 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +1 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +9 -11
  16. package/src/components/StatusDisplay.tsx +42 -16
  17. package/src/components/Toolbar.tsx +4 -4
  18. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  19. package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
  20. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  21. package/src/components/annotation/AnnotateToolbar.tsx +8 -7
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  23. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
  24. package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
  25. package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
  26. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  27. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  28. package/src/components/modals/ResourceSearchModal.tsx +10 -6
  29. package/src/components/modals/SearchModal.tsx +10 -6
  30. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  31. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  32. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  33. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  34. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  35. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  36. package/src/components/navigation/ObservableLink.tsx +6 -6
  37. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  38. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  39. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  40. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
  41. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
  42. package/src/components/resource/AnnotateView.tsx +7 -6
  43. package/src/components/resource/AnnotationHistory.tsx +9 -12
  44. package/src/components/resource/BrowseView.tsx +8 -7
  45. package/src/components/resource/ResourceViewer.tsx +17 -25
  46. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  47. package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
  48. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
  49. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  50. package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
  51. package/src/components/resource/panels/AssistSection.tsx +11 -13
  52. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  53. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  54. package/src/components/resource/panels/CommentsPanel.tsx +9 -11
  55. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  56. package/src/components/resource/panels/HighlightPanel.tsx +10 -11
  57. package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
  58. package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
  59. package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
  60. package/src/components/resource/panels/TagEntry.tsx +5 -4
  61. package/src/components/resource/panels/TaggingPanel.tsx +10 -16
  62. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
  63. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
  64. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  65. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
  66. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
  67. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
  68. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  69. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  70. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
  71. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
  72. package/src/components/settings/SettingsPanel.tsx +8 -8
  73. package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
  74. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
  75. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  76. package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
  77. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
  78. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  79. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
  80. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
  81. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  82. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  83. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  84. package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
  85. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  86. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  87. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
  88. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
  89. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  90. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  91. package/dist/chunk-OZICDVH7.mjs +0 -62
  92. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  93. package/dist/chunk-R4CCMFJH.mjs +0 -877
  94. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  95. package/dist/chunk-VN5NY4SN.mjs +0 -200
  96. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  97. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  98. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  99. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  100. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  101. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  102. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  103. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  104. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  105. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  106. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  107. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  108. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -9,9 +9,9 @@ import { JsonLdView } from '../annotation-popups/JsonLdView';
9
9
  import type { components } from '@semiont/core';
10
10
  import { resourceId as toResourceId, annotationId as toAnnotationId } from '@semiont/core';
11
11
  import { getExactText, getTargetSelector, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/api-client';
12
- import { useEventBus } from '../../contexts/EventBusContext';
13
12
  import { useEventSubscriptions } from '../../contexts/useEventSubscription';
14
- import { useCacheManager } from '../../contexts/CacheContext';
13
+ import { useSemiont } from '../../session/SemiontProvider';
14
+ import { useObservable } from '../../hooks/useObservable';
15
15
  import { useObservableExternalNavigation } from '../../hooks/useObservableBrowse';
16
16
  import { ANNOTATORS } from '../../lib/annotation-registry';
17
17
  import type { AnnotationsCollection } from '../../types/annotation-props';
@@ -29,7 +29,7 @@ type SemiontResource = components['schemas']['ResourceDescriptor'];
29
29
  * - No manual refetch needed - events handle cache invalidation
30
30
  *
31
31
  * Requirements:
32
- * - Must be wrapped in MakeMeaningEventBusProvider (provides event bus)
32
+ * - Must be wrapped in SemiontProvider (which owns the session's event bus)
33
33
  * - Must be wrapped in CacheContext (provides cache manager)
34
34
  *
35
35
  * Event flow:
@@ -49,7 +49,7 @@ interface Props {
49
49
 
50
50
  /**
51
51
  * @emits mark:delete - User requested to delete annotation. Payload: { annotationId: string }
52
- * @emits browse:panel-open - Request to open panel with annotation. Payload: { panel: string, scrollToAnnotationId?: string, motivation?: Motivation }
52
+ * @emits panel:open - Request to open panel with annotation. Payload: { panel: string, scrollToAnnotationId?: string, motivation?: Motivation }
53
53
  *
54
54
  * @subscribes mark:mode-toggled - Toggles between browse and annotate mode. Payload: { mode: 'browse' | 'annotate' }
55
55
  * @subscribes mark:added - New annotation was added. Payload: { annotation: Annotation }
@@ -71,8 +71,8 @@ export function ResourceViewer({
71
71
  const t = useTranslations('ResourceViewer');
72
72
  const documentViewerRef = useRef<HTMLDivElement>(null);
73
73
 
74
- // Get unified event bus for emitting UI events
75
- const eventBus = useEventBus();
74
+ const browser = useSemiont();
75
+ const session = useObservable(browser.activeSession$);
76
76
 
77
77
  // Get observable navigation for event-driven routing
78
78
  const navigate = useObservableExternalNavigation();
@@ -120,27 +120,19 @@ export function ResourceViewer({
120
120
  // Determine active view based on annotate mode
121
121
  const activeView = annotateMode ? 'annotate' : 'browse';
122
122
 
123
- // Event-based cache invalidation - subscribe to make-meaning events
124
- // This replaces manual onRefetchAnnotations calls with automatic updates
125
- const cacheManager = useCacheManager();
123
+ const semiont = session?.client;
126
124
 
127
125
  const handleAnnotateAdded = useCallback(() => {
128
- if (cacheManager) {
129
- cacheManager.invalidateAnnotations(rUri);
130
- }
131
- }, [cacheManager, rUri]);
126
+ semiont?.browse.invalidateAnnotationList(rUri);
127
+ }, [semiont, rUri]);
132
128
 
133
129
  const handleAnnotateRemoved = useCallback(() => {
134
- if (cacheManager) {
135
- cacheManager.invalidateAnnotations(rUri);
136
- }
137
- }, [cacheManager, rUri]);
130
+ semiont?.browse.invalidateAnnotationList(rUri);
131
+ }, [semiont, rUri]);
138
132
 
139
133
  const handleAnnotateBodyUpdated = useCallback(() => {
140
- if (cacheManager) {
141
- cacheManager.invalidateAnnotations(rUri);
142
- }
143
- }, [cacheManager, rUri]);
134
+ semiont?.browse.invalidateAnnotationList(rUri);
135
+ }, [semiont, rUri]);
144
136
 
145
137
  // Annotation toolbar state - persisted in localStorage
146
138
  const [selectedMotivation, setSelectedMotivation] = useState<SelectionMotivation | null>(() => {
@@ -251,8 +243,8 @@ export function ResourceViewer({
251
243
 
252
244
  // Handle deleting annotations - emit event instead of direct call
253
245
  const handleDeleteAnnotation = useCallback((id: string) => {
254
- eventBus.get('mark:delete').next({ annotationId: toAnnotationId(id) });
255
- }, []); // eventBus is stable
246
+ session?.client.emit('mark:delete', { annotationId: toAnnotationId(id) });
247
+ }, [session]);
256
248
 
257
249
  // Handle annotation clicks - memoized
258
250
  const handleAnnotationClick = useCallback((annotation: Annotation, event?: React.MouseEvent) => {
@@ -337,8 +329,8 @@ export function ResourceViewer({
337
329
 
338
330
  // All annotations open the unified annotations panel
339
331
  // The panel internally switches tabs based on the motivation → tab mapping in UnifiedAnnotationsPanel
340
- eventBus.get('browse:panel-open').next({ panel: 'annotations', scrollToAnnotationId: annotationId, motivation });
341
- }, [highlights, references, assessments, comments, tags, handleAnnotationClick, selectedClick]);
332
+ browser.emit('panel:open', { panel: 'annotations', scrollToAnnotationId: annotationId, motivation });
333
+ }, [highlights, references, assessments, comments, tags, handleAnnotationClick, selectedClick, session]);
342
334
 
343
335
  // Event subscriptions - Combined into single useEventSubscriptions call to prevent hook ordering issues
344
336
  // IMPORTANT: All event subscriptions MUST be in a single call to maintain consistent hook order between renders
@@ -4,7 +4,8 @@ import { screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
  import { AnnotationHistory } from '../AnnotationHistory';
6
6
  import { renderWithProviders } from '../../../test-utils';
7
- import type { StoredEvent, ResourceId } from '@semiont/core';
7
+ import type { ResourceId } from '@semiont/core';
8
+ import { BehaviorSubject } from 'rxjs';
8
9
 
9
10
  // Mock @semiont/core - must use importOriginal to preserve EventBus etc.
10
11
  vi.mock('@semiont/core', async (importOriginal) => {
@@ -27,16 +28,26 @@ vi.mock('../../../contexts/TranslationContext', () => ({
27
28
  TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
28
29
  }));
29
30
 
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
- }));
31
+ const eventsSubject = new BehaviorSubject<any[] | undefined>(undefined);
32
+ const annotationsSubject = new BehaviorSubject<any[] | undefined>(undefined);
33
+
34
+ const stableMockClient = {
35
+ browse: {
36
+ events: () => eventsSubject.asObservable(),
37
+ annotations: () => annotationsSubject.asObservable(),
38
+ },
39
+ };
40
+ const stableMockSession = { client: stableMockClient };
41
+ const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
42
+ const stableMockBrowser = { activeSession$: stableActiveSession$ };
43
+
44
+ vi.mock('../../../session/SemiontProvider', async (importOriginal) => {
45
+ const actual = await importOriginal<typeof import('../../../session/SemiontProvider')>();
46
+ return {
47
+ ...actual,
48
+ useSemiont: () => stableMockBrowser,
49
+ };
50
+ });
40
51
 
41
52
  // Mock HistoryEvent to avoid deep rendering and mocking all its dependencies
42
53
  const MockHistoryEvent = vi.fn(({ event }: any) => (
@@ -81,11 +92,12 @@ describe('AnnotationHistory', () => {
81
92
  beforeEach(() => {
82
93
  vi.clearAllMocks();
83
94
  mockGetAnnotationUri.mockReturnValue(null);
84
- mockAnnotationsUseQuery.mockReturnValue({ data: { annotations: [] } });
95
+ eventsSubject.next(undefined);
96
+ annotationsSubject.next([]);
85
97
  });
86
98
 
87
99
  it('renders loading state', () => {
88
- mockEventsUseQuery.mockReturnValue({ data: undefined, isLoading: true, isError: false });
100
+ eventsSubject.next(undefined);
89
101
 
90
102
  renderWithProviders(
91
103
  <AnnotationHistory
@@ -99,22 +111,8 @@ describe('AnnotationHistory', () => {
99
111
  expect(screen.getByText('Loading...')).toBeInTheDocument();
100
112
  });
101
113
 
102
- it('renders null on error', () => {
103
- mockEventsUseQuery.mockReturnValue({ data: undefined, isLoading: false, isError: true });
104
-
105
- const { container } = renderWithProviders(
106
- <AnnotationHistory
107
- rUri={testRId}
108
- Link={MockLink}
109
- routes={mockRoutes}
110
- />
111
- );
112
-
113
- expect(container.innerHTML).toBe('');
114
- });
115
-
116
114
  it('renders null when no events', () => {
117
- mockEventsUseQuery.mockReturnValue({ data: { events: [] }, isLoading: false, isError: false });
115
+ eventsSubject.next([]);
118
116
 
119
117
  const { container } = renderWithProviders(
120
118
  <AnnotationHistory
@@ -129,11 +127,11 @@ describe('AnnotationHistory', () => {
129
127
 
130
128
  it('renders events sorted by sequence number', () => {
131
129
  const events = [
132
- makeStoredEvent('evt-3', 'mark:added', 3),
133
- makeStoredEvent('evt-1', 'yield:created', 1),
134
- makeStoredEvent('evt-2', 'mark:added', 2),
130
+ makeStoredEvent('e3', 'mark:added', 3),
131
+ makeStoredEvent('e1', 'mark:added', 1),
132
+ makeStoredEvent('e2', 'mark:added', 2),
135
133
  ];
136
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
134
+ eventsSubject.next(events);
137
135
 
138
136
  renderWithProviders(
139
137
  <AnnotationHistory
@@ -143,195 +141,59 @@ describe('AnnotationHistory', () => {
143
141
  />
144
142
  );
145
143
 
146
- expect(screen.getByText('History')).toBeInTheDocument();
147
- // All three events rendered
148
- expect(screen.getByTestId('history-event-evt-1')).toBeInTheDocument();
149
- expect(screen.getByTestId('history-event-evt-2')).toBeInTheDocument();
150
- expect(screen.getByTestId('history-event-evt-3')).toBeInTheDocument();
151
-
152
- // Verify HistoryEvent was called with events in sequence order
153
- const calls = MockHistoryEvent.mock.calls;
154
- expect(calls[0][0].event.id).toBe('evt-1'); // .event is the React prop name
155
- expect(calls[1][0].event.id).toBe('evt-2');
156
- expect(calls[2][0].event.id).toBe('evt-3');
144
+ const renderedEvents = screen.getAllByTestId(/^history-event-/);
145
+ expect(renderedEvents).toHaveLength(3);
146
+ expect(renderedEvents[0]).toHaveAttribute('data-testid', 'history-event-e1');
147
+ expect(renderedEvents[1]).toHaveAttribute('data-testid', 'history-event-e2');
148
+ expect(renderedEvents[2]).toHaveAttribute('data-testid', 'history-event-e3');
157
149
  });
158
150
 
159
151
  it('filters out job events', () => {
160
152
  const events = [
161
- makeStoredEvent('evt-1', 'yield:created', 1),
162
- makeStoredEvent('evt-2', 'job:started', 2),
163
- makeStoredEvent('evt-3', 'job:progress', 3),
164
- makeStoredEvent('evt-4', 'job:completed', 4),
165
- makeStoredEvent('evt-5', 'mark:added', 5),
166
- ];
167
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
168
-
169
- renderWithProviders(
170
- <AnnotationHistory
171
- rUri={testRId}
172
- Link={MockLink}
173
- routes={mockRoutes}
174
- />
175
- );
176
-
177
- // Only non-job events should render
178
- expect(screen.getByTestId('history-event-evt-1')).toBeInTheDocument();
179
- expect(screen.getByTestId('history-event-evt-5')).toBeInTheDocument();
180
- expect(screen.queryByTestId('history-event-evt-2')).not.toBeInTheDocument();
181
- expect(screen.queryByTestId('history-event-evt-3')).not.toBeInTheDocument();
182
- expect(screen.queryByTestId('history-event-evt-4')).not.toBeInTheDocument();
183
- });
184
-
185
- it('passes isRelated=true when hoveredAnnotationId matches event', () => {
186
- const annotationUri = 'http://localhost/annotations/ann-1';
187
- mockGetAnnotationUri.mockReturnValue(annotationUri);
188
-
189
- const events = [
190
- makeStoredEvent('evt-1', 'mark:added', 1),
153
+ makeStoredEvent('e1', 'mark:added', 1),
154
+ makeStoredEvent('e2', 'job:started', 2),
155
+ makeStoredEvent('e3', 'job:progress', 3),
156
+ makeStoredEvent('e4', 'job:completed', 4),
157
+ makeStoredEvent('e5', 'mark:body-updated', 5),
191
158
  ];
192
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
159
+ eventsSubject.next(events);
193
160
 
194
161
  renderWithProviders(
195
162
  <AnnotationHistory
196
163
  rUri={testRId}
197
- hoveredAnnotationId={annotationUri}
198
164
  Link={MockLink}
199
165
  routes={mockRoutes}
200
166
  />
201
167
  );
202
168
 
203
- const call = MockHistoryEvent.mock.calls[0][0];
204
- expect(call.isRelated).toBe(true);
169
+ const renderedEvents = screen.getAllByTestId(/^history-event-/);
170
+ expect(renderedEvents).toHaveLength(2);
171
+ expect(renderedEvents[0]).toHaveAttribute('data-testid', 'history-event-e1');
172
+ expect(renderedEvents[1]).toHaveAttribute('data-testid', 'history-event-e5');
205
173
  });
206
174
 
207
- it('passes isRelated=false when hoveredAnnotationId does not match', () => {
208
- mockGetAnnotationUri.mockReturnValue('http://localhost/annotations/ann-other');
209
-
210
- const events = [
211
- makeStoredEvent('evt-1', 'mark:added', 1),
212
- ];
213
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
175
+ it('passes isRelated when hovered annotation matches event', () => {
176
+ const events = [makeStoredEvent('e1', 'mark:added', 1)];
177
+ eventsSubject.next(events);
178
+ mockGetAnnotationUri.mockReturnValue('ann-1');
214
179
 
215
180
  renderWithProviders(
216
181
  <AnnotationHistory
217
182
  rUri={testRId}
218
- hoveredAnnotationId="http://localhost/annotations/ann-1"
183
+ hoveredAnnotationId="ann-1"
219
184
  Link={MockLink}
220
185
  routes={mockRoutes}
221
186
  />
222
187
  );
223
188
 
224
- const call = MockHistoryEvent.mock.calls[0][0];
225
- expect(call.isRelated).toBe(false);
226
- });
227
-
228
- it('passes isRelated=false when no hoveredAnnotationId', () => {
229
- const events = [
230
- makeStoredEvent('evt-1', 'mark:added', 1),
231
- ];
232
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
233
-
234
- renderWithProviders(
235
- <AnnotationHistory
236
- rUri={testRId}
237
- Link={MockLink}
238
- routes={mockRoutes}
239
- />
189
+ expect(MockHistoryEvent).toHaveBeenCalledWith(
190
+ expect.objectContaining({ isRelated: true })
240
191
  );
241
-
242
- const call = MockHistoryEvent.mock.calls[0][0];
243
- expect(call.isRelated).toBe(false);
244
- });
245
-
246
- it('passes onEventClick and onEventHover to HistoryEvent', () => {
247
- const onEventClick = vi.fn();
248
- const onEventHover = vi.fn();
249
-
250
- const events = [
251
- makeStoredEvent('evt-1', 'yield:created', 1),
252
- ];
253
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
254
-
255
- renderWithProviders(
256
- <AnnotationHistory
257
- rUri={testRId}
258
- onEventClick={onEventClick}
259
- onEventHover={onEventHover}
260
- Link={MockLink}
261
- routes={mockRoutes}
262
- />
263
- );
264
-
265
- const call = MockHistoryEvent.mock.calls[0][0];
266
- expect(call.onEventClick).toBe(onEventClick);
267
- expect(call.onEventHover).toBe(onEventHover);
268
- });
269
-
270
- it('does not pass onEventClick/onEventHover when not provided', () => {
271
- const events = [
272
- makeStoredEvent('evt-1', 'yield:created', 1),
273
- ];
274
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
275
-
276
- renderWithProviders(
277
- <AnnotationHistory
278
- rUri={testRId}
279
- Link={MockLink}
280
- routes={mockRoutes}
281
- />
282
- );
283
-
284
- const call = MockHistoryEvent.mock.calls[0][0];
285
- expect(call.onEventClick).toBeUndefined();
286
- expect(call.onEventHover).toBeUndefined();
287
- });
288
-
289
- it('passes annotations from useQuery to HistoryEvent', () => {
290
- const mockAnnotations = [{ id: 'ann-1', body: [] }];
291
- mockAnnotationsUseQuery.mockReturnValue({ data: { annotations: mockAnnotations } });
292
-
293
- const events = [
294
- makeStoredEvent('evt-1', 'yield:created', 1),
295
- ];
296
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
297
-
298
- renderWithProviders(
299
- <AnnotationHistory
300
- rUri={testRId}
301
- Link={MockLink}
302
- routes={mockRoutes}
303
- />
304
- );
305
-
306
- const call = MockHistoryEvent.mock.calls[0][0];
307
- expect(call.annotations).toEqual(mockAnnotations);
308
- });
309
-
310
- it('defaults annotations to empty array when no data', () => {
311
- mockAnnotationsUseQuery.mockReturnValue({ data: undefined });
312
-
313
- const events = [
314
- makeStoredEvent('evt-1', 'yield:created', 1),
315
- ];
316
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
317
-
318
- renderWithProviders(
319
- <AnnotationHistory
320
- rUri={testRId}
321
- Link={MockLink}
322
- routes={mockRoutes}
323
- />
324
- );
325
-
326
- const call = MockHistoryEvent.mock.calls[0][0];
327
- expect(call.annotations).toEqual([]);
328
192
  });
329
193
 
330
194
  it('renders history panel structure with title and list', () => {
331
- const events = [
332
- makeStoredEvent('evt-1', 'yield:created', 1),
333
- ];
334
- mockEventsUseQuery.mockReturnValue({ data: { events }, isLoading: false, isError: false });
195
+ const events = [makeStoredEvent('e1', 'mark:added', 1)];
196
+ eventsSubject.next(events);
335
197
 
336
198
  const { container } = renderWithProviders(
337
199
  <AnnotationHistory
@@ -2,8 +2,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import React from 'react';
3
3
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import { BrowseView } from '../BrowseView';
5
- import type { components } from '@semiont/core';
6
- import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
5
+ import type { components, EventBus } from '@semiont/core';
6
+ import { createTestSemiontWrapper } from '../../../test-utils';
7
7
 
8
8
  type Annotation = components['schemas']['Annotation'];
9
9
 
@@ -97,68 +97,43 @@ interface TrackedEvent {
97
97
  function createEventTracker() {
98
98
  const events: TrackedEvent[] = [];
99
99
  const subscriptions: Set<string> = new Set();
100
-
101
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
102
- const eventBus = useEventBus();
103
-
104
- React.useEffect(() => {
105
- const handlers: Array<{ unsubscribe: () => void }> = [];
106
-
107
- // Track all annotation-related events
108
- const trackEvent = (eventName: string) => (payload: any) => {
109
- events.push({ event: eventName, payload });
110
- };
111
-
100
+ return {
101
+ events,
102
+ subscriptions,
103
+ clear: () => {
104
+ events.length = 0;
105
+ subscriptions.clear();
106
+ },
107
+ _attach(eventBus: EventBus) {
112
108
  const annotationEvents = [
113
109
  'beckon:hover',
114
110
  'browse:click',
115
111
  'beckon:focus',
116
112
  ] as const;
117
-
118
- annotationEvents.forEach(eventName => {
113
+ annotationEvents.forEach((eventName) => {
119
114
  subscriptions.add(eventName);
120
- const handler = trackEvent(eventName);
121
- const subscription = eventBus.get(eventName).subscribe(handler);
122
- handlers.push(subscription);
115
+ eventBus.get(eventName).subscribe((payload: any) => {
116
+ events.push({ event: eventName, payload });
117
+ });
123
118
  });
124
-
125
- return () => {
126
- handlers.forEach(sub => sub.unsubscribe());
127
- };
128
- }, [eventBus]);
129
-
130
- return <>{children}</>;
131
- }
132
-
133
- return {
134
- EventTrackingWrapper,
135
- events,
136
- subscriptions,
137
- clear: () => {
138
- events.length = 0;
139
- subscriptions.clear();
140
119
  },
141
120
  };
142
121
  }
143
122
 
144
- // Helper to render with providers - simple composition, no spy wrappers
145
123
  const renderWithProviders = (
146
124
  component: React.ReactElement,
147
125
  options: { newAnnotationIds?: Set<string> } = {}
148
126
  ) => {
149
- // Update the mock if new annotation IDs are provided
150
127
  if (options.newAnnotationIds) {
151
128
  mockNewAnnotationIds = options.newAnnotationIds;
152
129
  }
153
-
154
- return render(
155
- <EventBusProvider>
156
- {component}
157
- </EventBusProvider>
130
+ const { SemiontWrapper } = createTestSemiontWrapper();
131
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
132
+ <SemiontWrapper>{children}</SemiontWrapper>
158
133
  );
134
+ return render(component, { wrapper: Wrapper });
159
135
  };
160
136
 
161
- // Helper to render with event tracking
162
137
  const renderWithEventTracking = (
163
138
  component: React.ReactElement,
164
139
  tracker: ReturnType<typeof createEventTracker>,
@@ -167,14 +142,12 @@ const renderWithEventTracking = (
167
142
  if (options.newAnnotationIds) {
168
143
  mockNewAnnotationIds = options.newAnnotationIds;
169
144
  }
170
-
171
- return render(
172
- <EventBusProvider>
173
- <tracker.EventTrackingWrapper>
174
- {component}
175
- </tracker.EventTrackingWrapper>
176
- </EventBusProvider>
145
+ const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
146
+ tracker._attach(eventBus);
147
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
148
+ <SemiontWrapper>{children}</SemiontWrapper>
177
149
  );
150
+ return render(component, { wrapper: Wrapper });
178
151
  };
179
152
 
180
153
  // Test data fixtures
@@ -193,7 +166,6 @@ const createMockAnnotation = (motivation: string, id: string): Annotation => ({
193
166
  end: 10,
194
167
  },
195
168
  },
196
- body: [],
197
169
  });
198
170
 
199
171
  describe('BrowseView Component', () => {
@@ -515,35 +487,8 @@ describe('BrowseView Component', () => {
515
487
 
516
488
  describe('Performance - Event Listener Efficiency', () => {
517
489
  it('should handle many annotations efficiently through event delegation', async () => {
518
- // Create a composition-based event tracker that subscribes like a real consumer
519
490
  const eventTracker: Array<{ event: string; annotationId: string | null }> = [];
520
491
 
521
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
522
- const eventBus = useEventBus();
523
-
524
- React.useEffect(() => {
525
- // Subscribe to events like a real component would
526
- const handleHover = (payload: any) => {
527
- eventTracker.push({ event: 'beckon:hover', annotationId: payload?.annotationId ?? null });
528
- };
529
-
530
- const handleClick = (payload: any) => {
531
- eventTracker.push({ event: 'browse:click', annotationId: payload?.annotationId ?? null });
532
- };
533
-
534
- const subscription1 = eventBus.get('beckon:hover').subscribe(handleHover);
535
- const subscription2 = eventBus.get('browse:click').subscribe(handleClick);
536
-
537
- return () => {
538
- subscription1.unsubscribe();
539
- subscription2.unsubscribe();
540
- };
541
- }, [eventBus]);
542
-
543
- return <>{children}</>;
544
- }
545
-
546
- // Create many annotations
547
492
  const manyAnnotations = {
548
493
  highlights: Array.from({ length: 50 }, (_, i) =>
549
494
  createMockAnnotation('highlighting', `highlight-${i}`)
@@ -556,12 +501,18 @@ describe('BrowseView Component', () => {
556
501
  tags: [],
557
502
  };
558
503
 
504
+ const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
505
+ eventBus.get('beckon:hover').subscribe((payload: any) => {
506
+ eventTracker.push({ event: 'beckon:hover', annotationId: payload?.annotationId ?? null });
507
+ });
508
+ eventBus.get('browse:click').subscribe((payload: any) => {
509
+ eventTracker.push({ event: 'browse:click', annotationId: payload?.annotationId ?? null });
510
+ });
511
+
559
512
  const { container } = render(
560
- <EventBusProvider>
561
- <EventTrackingWrapper>
562
- <BrowseView {...defaultProps} annotations={manyAnnotations} />
563
- </EventTrackingWrapper>
564
- </EventBusProvider>
513
+ <SemiontWrapper>
514
+ <BrowseView {...defaultProps} annotations={manyAnnotations} />
515
+ </SemiontWrapper>
565
516
  );
566
517
 
567
518
  const browseContainer = container.querySelector('.semiont-browse-view__content');